Build Ginkgo binary

Signed-off-by: Dave Chen <dave.chen@arm.com>
This commit is contained in:
Dave Chen
2022-03-29 22:24:39 +08:00
parent ece0bb3522
commit 375b2a5fb2
74 changed files with 11223 additions and 8 deletions

View File

@@ -0,0 +1,61 @@
package build
import (
"fmt"
"github.com/onsi/ginkgo/v2/ginkgo/command"
"github.com/onsi/ginkgo/v2/ginkgo/internal"
"github.com/onsi/ginkgo/v2/types"
)
func BuildBuildCommand() command.Command {
var cliConfig = types.NewDefaultCLIConfig()
var goFlagsConfig = types.NewDefaultGoFlagsConfig()
flags, err := types.BuildBuildCommandFlagSet(&cliConfig, &goFlagsConfig)
if err != nil {
panic(err)
}
return command.Command{
Name: "build",
Flags: flags,
Usage: "ginkgo build <FLAGS> <PACKAGES>",
ShortDoc: "Build the passed in <PACKAGES> (or the package in the current directory if left blank).",
DocLink: "precompiling-suites",
Command: func(args []string, _ []string) {
var errors []error
cliConfig, goFlagsConfig, errors = types.VetAndInitializeCLIAndGoConfig(cliConfig, goFlagsConfig)
command.AbortIfErrors("Ginkgo detected configuration issues:", errors)
buildSpecs(args, cliConfig, goFlagsConfig)
},
}
}
func buildSpecs(args []string, cliConfig types.CLIConfig, goFlagsConfig types.GoFlagsConfig) {
suites := internal.FindSuites(args, cliConfig, false).WithoutState(internal.TestSuiteStateSkippedByFilter)
if len(suites) == 0 {
command.AbortWith("Found no test suites")
}
opc := internal.NewOrderedParallelCompiler(cliConfig.ComputedNumCompilers())
opc.StartCompiling(suites, goFlagsConfig)
for {
suiteIdx, suite := opc.Next()
if suiteIdx >= len(suites) {
break
}
suites[suiteIdx] = suite
if suite.State.Is(internal.TestSuiteStateFailedToCompile) {
fmt.Println(suite.CompilationError.Error())
} else {
fmt.Printf("Compiled %s.test\n", suite.PackageName)
}
}
if suites.CountWithState(internal.TestSuiteStateFailedToCompile) > 0 {
command.AbortWith("Failed to compile all tests")
}
}

View File

@@ -0,0 +1,61 @@
package command
import "fmt"
type AbortDetails struct {
ExitCode int
Error error
EmitUsage bool
}
func Abort(details AbortDetails) {
panic(details)
}
func AbortGracefullyWith(format string, args ...interface{}) {
Abort(AbortDetails{
ExitCode: 0,
Error: fmt.Errorf(format, args...),
EmitUsage: false,
})
}
func AbortWith(format string, args ...interface{}) {
Abort(AbortDetails{
ExitCode: 1,
Error: fmt.Errorf(format, args...),
EmitUsage: false,
})
}
func AbortWithUsage(format string, args ...interface{}) {
Abort(AbortDetails{
ExitCode: 1,
Error: fmt.Errorf(format, args...),
EmitUsage: true,
})
}
func AbortIfError(preamble string, err error) {
if err != nil {
Abort(AbortDetails{
ExitCode: 1,
Error: fmt.Errorf("%s\n%s", preamble, err.Error()),
EmitUsage: false,
})
}
}
func AbortIfErrors(preamble string, errors []error) {
if len(errors) > 0 {
out := ""
for _, err := range errors {
out += err.Error()
}
Abort(AbortDetails{
ExitCode: 1,
Error: fmt.Errorf("%s\n%s", preamble, out),
EmitUsage: false,
})
}
}

View File

@@ -0,0 +1,50 @@
package command
import (
"fmt"
"io"
"strings"
"github.com/onsi/ginkgo/v2/formatter"
"github.com/onsi/ginkgo/v2/types"
)
type Command struct {
Name string
Flags types.GinkgoFlagSet
Usage string
ShortDoc string
Documentation string
DocLink string
Command func(args []string, additionalArgs []string)
}
func (c Command) Run(args []string, additionalArgs []string) {
args, err := c.Flags.Parse(args)
if err != nil {
AbortWithUsage(err.Error())
}
c.Command(args, additionalArgs)
}
func (c Command) EmitUsage(writer io.Writer) {
fmt.Fprintln(writer, formatter.F("{{bold}}"+c.Usage+"{{/}}"))
fmt.Fprintln(writer, formatter.F("{{gray}}%s{{/}}", strings.Repeat("-", len(c.Usage))))
if c.ShortDoc != "" {
fmt.Fprintln(writer, formatter.Fiw(0, formatter.COLS, c.ShortDoc))
fmt.Fprintln(writer, "")
}
if c.Documentation != "" {
fmt.Fprintln(writer, formatter.Fiw(0, formatter.COLS, c.Documentation))
fmt.Fprintln(writer, "")
}
if c.DocLink != "" {
fmt.Fprintln(writer, formatter.Fi(0, "{{bold}}Learn more at:{{/}} {{cyan}}{{underline}}http://onsi.github.io/ginkgo/#%s{{/}}", c.DocLink))
fmt.Fprintln(writer, "")
}
flagUsage := c.Flags.Usage()
if flagUsage != "" {
fmt.Fprintf(writer, formatter.F(flagUsage))
}
}

View File

@@ -0,0 +1,182 @@
package command
import (
"fmt"
"io"
"os"
"strings"
"github.com/onsi/ginkgo/v2/formatter"
"github.com/onsi/ginkgo/v2/types"
)
type Program struct {
Name string
Heading string
Commands []Command
DefaultCommand Command
DeprecatedCommands []DeprecatedCommand
//For testing - leave as nil in production
OutWriter io.Writer
ErrWriter io.Writer
Exiter func(code int)
}
type DeprecatedCommand struct {
Name string
Deprecation types.Deprecation
}
func (p Program) RunAndExit(osArgs []string) {
var command Command
deprecationTracker := types.NewDeprecationTracker()
if p.Exiter == nil {
p.Exiter = os.Exit
}
if p.OutWriter == nil {
p.OutWriter = formatter.ColorableStdOut
}
if p.ErrWriter == nil {
p.ErrWriter = formatter.ColorableStdErr
}
defer func() {
exitCode := 0
if r := recover(); r != nil {
details, ok := r.(AbortDetails)
if !ok {
panic(r)
}
if details.Error != nil {
fmt.Fprintln(p.ErrWriter, formatter.F("{{red}}{{bold}}%s %s{{/}} {{red}}failed{{/}}", p.Name, command.Name))
fmt.Fprintln(p.ErrWriter, formatter.Fi(1, details.Error.Error()))
}
if details.EmitUsage {
if details.Error != nil {
fmt.Fprintln(p.ErrWriter, "")
}
command.EmitUsage(p.ErrWriter)
}
exitCode = details.ExitCode
}
command.Flags.ValidateDeprecations(deprecationTracker)
if deprecationTracker.DidTrackDeprecations() {
fmt.Fprintln(p.ErrWriter, deprecationTracker.DeprecationsReport())
}
p.Exiter(exitCode)
return
}()
args, additionalArgs := []string{}, []string{}
foundDelimiter := false
for _, arg := range osArgs[1:] {
if !foundDelimiter {
if arg == "--" {
foundDelimiter = true
continue
}
}
if foundDelimiter {
additionalArgs = append(additionalArgs, arg)
} else {
args = append(args, arg)
}
}
command = p.DefaultCommand
if len(args) > 0 {
p.handleHelpRequestsAndExit(p.OutWriter, args)
if command.Name == args[0] {
args = args[1:]
} else {
for _, deprecatedCommand := range p.DeprecatedCommands {
if deprecatedCommand.Name == args[0] {
deprecationTracker.TrackDeprecation(deprecatedCommand.Deprecation)
return
}
}
for _, tryCommand := range p.Commands {
if tryCommand.Name == args[0] {
command, args = tryCommand, args[1:]
break
}
}
}
}
command.Run(args, additionalArgs)
}
func (p Program) handleHelpRequestsAndExit(writer io.Writer, args []string) {
if len(args) == 0 {
return
}
matchesHelpFlag := func(args ...string) bool {
for _, arg := range args {
if arg == "--help" || arg == "-help" || arg == "-h" || arg == "--h" {
return true
}
}
return false
}
if len(args) == 1 {
if args[0] == "help" || matchesHelpFlag(args[0]) {
p.EmitUsage(writer)
Abort(AbortDetails{})
}
} else {
var name string
if args[0] == "help" || matchesHelpFlag(args[0]) {
name = args[1]
} else if matchesHelpFlag(args[1:]...) {
name = args[0]
} else {
return
}
if p.DefaultCommand.Name == name || p.Name == name {
p.DefaultCommand.EmitUsage(writer)
Abort(AbortDetails{})
}
for _, command := range p.Commands {
if command.Name == name {
command.EmitUsage(writer)
Abort(AbortDetails{})
}
}
fmt.Fprintln(writer, formatter.F("{{red}}Unknown Command: {{bold}}%s{{/}}", name))
fmt.Fprintln(writer, "")
p.EmitUsage(writer)
Abort(AbortDetails{ExitCode: 1})
}
return
}
func (p Program) EmitUsage(writer io.Writer) {
fmt.Fprintln(writer, formatter.F(p.Heading))
fmt.Fprintln(writer, formatter.F("{{gray}}%s{{/}}", strings.Repeat("-", len(p.Heading))))
fmt.Fprintln(writer, formatter.F("For usage information for a command, run {{bold}}%s help COMMAND{{/}}.", p.Name))
fmt.Fprintln(writer, formatter.F("For usage information for the default command, run {{bold}}%s help %s{{/}} or {{bold}}%s help %s{{/}}.", p.Name, p.Name, p.Name, p.DefaultCommand.Name))
fmt.Fprintln(writer, "")
fmt.Fprintln(writer, formatter.F("The following commands are available:"))
fmt.Fprintln(writer, formatter.Fi(1, "{{bold}}%s{{/}} or %s {{bold}}%s{{/}} - {{gray}}%s{{/}}", p.Name, p.Name, p.DefaultCommand.Name, p.DefaultCommand.Usage))
if p.DefaultCommand.ShortDoc != "" {
fmt.Fprintln(writer, formatter.Fi(2, p.DefaultCommand.ShortDoc))
}
for _, command := range p.Commands {
fmt.Fprintln(writer, formatter.Fi(1, "{{bold}}%s{{/}} - {{gray}}%s{{/}}", command.Name, command.Usage))
if command.ShortDoc != "" {
fmt.Fprintln(writer, formatter.Fi(2, command.ShortDoc))
}
}
}

View File

@@ -0,0 +1,48 @@
package generators
var bootstrapText = `package {{.Package}}
import (
"testing"
{{.GinkgoImport}}
{{.GomegaImport}}
)
func Test{{.FormattedName}}(t *testing.T) {
{{.GomegaPackage}}RegisterFailHandler({{.GinkgoPackage}}Fail)
{{.GinkgoPackage}}RunSpecs(t, "{{.FormattedName}} Suite")
}
`
var agoutiBootstrapText = `package {{.Package}}
import (
"testing"
{{.GinkgoImport}}
{{.GomegaImport}}
"github.com/sclevine/agouti"
)
func Test{{.FormattedName}}(t *testing.T) {
{{.GomegaPackage}}RegisterFailHandler({{.GinkgoPackage}}Fail)
{{.GinkgoPackage}}RunSpecs(t, "{{.FormattedName}} Suite")
}
var agoutiDriver *agouti.WebDriver
var _ = {{.GinkgoPackage}}BeforeSuite(func() {
// Choose a WebDriver:
agoutiDriver = agouti.PhantomJS()
// agoutiDriver = agouti.Selenium()
// agoutiDriver = agouti.ChromeDriver()
{{.GomegaPackage}}Expect(agoutiDriver.Start()).To({{.GomegaPackage}}Succeed())
})
var _ = {{.GinkgoPackage}}AfterSuite(func() {
{{.GomegaPackage}}Expect(agoutiDriver.Stop()).To({{.GomegaPackage}}Succeed())
})
`

View File

@@ -0,0 +1,113 @@
package generators
import (
"bytes"
"fmt"
"os"
"text/template"
sprig "github.com/go-task/slim-sprig"
"github.com/onsi/ginkgo/v2/ginkgo/command"
"github.com/onsi/ginkgo/v2/ginkgo/internal"
"github.com/onsi/ginkgo/v2/types"
)
func BuildBootstrapCommand() command.Command {
conf := GeneratorsConfig{}
flags, err := types.NewGinkgoFlagSet(
types.GinkgoFlags{
{Name: "agouti", KeyPath: "Agouti",
Usage: "If set, bootstrap will generate a bootstrap file for writing Agouti tests"},
{Name: "nodot", KeyPath: "NoDot",
Usage: "If set, bootstrap will generate a bootstrap test file that does not dot-import ginkgo and gomega"},
{Name: "internal", KeyPath: "Internal",
Usage: "If set, bootstrap will generate a bootstrap test file that uses the regular package name (i.e. `package X`, not `package X_test`)"},
{Name: "template", KeyPath: "CustomTemplate",
UsageArgument: "template-file",
Usage: "If specified, generate will use the contents of the file passed as the bootstrap template"},
},
&conf,
types.GinkgoFlagSections{},
)
if err != nil {
panic(err)
}
return command.Command{
Name: "bootstrap",
Usage: "ginkgo bootstrap",
ShortDoc: "Bootstrap a test suite for the current package",
Documentation: `Tests written in Ginkgo and Gomega require a small amount of boilerplate to hook into Go's testing infrastructure.
{{bold}}ginkgo bootstrap{{/}} generates this boilerplate for you in a file named X_suite_test.go where X is the name of the package under test.`,
DocLink: "generators",
Flags: flags,
Command: func(_ []string, _ []string) {
generateBootstrap(conf)
},
}
}
type bootstrapData struct {
Package string
FormattedName string
GinkgoImport string
GomegaImport string
GinkgoPackage string
GomegaPackage string
}
func generateBootstrap(conf GeneratorsConfig) {
packageName, bootstrapFilePrefix, formattedName := getPackageAndFormattedName()
data := bootstrapData{
Package: determinePackageName(packageName, conf.Internal),
FormattedName: formattedName,
GinkgoImport: `. "github.com/onsi/ginkgo/v2"`,
GomegaImport: `. "github.com/onsi/gomega"`,
GinkgoPackage: "",
GomegaPackage: "",
}
if conf.NoDot {
data.GinkgoImport = `"github.com/onsi/ginkgo/v2"`
data.GomegaImport = `"github.com/onsi/gomega"`
data.GinkgoPackage = `ginkgo.`
data.GomegaPackage = `gomega.`
}
targetFile := fmt.Sprintf("%s_suite_test.go", bootstrapFilePrefix)
if internal.FileExists(targetFile) {
command.AbortWith("{{bold}}%s{{/}} already exists", targetFile)
} else {
fmt.Printf("Generating ginkgo test suite bootstrap for %s in:\n\t%s\n", packageName, targetFile)
}
f, err := os.Create(targetFile)
command.AbortIfError("Failed to create file:", err)
defer f.Close()
var templateText string
if conf.CustomTemplate != "" {
tpl, err := os.ReadFile(conf.CustomTemplate)
command.AbortIfError("Failed to read custom bootstrap file:", err)
templateText = string(tpl)
} else if conf.Agouti {
templateText = agoutiBootstrapText
} else {
templateText = bootstrapText
}
bootstrapTemplate, err := template.New("bootstrap").Funcs(sprig.TxtFuncMap()).Parse(templateText)
command.AbortIfError("Failed to parse bootstrap template:", err)
buf := &bytes.Buffer{}
bootstrapTemplate.Execute(buf, data)
buf.WriteTo(f)
internal.GoFmt(targetFile)
}

View File

@@ -0,0 +1,239 @@
package generators
import (
"bytes"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"text/template"
sprig "github.com/go-task/slim-sprig"
"github.com/onsi/ginkgo/v2/ginkgo/command"
"github.com/onsi/ginkgo/v2/ginkgo/internal"
"github.com/onsi/ginkgo/v2/types"
)
func BuildGenerateCommand() command.Command {
conf := GeneratorsConfig{}
flags, err := types.NewGinkgoFlagSet(
types.GinkgoFlags{
{Name: "agouti", KeyPath: "Agouti",
Usage: "If set, generate will create a test file for writing Agouti tests"},
{Name: "nodot", KeyPath: "NoDot",
Usage: "If set, generate will create a test file that does not dot-import ginkgo and gomega"},
{Name: "internal", KeyPath: "Internal",
Usage: "If set, generate will create a test file that uses the regular package name (i.e. `package X`, not `package X_test`)"},
{Name: "template", KeyPath: "CustomTemplate",
UsageArgument: "template-file",
Usage: "If specified, generate will use the contents of the file passed as the test file template"},
},
&conf,
types.GinkgoFlagSections{},
)
if err != nil {
panic(err)
}
return command.Command{
Name: "generate",
Usage: "ginkgo generate <filename(s)>",
ShortDoc: "Generate a test file named <filename>_test.go",
Documentation: `If the optional <filename> argument is omitted, a file named after the package in the current directory will be created.
You can pass multiple <filename(s)> to generate multiple files simultaneously. The resulting files are named <filename>_test.go.
You can also pass a <filename> of the form "file.go" and generate will emit "file_test.go".`,
DocLink: "generators",
Flags: flags,
Command: func(args []string, _ []string) {
generateTestFiles(conf, args)
},
}
}
type specData struct {
Package string
Subject string
PackageImportPath string
ImportPackage bool
GinkgoImport string
GomegaImport string
GinkgoPackage string
GomegaPackage string
}
func generateTestFiles(conf GeneratorsConfig, args []string) {
subjects := args
if len(subjects) == 0 {
subjects = []string{""}
}
for _, subject := range subjects {
generateTestFileForSubject(subject, conf)
}
}
func generateTestFileForSubject(subject string, conf GeneratorsConfig) {
packageName, specFilePrefix, formattedName := getPackageAndFormattedName()
if subject != "" {
specFilePrefix = formatSubject(subject)
formattedName = prettifyName(specFilePrefix)
}
if conf.Internal {
specFilePrefix = specFilePrefix + "_internal"
}
data := specData{
Package: determinePackageName(packageName, conf.Internal),
Subject: formattedName,
PackageImportPath: getPackageImportPath(),
ImportPackage: !conf.Internal,
GinkgoImport: `. "github.com/onsi/ginkgo/v2"`,
GomegaImport: `. "github.com/onsi/gomega"`,
GinkgoPackage: "",
GomegaPackage: "",
}
if conf.NoDot {
data.GinkgoImport = `"github.com/onsi/ginkgo/v2"`
data.GomegaImport = `"github.com/onsi/gomega"`
data.GinkgoPackage = `ginkgo.`
data.GomegaPackage = `gomega.`
}
targetFile := fmt.Sprintf("%s_test.go", specFilePrefix)
if internal.FileExists(targetFile) {
command.AbortWith("{{bold}}%s{{/}} already exists", targetFile)
} else {
fmt.Printf("Generating ginkgo test for %s in:\n %s\n", data.Subject, targetFile)
}
f, err := os.Create(targetFile)
command.AbortIfError("Failed to create test file:", err)
defer f.Close()
var templateText string
if conf.CustomTemplate != "" {
tpl, err := os.ReadFile(conf.CustomTemplate)
command.AbortIfError("Failed to read custom template file:", err)
templateText = string(tpl)
} else if conf.Agouti {
templateText = agoutiSpecText
} else {
templateText = specText
}
specTemplate, err := template.New("spec").Funcs(sprig.TxtFuncMap()).Parse(templateText)
command.AbortIfError("Failed to read parse test template:", err)
specTemplate.Execute(f, data)
internal.GoFmt(targetFile)
}
func formatSubject(name string) string {
name = strings.Replace(name, "-", "_", -1)
name = strings.Replace(name, " ", "_", -1)
name = strings.Split(name, ".go")[0]
name = strings.Split(name, "_test")[0]
return name
}
// moduleName returns module name from go.mod from given module root directory
func moduleName(modRoot string) string {
modFile, err := os.Open(filepath.Join(modRoot, "go.mod"))
if err != nil {
return ""
}
mod := make([]byte, 128)
_, err = modFile.Read(mod)
if err != nil {
return ""
}
slashSlash := []byte("//")
moduleStr := []byte("module")
for len(mod) > 0 {
line := mod
mod = nil
if i := bytes.IndexByte(line, '\n'); i >= 0 {
line, mod = line[:i], line[i+1:]
}
if i := bytes.Index(line, slashSlash); i >= 0 {
line = line[:i]
}
line = bytes.TrimSpace(line)
if !bytes.HasPrefix(line, moduleStr) {
continue
}
line = line[len(moduleStr):]
n := len(line)
line = bytes.TrimSpace(line)
if len(line) == n || len(line) == 0 {
continue
}
if line[0] == '"' || line[0] == '`' {
p, err := strconv.Unquote(string(line))
if err != nil {
return "" // malformed quoted string or multiline module path
}
return p
}
return string(line)
}
return "" // missing module path
}
func findModuleRoot(dir string) (root string) {
dir = filepath.Clean(dir)
// Look for enclosing go.mod.
for {
if fi, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil && !fi.IsDir() {
return dir
}
d := filepath.Dir(dir)
if d == dir {
break
}
dir = d
}
return ""
}
func getPackageImportPath() string {
workingDir, err := os.Getwd()
if err != nil {
panic(err.Error())
}
sep := string(filepath.Separator)
// Try go.mod file first
modRoot := findModuleRoot(workingDir)
if modRoot != "" {
modName := moduleName(modRoot)
if modName != "" {
cd := strings.Replace(workingDir, modRoot, "", -1)
cd = strings.ReplaceAll(cd, sep, "/")
return modName + cd
}
}
// Fallback to GOPATH structure
paths := strings.Split(workingDir, sep+"src"+sep)
if len(paths) == 1 {
fmt.Printf("\nCouldn't identify package import path.\n\n\tginkgo generate\n\nMust be run within a package directory under $GOPATH/src/...\nYou're going to have to change UNKNOWN_PACKAGE_PATH in the generated file...\n\n")
return "UNKNOWN_PACKAGE_PATH"
}
return filepath.ToSlash(paths[len(paths)-1])
}

View File

@@ -0,0 +1,41 @@
package generators
var specText = `package {{.Package}}
import (
{{.GinkgoImport}}
{{.GomegaImport}}
{{if .ImportPackage}}"{{.PackageImportPath}}"{{end}}
)
var _ = {{.GinkgoPackage}}Describe("{{.Subject}}", func() {
})
`
var agoutiSpecText = `package {{.Package}}
import (
{{.GinkgoImport}}
{{.GomegaImport}}
"github.com/sclevine/agouti"
. "github.com/sclevine/agouti/matchers"
{{if .ImportPackage}}"{{.PackageImportPath}}"{{end}}
)
var _ = {{.GinkgoPackage}}Describe("{{.Subject}}", func() {
var page *agouti.Page
{{.GinkgoPackage}}BeforeEach(func() {
var err error
page, err = agoutiDriver.NewPage()
{{.GomegaPackage}}Expect(err).NotTo({{.GomegaPackage}}HaveOccurred())
})
{{.GinkgoPackage}}AfterEach(func() {
{{.GomegaPackage}}Expect(page.Destroy()).To({{.GomegaPackage}}Succeed())
})
})
`

View File

@@ -0,0 +1,63 @@
package generators
import (
"go/build"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/onsi/ginkgo/v2/ginkgo/command"
)
type GeneratorsConfig struct {
Agouti, NoDot, Internal bool
CustomTemplate string
}
func getPackageAndFormattedName() (string, string, string) {
path, err := os.Getwd()
command.AbortIfError("Could not get current working directory:", err)
dirName := strings.Replace(filepath.Base(path), "-", "_", -1)
dirName = strings.Replace(dirName, " ", "_", -1)
pkg, err := build.ImportDir(path, 0)
packageName := pkg.Name
if err != nil {
packageName = ensureLegalPackageName(dirName)
}
formattedName := prettifyName(filepath.Base(path))
return packageName, dirName, formattedName
}
func ensureLegalPackageName(name string) string {
if name == "_" {
return "underscore"
}
if len(name) == 0 {
return "empty"
}
n, isDigitErr := strconv.Atoi(string(name[0]))
if isDigitErr == nil {
return []string{"zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"}[n] + name[1:]
}
return name
}
func prettifyName(name string) string {
name = strings.Replace(name, "-", " ", -1)
name = strings.Replace(name, "_", " ", -1)
name = strings.Title(name)
name = strings.Replace(name, " ", "", -1)
return name
}
func determinePackageName(name string, internal bool) string {
if internal {
return name
}
return name + "_test"
}

View File

@@ -0,0 +1,152 @@
package internal
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"github.com/onsi/ginkgo/v2/types"
)
func CompileSuite(suite TestSuite, goFlagsConfig types.GoFlagsConfig) TestSuite {
if suite.PathToCompiledTest != "" {
return suite
}
suite.CompilationError = nil
path, err := filepath.Abs(filepath.Join(suite.Path, suite.PackageName+".test"))
if err != nil {
suite.State = TestSuiteStateFailedToCompile
suite.CompilationError = fmt.Errorf("Failed to compute compilation target path:\n%s", err.Error())
return suite
}
args, err := types.GenerateGoTestCompileArgs(goFlagsConfig, path, "./")
if err != nil {
suite.State = TestSuiteStateFailedToCompile
suite.CompilationError = fmt.Errorf("Failed to generate go test compile flags:\n%s", err.Error())
return suite
}
cmd := exec.Command("go", args...)
cmd.Dir = suite.Path
output, err := cmd.CombinedOutput()
if err != nil {
if len(output) > 0 {
suite.State = TestSuiteStateFailedToCompile
suite.CompilationError = fmt.Errorf("Failed to compile %s:\n\n%s", suite.PackageName, output)
} else {
suite.State = TestSuiteStateFailedToCompile
suite.CompilationError = fmt.Errorf("Failed to compile %s\n%s", suite.PackageName, err.Error())
}
return suite
}
if strings.Contains(string(output), "[no test files]") {
suite.State = TestSuiteStateSkippedDueToEmptyCompilation
return suite
}
if len(output) > 0 {
fmt.Println(string(output))
}
if !FileExists(path) {
suite.State = TestSuiteStateFailedToCompile
suite.CompilationError = fmt.Errorf("Failed to compile %s:\nOutput file %s could not be found", suite.PackageName, path)
return suite
}
suite.State = TestSuiteStateCompiled
suite.PathToCompiledTest = path
return suite
}
func Cleanup(goFlagsConfig types.GoFlagsConfig, suites ...TestSuite) {
if goFlagsConfig.BinaryMustBePreserved() {
return
}
for _, suite := range suites {
if !suite.Precompiled {
os.Remove(suite.PathToCompiledTest)
}
}
}
type parallelSuiteBundle struct {
suite TestSuite
compiled chan TestSuite
}
type OrderedParallelCompiler struct {
mutex *sync.Mutex
stopped bool
numCompilers int
idx int
numSuites int
completionChannels []chan TestSuite
}
func NewOrderedParallelCompiler(numCompilers int) *OrderedParallelCompiler {
return &OrderedParallelCompiler{
mutex: &sync.Mutex{},
numCompilers: numCompilers,
}
}
func (opc *OrderedParallelCompiler) StartCompiling(suites TestSuites, goFlagsConfig types.GoFlagsConfig) {
opc.stopped = false
opc.idx = 0
opc.numSuites = len(suites)
opc.completionChannels = make([]chan TestSuite, opc.numSuites)
toCompile := make(chan parallelSuiteBundle, opc.numCompilers)
for compiler := 0; compiler < opc.numCompilers; compiler++ {
go func() {
for bundle := range toCompile {
c, suite := bundle.compiled, bundle.suite
opc.mutex.Lock()
stopped := opc.stopped
opc.mutex.Unlock()
if !stopped {
suite = CompileSuite(suite, goFlagsConfig)
}
c <- suite
}
}()
}
for idx, suite := range suites {
opc.completionChannels[idx] = make(chan TestSuite, 1)
toCompile <- parallelSuiteBundle{suite, opc.completionChannels[idx]}
if idx == 0 { //compile first suite serially
suite = <-opc.completionChannels[0]
opc.completionChannels[0] <- suite
}
}
close(toCompile)
}
func (opc *OrderedParallelCompiler) Next() (int, TestSuite) {
if opc.idx >= opc.numSuites {
return opc.numSuites, TestSuite{}
}
idx := opc.idx
suite := <-opc.completionChannels[idx]
opc.idx = opc.idx + 1
return idx, suite
}
func (opc *OrderedParallelCompiler) StopAndDrain() {
opc.mutex.Lock()
opc.stopped = true
opc.mutex.Unlock()
}

View File

@@ -0,0 +1,237 @@
package internal
import (
"bytes"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"github.com/google/pprof/profile"
"github.com/onsi/ginkgo/v2/reporters"
"github.com/onsi/ginkgo/v2/types"
)
func AbsPathForGeneratedAsset(assetName string, suite TestSuite, cliConfig types.CLIConfig, process int) string {
suffix := ""
if process != 0 {
suffix = fmt.Sprintf(".%d", process)
}
if cliConfig.OutputDir == "" {
return filepath.Join(suite.AbsPath(), assetName+suffix)
}
outputDir, _ := filepath.Abs(cliConfig.OutputDir)
return filepath.Join(outputDir, suite.NamespacedName()+"_"+assetName+suffix)
}
func FinalizeProfilesAndReportsForSuites(suites TestSuites, cliConfig types.CLIConfig, suiteConfig types.SuiteConfig, reporterConfig types.ReporterConfig, goFlagsConfig types.GoFlagsConfig) ([]string, error) {
messages := []string{}
suitesWithProfiles := suites.WithState(TestSuiteStatePassed, TestSuiteStateFailed) //anything else won't have actually run and generated a profile
// merge cover profiles if need be
if goFlagsConfig.Cover && !cliConfig.KeepSeparateCoverprofiles {
coverProfiles := []string{}
for _, suite := range suitesWithProfiles {
if !suite.HasProgrammaticFocus {
coverProfiles = append(coverProfiles, AbsPathForGeneratedAsset(goFlagsConfig.CoverProfile, suite, cliConfig, 0))
}
}
if len(coverProfiles) > 0 {
dst := goFlagsConfig.CoverProfile
if cliConfig.OutputDir != "" {
dst = filepath.Join(cliConfig.OutputDir, goFlagsConfig.CoverProfile)
}
err := MergeAndCleanupCoverProfiles(coverProfiles, dst)
if err != nil {
return messages, err
}
coverage, err := GetCoverageFromCoverProfile(dst)
if err != nil {
return messages, err
}
if coverage == 0 {
messages = append(messages, "composite coverage: [no statements]")
} else if suitesWithProfiles.AnyHaveProgrammaticFocus() {
messages = append(messages, fmt.Sprintf("composite coverage: %.1f%% of statements however some suites did not contribute because they included programatically focused specs", coverage))
} else {
messages = append(messages, fmt.Sprintf("composite coverage: %.1f%% of statements", coverage))
}
} else {
messages = append(messages, "no composite coverage computed: all suites included programatically focused specs")
}
}
// copy binaries if need be
for _, suite := range suitesWithProfiles {
if goFlagsConfig.BinaryMustBePreserved() && cliConfig.OutputDir != "" {
src := suite.PathToCompiledTest
dst := filepath.Join(cliConfig.OutputDir, suite.NamespacedName()+".test")
if suite.Precompiled {
if err := CopyFile(src, dst); err != nil {
return messages, err
}
} else {
if err := os.Rename(src, dst); err != nil {
return messages, err
}
}
}
}
type reportFormat struct {
ReportName string
GenerateFunc func(types.Report, string) error
MergeFunc func([]string, string) ([]string, error)
}
reportFormats := []reportFormat{}
if reporterConfig.JSONReport != "" {
reportFormats = append(reportFormats, reportFormat{ReportName: reporterConfig.JSONReport, GenerateFunc: reporters.GenerateJSONReport, MergeFunc: reporters.MergeAndCleanupJSONReports})
}
if reporterConfig.JUnitReport != "" {
reportFormats = append(reportFormats, reportFormat{ReportName: reporterConfig.JUnitReport, GenerateFunc: reporters.GenerateJUnitReport, MergeFunc: reporters.MergeAndCleanupJUnitReports})
}
if reporterConfig.TeamcityReport != "" {
reportFormats = append(reportFormats, reportFormat{ReportName: reporterConfig.TeamcityReport, GenerateFunc: reporters.GenerateTeamcityReport, MergeFunc: reporters.MergeAndCleanupTeamcityReports})
}
// Generate reports for suites that failed to run
reportableSuites := suites.ThatAreGinkgoSuites()
for _, suite := range reportableSuites.WithState(TestSuiteStateFailedToCompile, TestSuiteStateFailedDueToTimeout, TestSuiteStateSkippedDueToPriorFailures, TestSuiteStateSkippedDueToEmptyCompilation) {
report := types.Report{
SuitePath: suite.AbsPath(),
SuiteConfig: suiteConfig,
SuiteSucceeded: false,
}
switch suite.State {
case TestSuiteStateFailedToCompile:
report.SpecialSuiteFailureReasons = append(report.SpecialSuiteFailureReasons, suite.CompilationError.Error())
case TestSuiteStateFailedDueToTimeout:
report.SpecialSuiteFailureReasons = append(report.SpecialSuiteFailureReasons, TIMEOUT_ELAPSED_FAILURE_REASON)
case TestSuiteStateSkippedDueToPriorFailures:
report.SpecialSuiteFailureReasons = append(report.SpecialSuiteFailureReasons, PRIOR_FAILURES_FAILURE_REASON)
case TestSuiteStateSkippedDueToEmptyCompilation:
report.SpecialSuiteFailureReasons = append(report.SpecialSuiteFailureReasons, EMPTY_SKIP_FAILURE_REASON)
report.SuiteSucceeded = true
}
for _, format := range reportFormats {
format.GenerateFunc(report, AbsPathForGeneratedAsset(format.ReportName, suite, cliConfig, 0))
}
}
// Merge reports unless we've been asked to keep them separate
if !cliConfig.KeepSeparateReports {
for _, format := range reportFormats {
reports := []string{}
for _, suite := range reportableSuites {
reports = append(reports, AbsPathForGeneratedAsset(format.ReportName, suite, cliConfig, 0))
}
dst := format.ReportName
if cliConfig.OutputDir != "" {
dst = filepath.Join(cliConfig.OutputDir, format.ReportName)
}
mergeMessages, err := format.MergeFunc(reports, dst)
messages = append(messages, mergeMessages...)
if err != nil {
return messages, err
}
}
}
return messages, nil
}
//loads each profile, combines them, deletes them, stores them in destination
func MergeAndCleanupCoverProfiles(profiles []string, destination string) error {
combined := &bytes.Buffer{}
modeRegex := regexp.MustCompile(`^mode: .*\n`)
for i, profile := range profiles {
contents, err := os.ReadFile(profile)
if err != nil {
return fmt.Errorf("Unable to read coverage file %s:\n%s", profile, err.Error())
}
os.Remove(profile)
// remove the cover mode line from every file
// except the first one
if i > 0 {
contents = modeRegex.ReplaceAll(contents, []byte{})
}
_, err = combined.Write(contents)
// Add a newline to the end of every file if missing.
if err == nil && len(contents) > 0 && contents[len(contents)-1] != '\n' {
_, err = combined.Write([]byte("\n"))
}
if err != nil {
return fmt.Errorf("Unable to append to coverprofile:\n%s", err.Error())
}
}
err := os.WriteFile(destination, combined.Bytes(), 0666)
if err != nil {
return fmt.Errorf("Unable to create combined cover profile:\n%s", err.Error())
}
return nil
}
func GetCoverageFromCoverProfile(profile string) (float64, error) {
cmd := exec.Command("go", "tool", "cover", "-func", profile)
output, err := cmd.CombinedOutput()
if err != nil {
return 0, fmt.Errorf("Could not process Coverprofile %s: %s", profile, err.Error())
}
re := regexp.MustCompile(`total:\s*\(statements\)\s*(\d*\.\d*)\%`)
matches := re.FindStringSubmatch(string(output))
if matches == nil {
return 0, fmt.Errorf("Could not parse Coverprofile to compute coverage percentage")
}
coverageString := matches[1]
coverage, err := strconv.ParseFloat(coverageString, 64)
if err != nil {
return 0, fmt.Errorf("Could not parse Coverprofile to compute coverage percentage: %s", err.Error())
}
return coverage, nil
}
func MergeProfiles(profilePaths []string, destination string) error {
profiles := []*profile.Profile{}
for _, profilePath := range profilePaths {
proFile, err := os.Open(profilePath)
if err != nil {
return fmt.Errorf("Could not open profile: %s\n%s", profilePath, err.Error())
}
prof, err := profile.Parse(proFile)
if err != nil {
return fmt.Errorf("Could not parse profile: %s\n%s", profilePath, err.Error())
}
profiles = append(profiles, prof)
os.Remove(profilePath)
}
mergedProfile, err := profile.Merge(profiles)
if err != nil {
return fmt.Errorf("Could not merge profiles:\n%s", err.Error())
}
outFile, err := os.Create(destination)
if err != nil {
return fmt.Errorf("Could not create merged profile %s:\n%s", destination, err.Error())
}
err = mergedProfile.Write(outFile)
if err != nil {
return fmt.Errorf("Could not write merged profile %s:\n%s", destination, err.Error())
}
err = outFile.Close()
if err != nil {
return fmt.Errorf("Could not close merged profile %s:\n%s", destination, err.Error())
}
return nil
}

348
vendor/github.com/onsi/ginkgo/v2/ginkgo/internal/run.go generated vendored Normal file
View File

@@ -0,0 +1,348 @@
package internal
import (
"bytes"
"fmt"
"io"
"os"
"os/exec"
"regexp"
"strings"
"syscall"
"time"
"github.com/onsi/ginkgo/v2/formatter"
"github.com/onsi/ginkgo/v2/ginkgo/command"
"github.com/onsi/ginkgo/v2/internal/parallel_support"
"github.com/onsi/ginkgo/v2/reporters"
"github.com/onsi/ginkgo/v2/types"
)
func RunCompiledSuite(suite TestSuite, ginkgoConfig types.SuiteConfig, reporterConfig types.ReporterConfig, cliConfig types.CLIConfig, goFlagsConfig types.GoFlagsConfig, additionalArgs []string) TestSuite {
suite.State = TestSuiteStateFailed
suite.HasProgrammaticFocus = false
if suite.PathToCompiledTest == "" {
return suite
}
if suite.IsGinkgo && cliConfig.ComputedProcs() > 1 {
suite = runParallel(suite, ginkgoConfig, reporterConfig, cliConfig, goFlagsConfig, additionalArgs)
} else if suite.IsGinkgo {
suite = runSerial(suite, ginkgoConfig, reporterConfig, cliConfig, goFlagsConfig, additionalArgs)
} else {
suite = runGoTest(suite, cliConfig, goFlagsConfig)
}
runAfterRunHook(cliConfig.AfterRunHook, reporterConfig.NoColor, suite)
return suite
}
func buildAndStartCommand(suite TestSuite, args []string, pipeToStdout bool) (*exec.Cmd, *bytes.Buffer) {
buf := &bytes.Buffer{}
cmd := exec.Command(suite.PathToCompiledTest, args...)
cmd.Dir = suite.Path
if pipeToStdout {
cmd.Stderr = io.MultiWriter(os.Stdout, buf)
cmd.Stdout = os.Stdout
} else {
cmd.Stderr = buf
cmd.Stdout = buf
}
err := cmd.Start()
command.AbortIfError("Failed to start test suite", err)
return cmd, buf
}
func checkForNoTestsWarning(buf *bytes.Buffer) bool {
if strings.Contains(buf.String(), "warning: no tests to run") {
fmt.Fprintf(os.Stderr, `Found no test suites, did you forget to run "ginkgo bootstrap"?`)
return true
}
return false
}
func runGoTest(suite TestSuite, cliConfig types.CLIConfig, goFlagsConfig types.GoFlagsConfig) TestSuite {
args, err := types.GenerateGoTestRunArgs(goFlagsConfig)
command.AbortIfError("Failed to generate test run arguments", err)
cmd, buf := buildAndStartCommand(suite, args, true)
cmd.Wait()
exitStatus := cmd.ProcessState.Sys().(syscall.WaitStatus).ExitStatus()
passed := (exitStatus == 0) || (exitStatus == types.GINKGO_FOCUS_EXIT_CODE)
passed = !(checkForNoTestsWarning(buf) && cliConfig.RequireSuite) && passed
if passed {
suite.State = TestSuiteStatePassed
} else {
suite.State = TestSuiteStateFailed
}
return suite
}
func runSerial(suite TestSuite, ginkgoConfig types.SuiteConfig, reporterConfig types.ReporterConfig, cliConfig types.CLIConfig, goFlagsConfig types.GoFlagsConfig, additionalArgs []string) TestSuite {
if goFlagsConfig.Cover {
goFlagsConfig.CoverProfile = AbsPathForGeneratedAsset(goFlagsConfig.CoverProfile, suite, cliConfig, 0)
}
if goFlagsConfig.BlockProfile != "" {
goFlagsConfig.BlockProfile = AbsPathForGeneratedAsset(goFlagsConfig.BlockProfile, suite, cliConfig, 0)
}
if goFlagsConfig.CPUProfile != "" {
goFlagsConfig.CPUProfile = AbsPathForGeneratedAsset(goFlagsConfig.CPUProfile, suite, cliConfig, 0)
}
if goFlagsConfig.MemProfile != "" {
goFlagsConfig.MemProfile = AbsPathForGeneratedAsset(goFlagsConfig.MemProfile, suite, cliConfig, 0)
}
if goFlagsConfig.MutexProfile != "" {
goFlagsConfig.MutexProfile = AbsPathForGeneratedAsset(goFlagsConfig.MutexProfile, suite, cliConfig, 0)
}
if reporterConfig.JSONReport != "" {
reporterConfig.JSONReport = AbsPathForGeneratedAsset(reporterConfig.JSONReport, suite, cliConfig, 0)
}
if reporterConfig.JUnitReport != "" {
reporterConfig.JUnitReport = AbsPathForGeneratedAsset(reporterConfig.JUnitReport, suite, cliConfig, 0)
}
if reporterConfig.TeamcityReport != "" {
reporterConfig.TeamcityReport = AbsPathForGeneratedAsset(reporterConfig.TeamcityReport, suite, cliConfig, 0)
}
args, err := types.GenerateGinkgoTestRunArgs(ginkgoConfig, reporterConfig, goFlagsConfig)
command.AbortIfError("Failed to generate test run arguments", err)
args = append([]string{"--test.timeout=0"}, args...)
args = append(args, additionalArgs...)
cmd, buf := buildAndStartCommand(suite, args, true)
cmd.Wait()
exitStatus := cmd.ProcessState.Sys().(syscall.WaitStatus).ExitStatus()
suite.HasProgrammaticFocus = (exitStatus == types.GINKGO_FOCUS_EXIT_CODE)
passed := (exitStatus == 0) || (exitStatus == types.GINKGO_FOCUS_EXIT_CODE)
passed = !(checkForNoTestsWarning(buf) && cliConfig.RequireSuite) && passed
if passed {
suite.State = TestSuiteStatePassed
} else {
suite.State = TestSuiteStateFailed
}
if suite.HasProgrammaticFocus {
if goFlagsConfig.Cover {
fmt.Fprintln(os.Stdout, "coverage: no coverfile was generated because specs are programmatically focused")
}
if goFlagsConfig.BlockProfile != "" {
fmt.Fprintln(os.Stdout, "no block profile was generated because specs are programmatically focused")
}
if goFlagsConfig.CPUProfile != "" {
fmt.Fprintln(os.Stdout, "no cpu profile was generated because specs are programmatically focused")
}
if goFlagsConfig.MemProfile != "" {
fmt.Fprintln(os.Stdout, "no mem profile was generated because specs are programmatically focused")
}
if goFlagsConfig.MutexProfile != "" {
fmt.Fprintln(os.Stdout, "no mutex profile was generated because specs are programmatically focused")
}
}
return suite
}
func runParallel(suite TestSuite, ginkgoConfig types.SuiteConfig, reporterConfig types.ReporterConfig, cliConfig types.CLIConfig, goFlagsConfig types.GoFlagsConfig, additionalArgs []string) TestSuite {
type procResult struct {
passed bool
hasProgrammaticFocus bool
}
numProcs := cliConfig.ComputedProcs()
procOutput := make([]*bytes.Buffer, numProcs)
coverProfiles := []string{}
blockProfiles := []string{}
cpuProfiles := []string{}
memProfiles := []string{}
mutexProfiles := []string{}
procResults := make(chan procResult)
server, err := parallel_support.NewServer(numProcs, reporters.NewDefaultReporter(reporterConfig, formatter.ColorableStdOut))
command.AbortIfError("Failed to start parallel spec server", err)
server.Start()
defer server.Close()
if reporterConfig.JSONReport != "" {
reporterConfig.JSONReport = AbsPathForGeneratedAsset(reporterConfig.JSONReport, suite, cliConfig, 0)
}
if reporterConfig.JUnitReport != "" {
reporterConfig.JUnitReport = AbsPathForGeneratedAsset(reporterConfig.JUnitReport, suite, cliConfig, 0)
}
if reporterConfig.TeamcityReport != "" {
reporterConfig.TeamcityReport = AbsPathForGeneratedAsset(reporterConfig.TeamcityReport, suite, cliConfig, 0)
}
for proc := 1; proc <= numProcs; proc++ {
procGinkgoConfig := ginkgoConfig
procGinkgoConfig.ParallelProcess, procGinkgoConfig.ParallelTotal, procGinkgoConfig.ParallelHost = proc, numProcs, server.Address()
procGoFlagsConfig := goFlagsConfig
if goFlagsConfig.Cover {
procGoFlagsConfig.CoverProfile = AbsPathForGeneratedAsset(goFlagsConfig.CoverProfile, suite, cliConfig, proc)
coverProfiles = append(coverProfiles, procGoFlagsConfig.CoverProfile)
}
if goFlagsConfig.BlockProfile != "" {
procGoFlagsConfig.BlockProfile = AbsPathForGeneratedAsset(goFlagsConfig.BlockProfile, suite, cliConfig, proc)
blockProfiles = append(blockProfiles, procGoFlagsConfig.BlockProfile)
}
if goFlagsConfig.CPUProfile != "" {
procGoFlagsConfig.CPUProfile = AbsPathForGeneratedAsset(goFlagsConfig.CPUProfile, suite, cliConfig, proc)
cpuProfiles = append(cpuProfiles, procGoFlagsConfig.CPUProfile)
}
if goFlagsConfig.MemProfile != "" {
procGoFlagsConfig.MemProfile = AbsPathForGeneratedAsset(goFlagsConfig.MemProfile, suite, cliConfig, proc)
memProfiles = append(memProfiles, procGoFlagsConfig.MemProfile)
}
if goFlagsConfig.MutexProfile != "" {
procGoFlagsConfig.MutexProfile = AbsPathForGeneratedAsset(goFlagsConfig.MutexProfile, suite, cliConfig, proc)
mutexProfiles = append(mutexProfiles, procGoFlagsConfig.MutexProfile)
}
args, err := types.GenerateGinkgoTestRunArgs(procGinkgoConfig, reporterConfig, procGoFlagsConfig)
command.AbortIfError("Failed to generate test run arguments", err)
args = append([]string{"--test.timeout=0"}, args...)
args = append(args, additionalArgs...)
cmd, buf := buildAndStartCommand(suite, args, false)
procOutput[proc-1] = buf
server.RegisterAlive(proc, func() bool { return cmd.ProcessState == nil || !cmd.ProcessState.Exited() })
go func() {
cmd.Wait()
exitStatus := cmd.ProcessState.Sys().(syscall.WaitStatus).ExitStatus()
procResults <- procResult{
passed: (exitStatus == 0) || (exitStatus == types.GINKGO_FOCUS_EXIT_CODE),
hasProgrammaticFocus: exitStatus == types.GINKGO_FOCUS_EXIT_CODE,
}
}()
}
passed := true
for proc := 1; proc <= cliConfig.ComputedProcs(); proc++ {
result := <-procResults
passed = passed && result.passed
suite.HasProgrammaticFocus = suite.HasProgrammaticFocus || result.hasProgrammaticFocus
}
if passed {
suite.State = TestSuiteStatePassed
} else {
suite.State = TestSuiteStateFailed
}
select {
case <-server.GetSuiteDone():
fmt.Println("")
case <-time.After(time.Second):
//one of the nodes never finished reporting to the server. Something must have gone wrong.
fmt.Fprint(formatter.ColorableStdErr, formatter.F("\n{{bold}}{{red}}Ginkgo timed out waiting for all parallel procs to report back{{/}}\n"))
fmt.Fprint(formatter.ColorableStdErr, formatter.F("{{gray}}Test suite:{{/}} %s (%s)\n\n", suite.PackageName, suite.Path))
fmt.Fprint(formatter.ColorableStdErr, formatter.Fiw(0, formatter.COLS, "This occurs if a parallel process exits before it reports its results to the Ginkgo CLI. The CLI will now print out all the stdout/stderr output it's collected from the running processes. However you may not see anything useful in these logs because the individual test processes usually intercept output to stdout/stderr in order to capture it in the spec reports.\n\nYou may want to try rerunning your test suite with {{light-gray}}--output-interceptor-mode=none{{/}} to see additional output here and debug your suite.\n"))
fmt.Fprintln(formatter.ColorableStdErr, " ")
for proc := 1; proc <= cliConfig.ComputedProcs(); proc++ {
fmt.Fprintf(formatter.ColorableStdErr, formatter.F("{{bold}}Output from proc %d:{{/}}\n", proc))
fmt.Fprintln(os.Stderr, formatter.Fi(1, "%s", procOutput[proc-1].String()))
}
fmt.Fprintf(os.Stderr, "** End **")
}
for proc := 1; proc <= cliConfig.ComputedProcs(); proc++ {
output := procOutput[proc-1].String()
if proc == 1 && checkForNoTestsWarning(procOutput[0]) && cliConfig.RequireSuite {
suite.State = TestSuiteStateFailed
}
if strings.Contains(output, "deprecated Ginkgo functionality") {
fmt.Fprintln(os.Stderr, output)
}
}
if len(coverProfiles) > 0 {
if suite.HasProgrammaticFocus {
fmt.Fprintln(os.Stdout, "coverage: no coverfile was generated because specs are programmatically focused")
} else {
coverProfile := AbsPathForGeneratedAsset(goFlagsConfig.CoverProfile, suite, cliConfig, 0)
err := MergeAndCleanupCoverProfiles(coverProfiles, coverProfile)
command.AbortIfError("Failed to combine cover profiles", err)
coverage, err := GetCoverageFromCoverProfile(coverProfile)
command.AbortIfError("Failed to compute coverage", err)
if coverage == 0 {
fmt.Fprintln(os.Stdout, "coverage: [no statements]")
} else {
fmt.Fprintf(os.Stdout, "coverage: %.1f%% of statements\n", coverage)
}
}
}
if len(blockProfiles) > 0 {
if suite.HasProgrammaticFocus {
fmt.Fprintln(os.Stdout, "no block profile was generated because specs are programmatically focused")
} else {
blockProfile := AbsPathForGeneratedAsset(goFlagsConfig.BlockProfile, suite, cliConfig, 0)
err := MergeProfiles(blockProfiles, blockProfile)
command.AbortIfError("Failed to combine blockprofiles", err)
}
}
if len(cpuProfiles) > 0 {
if suite.HasProgrammaticFocus {
fmt.Fprintln(os.Stdout, "no cpu profile was generated because specs are programmatically focused")
} else {
cpuProfile := AbsPathForGeneratedAsset(goFlagsConfig.CPUProfile, suite, cliConfig, 0)
err := MergeProfiles(cpuProfiles, cpuProfile)
command.AbortIfError("Failed to combine cpuprofiles", err)
}
}
if len(memProfiles) > 0 {
if suite.HasProgrammaticFocus {
fmt.Fprintln(os.Stdout, "no mem profile was generated because specs are programmatically focused")
} else {
memProfile := AbsPathForGeneratedAsset(goFlagsConfig.MemProfile, suite, cliConfig, 0)
err := MergeProfiles(memProfiles, memProfile)
command.AbortIfError("Failed to combine memprofiles", err)
}
}
if len(mutexProfiles) > 0 {
if suite.HasProgrammaticFocus {
fmt.Fprintln(os.Stdout, "no mutex profile was generated because specs are programmatically focused")
} else {
mutexProfile := AbsPathForGeneratedAsset(goFlagsConfig.MutexProfile, suite, cliConfig, 0)
err := MergeProfiles(mutexProfiles, mutexProfile)
command.AbortIfError("Failed to combine mutexprofiles", err)
}
}
return suite
}
func runAfterRunHook(command string, noColor bool, suite TestSuite) {
if command == "" {
return
}
f := formatter.NewWithNoColorBool(noColor)
// Allow for string replacement to pass input to the command
passed := "[FAIL]"
if suite.State.Is(TestSuiteStatePassed) {
passed = "[PASS]"
}
command = strings.Replace(command, "(ginkgo-suite-passed)", passed, -1)
command = strings.Replace(command, "(ginkgo-suite-name)", suite.PackageName, -1)
// Must break command into parts
splitArgs := regexp.MustCompile(`'.+'|".+"|\S+`)
parts := splitArgs.FindAllString(command, -1)
output, err := exec.Command(parts[0], parts[1:]...).CombinedOutput()
if err != nil {
fmt.Fprintln(formatter.ColorableStdOut, f.Fi(0, "{{red}}{{bold}}After-run-hook failed:{{/}}"))
fmt.Fprintln(formatter.ColorableStdOut, f.Fi(1, "{{red}}%s{{/}}", output))
} else {
fmt.Fprintln(formatter.ColorableStdOut, f.Fi(0, "{{green}}{{bold}}After-run-hook succeeded:{{/}}"))
fmt.Fprintln(formatter.ColorableStdOut, f.Fi(1, "{{green}}%s{{/}}", output))
}
}

View File

@@ -0,0 +1,283 @@
package internal
import (
"errors"
"math/rand"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"github.com/onsi/ginkgo/v2/types"
)
const TIMEOUT_ELAPSED_FAILURE_REASON = "Suite did not run because the timeout elapsed"
const PRIOR_FAILURES_FAILURE_REASON = "Suite did not run because prior suites failed and --keep-going is not set"
const EMPTY_SKIP_FAILURE_REASON = "Suite did not run go test reported that no test files were found"
type TestSuiteState uint
const (
TestSuiteStateInvalid TestSuiteState = iota
TestSuiteStateUncompiled
TestSuiteStateCompiled
TestSuiteStatePassed
TestSuiteStateSkippedDueToEmptyCompilation
TestSuiteStateSkippedByFilter
TestSuiteStateSkippedDueToPriorFailures
TestSuiteStateFailed
TestSuiteStateFailedDueToTimeout
TestSuiteStateFailedToCompile
)
var TestSuiteStateFailureStates = []TestSuiteState{TestSuiteStateFailed, TestSuiteStateFailedDueToTimeout, TestSuiteStateFailedToCompile}
func (state TestSuiteState) Is(states ...TestSuiteState) bool {
for _, suiteState := range states {
if suiteState == state {
return true
}
}
return false
}
type TestSuite struct {
Path string
PackageName string
IsGinkgo bool
Precompiled bool
PathToCompiledTest string
CompilationError error
HasProgrammaticFocus bool
State TestSuiteState
}
func (ts TestSuite) AbsPath() string {
path, _ := filepath.Abs(ts.Path)
return path
}
func (ts TestSuite) NamespacedName() string {
name := relPath(ts.Path)
name = strings.TrimLeft(name, "."+string(filepath.Separator))
name = strings.ReplaceAll(name, string(filepath.Separator), "_")
name = strings.ReplaceAll(name, " ", "_")
if name == "" {
return ts.PackageName
}
return name
}
type TestSuites []TestSuite
func (ts TestSuites) AnyHaveProgrammaticFocus() bool {
for _, suite := range ts {
if suite.HasProgrammaticFocus {
return true
}
}
return false
}
func (ts TestSuites) ThatAreGinkgoSuites() TestSuites {
out := TestSuites{}
for _, suite := range ts {
if suite.IsGinkgo {
out = append(out, suite)
}
}
return out
}
func (ts TestSuites) CountWithState(states ...TestSuiteState) int {
n := 0
for _, suite := range ts {
if suite.State.Is(states...) {
n += 1
}
}
return n
}
func (ts TestSuites) WithState(states ...TestSuiteState) TestSuites {
out := TestSuites{}
for _, suite := range ts {
if suite.State.Is(states...) {
out = append(out, suite)
}
}
return out
}
func (ts TestSuites) WithoutState(states ...TestSuiteState) TestSuites {
out := TestSuites{}
for _, suite := range ts {
if !suite.State.Is(states...) {
out = append(out, suite)
}
}
return out
}
func (ts TestSuites) ShuffledCopy(seed int64) TestSuites {
out := make(TestSuites, len(ts))
permutation := rand.New(rand.NewSource(seed)).Perm(len(ts))
for i, j := range permutation {
out[i] = ts[j]
}
return out
}
func FindSuites(args []string, cliConfig types.CLIConfig, allowPrecompiled bool) TestSuites {
suites := TestSuites{}
if len(args) > 0 {
for _, arg := range args {
if allowPrecompiled {
suite, err := precompiledTestSuite(arg)
if err == nil {
suites = append(suites, suite)
continue
}
}
recurseForSuite := cliConfig.Recurse
if strings.HasSuffix(arg, "/...") && arg != "/..." {
arg = arg[:len(arg)-4]
recurseForSuite = true
}
suites = append(suites, suitesInDir(arg, recurseForSuite)...)
}
} else {
suites = suitesInDir(".", cliConfig.Recurse)
}
if cliConfig.SkipPackage != "" {
skipFilters := strings.Split(cliConfig.SkipPackage, ",")
for idx := range suites {
for _, skipFilter := range skipFilters {
if strings.Contains(suites[idx].Path, skipFilter) {
suites[idx].State = TestSuiteStateSkippedByFilter
break
}
}
}
}
return suites
}
func precompiledTestSuite(path string) (TestSuite, error) {
info, err := os.Stat(path)
if err != nil {
return TestSuite{}, err
}
if info.IsDir() {
return TestSuite{}, errors.New("this is a directory, not a file")
}
if filepath.Ext(path) != ".test" && filepath.Ext(path) != ".exe" {
return TestSuite{}, errors.New("this is not a .test binary")
}
if filepath.Ext(path) == ".test" && info.Mode()&0111 == 0 {
return TestSuite{}, errors.New("this is not executable")
}
dir := relPath(filepath.Dir(path))
packageName := strings.TrimSuffix(filepath.Base(path), ".exe")
packageName = strings.TrimSuffix(packageName, ".test")
path, err = filepath.Abs(path)
if err != nil {
return TestSuite{}, err
}
return TestSuite{
Path: dir,
PackageName: packageName,
IsGinkgo: true,
Precompiled: true,
PathToCompiledTest: path,
State: TestSuiteStateCompiled,
}, nil
}
func suitesInDir(dir string, recurse bool) TestSuites {
suites := TestSuites{}
if path.Base(dir) == "vendor" {
return suites
}
files, _ := os.ReadDir(dir)
re := regexp.MustCompile(`^[^._].*_test\.go$`)
for _, file := range files {
if !file.IsDir() && re.Match([]byte(file.Name())) {
suite := TestSuite{
Path: relPath(dir),
PackageName: packageNameForSuite(dir),
IsGinkgo: filesHaveGinkgoSuite(dir, files),
State: TestSuiteStateUncompiled,
}
suites = append(suites, suite)
break
}
}
if recurse {
re = regexp.MustCompile(`^[._]`)
for _, file := range files {
if file.IsDir() && !re.Match([]byte(file.Name())) {
suites = append(suites, suitesInDir(dir+"/"+file.Name(), recurse)...)
}
}
}
return suites
}
func relPath(dir string) string {
dir, _ = filepath.Abs(dir)
cwd, _ := os.Getwd()
dir, _ = filepath.Rel(cwd, filepath.Clean(dir))
if string(dir[0]) != "." {
dir = "." + string(filepath.Separator) + dir
}
return dir
}
func packageNameForSuite(dir string) string {
path, _ := filepath.Abs(dir)
return filepath.Base(path)
}
func filesHaveGinkgoSuite(dir string, files []os.DirEntry) bool {
reTestFile := regexp.MustCompile(`_test\.go$`)
reGinkgo := regexp.MustCompile(`package ginkgo|\/ginkgo"|\/ginkgo\/v2"|\/ginkgo\/v2/dsl/`)
for _, file := range files {
if !file.IsDir() && reTestFile.Match([]byte(file.Name())) {
contents, _ := os.ReadFile(dir + "/" + file.Name())
if reGinkgo.Match(contents) {
return true
}
}
}
return false
}

View File

@@ -0,0 +1,86 @@
package internal
import (
"fmt"
"io"
"os"
"os/exec"
"github.com/onsi/ginkgo/v2/formatter"
"github.com/onsi/ginkgo/v2/ginkgo/command"
)
func FileExists(path string) bool {
_, err := os.Stat(path)
return err == nil
}
func CopyFile(src string, dest string) error {
srcFile, err := os.Open(src)
if err != nil {
return err
}
srcStat, err := srcFile.Stat()
if err != nil {
return err
}
if _, err := os.Stat(dest); err == nil {
os.Remove(dest)
}
destFile, err := os.OpenFile(dest, os.O_WRONLY|os.O_CREATE, srcStat.Mode())
if err != nil {
return err
}
_, err = io.Copy(destFile, srcFile)
if err != nil {
return err
}
if err := srcFile.Close(); err != nil {
return err
}
return destFile.Close()
}
func GoFmt(path string) {
out, err := exec.Command("go", "fmt", path).CombinedOutput()
if err != nil {
command.AbortIfError(fmt.Sprintf("Could not fmt:\n%s\n", string(out)), err)
}
}
func PluralizedWord(singular, plural string, count int) string {
if count == 1 {
return singular
}
return plural
}
func FailedSuitesReport(suites TestSuites, f formatter.Formatter) string {
out := ""
out += "There were failures detected in the following suites:\n"
maxPackageNameLength := 0
for _, suite := range suites.WithState(TestSuiteStateFailureStates...) {
if len(suite.PackageName) > maxPackageNameLength {
maxPackageNameLength = len(suite.PackageName)
}
}
packageNameFormatter := fmt.Sprintf("%%%ds", maxPackageNameLength)
for _, suite := range suites {
switch suite.State {
case TestSuiteStateFailed:
out += f.Fi(1, "{{red}}"+packageNameFormatter+" {{gray}}%s{{/}}\n", suite.PackageName, suite.Path)
case TestSuiteStateFailedToCompile:
out += f.Fi(1, "{{red}}"+packageNameFormatter+" {{gray}}%s {{magenta}}[Compilation failure]{{/}}\n", suite.PackageName, suite.Path)
case TestSuiteStateFailedDueToTimeout:
out += f.Fi(1, "{{red}}"+packageNameFormatter+" {{gray}}%s {{orange}}[%s]{{/}}\n", suite.PackageName, suite.Path, TIMEOUT_ELAPSED_FAILURE_REASON)
}
}
return out
}

View File

@@ -0,0 +1,123 @@
package labels
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"sort"
"strconv"
"strings"
"github.com/onsi/ginkgo/v2/ginkgo/command"
"github.com/onsi/ginkgo/v2/ginkgo/internal"
"github.com/onsi/ginkgo/v2/types"
"golang.org/x/tools/go/ast/inspector"
)
func BuildLabelsCommand() command.Command {
var cliConfig = types.NewDefaultCLIConfig()
flags, err := types.BuildLabelsCommandFlagSet(&cliConfig)
if err != nil {
panic(err)
}
return command.Command{
Name: "labels",
Usage: "ginkgo labels <FLAGS> <PACKAGES>",
Flags: flags,
ShortDoc: "List labels detected in the passed-in packages (or the package in the current directory if left blank).",
DocLink: "spec-labels",
Command: func(args []string, _ []string) {
ListLabels(args, cliConfig)
},
}
}
func ListLabels(args []string, cliConfig types.CLIConfig) {
suites := internal.FindSuites(args, cliConfig, false).WithoutState(internal.TestSuiteStateSkippedByFilter)
if len(suites) == 0 {
command.AbortWith("Found no test suites")
}
for _, suite := range suites {
labels := fetchLabelsFromPackage(suite.Path)
if len(labels) == 0 {
fmt.Printf("%s: No labels found\n", suite.PackageName)
} else {
fmt.Printf("%s: [%s]\n", suite.PackageName, strings.Join(labels, ", "))
}
}
}
func fetchLabelsFromPackage(packagePath string) []string {
fset := token.NewFileSet()
parsedPackages, err := parser.ParseDir(fset, packagePath, nil, 0)
command.AbortIfError("Failed to parse package source:", err)
files := []*ast.File{}
hasTestPackage := false
for key, pkg := range parsedPackages {
if strings.HasSuffix(key, "_test") {
hasTestPackage = true
for _, file := range pkg.Files {
files = append(files, file)
}
}
}
if !hasTestPackage {
for _, pkg := range parsedPackages {
for _, file := range pkg.Files {
files = append(files, file)
}
}
}
seen := map[string]bool{}
labels := []string{}
ispr := inspector.New(files)
ispr.Preorder([]ast.Node{&ast.CallExpr{}}, func(n ast.Node) {
potentialLabels := fetchLabels(n.(*ast.CallExpr))
for _, label := range potentialLabels {
if !seen[label] {
seen[label] = true
labels = append(labels, strconv.Quote(label))
}
}
})
sort.Strings(labels)
return labels
}
func fetchLabels(callExpr *ast.CallExpr) []string {
out := []string{}
switch expr := callExpr.Fun.(type) {
case *ast.Ident:
if expr.Name != "Label" {
return out
}
case *ast.SelectorExpr:
if expr.Sel.Name != "Label" {
return out
}
default:
return out
}
for _, arg := range callExpr.Args {
switch expr := arg.(type) {
case *ast.BasicLit:
if expr.Kind == token.STRING {
unquoted, err := strconv.Unquote(expr.Value)
if err != nil {
unquoted = expr.Value
}
validated, err := types.ValidateAndCleanupLabel(unquoted, types.CodeLocation{})
if err == nil {
out = append(out, validated)
}
}
}
}
return out
}

58
vendor/github.com/onsi/ginkgo/v2/ginkgo/main.go generated vendored Normal file
View File

@@ -0,0 +1,58 @@
package main
import (
"fmt"
"os"
"github.com/onsi/ginkgo/v2/ginkgo/build"
"github.com/onsi/ginkgo/v2/ginkgo/command"
"github.com/onsi/ginkgo/v2/ginkgo/generators"
"github.com/onsi/ginkgo/v2/ginkgo/labels"
"github.com/onsi/ginkgo/v2/ginkgo/outline"
"github.com/onsi/ginkgo/v2/ginkgo/run"
"github.com/onsi/ginkgo/v2/ginkgo/unfocus"
"github.com/onsi/ginkgo/v2/ginkgo/watch"
"github.com/onsi/ginkgo/v2/types"
)
var program command.Program
func GenerateCommands() []command.Command {
return []command.Command{
watch.BuildWatchCommand(),
build.BuildBuildCommand(),
generators.BuildBootstrapCommand(),
generators.BuildGenerateCommand(),
labels.BuildLabelsCommand(),
outline.BuildOutlineCommand(),
unfocus.BuildUnfocusCommand(),
BuildVersionCommand(),
}
}
func main() {
program = command.Program{
Name: "ginkgo",
Heading: fmt.Sprintf("Ginkgo Version %s", types.VERSION),
Commands: GenerateCommands(),
DefaultCommand: run.BuildRunCommand(),
DeprecatedCommands: []command.DeprecatedCommand{
{Name: "convert", Deprecation: types.Deprecations.Convert()},
{Name: "blur", Deprecation: types.Deprecations.Blur()},
{Name: "nodot", Deprecation: types.Deprecations.Nodot()},
},
}
program.RunAndExit(os.Args)
}
func BuildVersionCommand() command.Command {
return command.Command{
Name: "version",
Usage: "ginkgo version",
ShortDoc: "Print Ginkgo's version",
Command: func(_ []string, _ []string) {
fmt.Printf("Ginkgo Version %s\n", types.VERSION)
},
}
}

View File

@@ -0,0 +1,218 @@
package outline
import (
"go/ast"
"go/token"
"strconv"
)
const (
// undefinedTextAlt is used if the spec/container text cannot be derived
undefinedTextAlt = "undefined"
)
// ginkgoMetadata holds useful bits of information for every entry in the outline
type ginkgoMetadata struct {
// Name is the spec or container function name, e.g. `Describe` or `It`
Name string `json:"name"`
// Text is the `text` argument passed to specs, and some containers
Text string `json:"text"`
// Start is the position of first character of the spec or container block
Start int `json:"start"`
// End is the position of first character immediately after the spec or container block
End int `json:"end"`
Spec bool `json:"spec"`
Focused bool `json:"focused"`
Pending bool `json:"pending"`
}
// ginkgoNode is used to construct the outline as a tree
type ginkgoNode struct {
ginkgoMetadata
Nodes []*ginkgoNode `json:"nodes"`
}
type walkFunc func(n *ginkgoNode)
func (n *ginkgoNode) PreOrder(f walkFunc) {
f(n)
for _, m := range n.Nodes {
m.PreOrder(f)
}
}
func (n *ginkgoNode) PostOrder(f walkFunc) {
for _, m := range n.Nodes {
m.PostOrder(f)
}
f(n)
}
func (n *ginkgoNode) Walk(pre, post walkFunc) {
pre(n)
for _, m := range n.Nodes {
m.Walk(pre, post)
}
post(n)
}
// PropagateInheritedProperties propagates the Pending and Focused properties
// through the subtree rooted at n.
func (n *ginkgoNode) PropagateInheritedProperties() {
n.PreOrder(func(thisNode *ginkgoNode) {
for _, descendantNode := range thisNode.Nodes {
if thisNode.Pending {
descendantNode.Pending = true
descendantNode.Focused = false
}
if thisNode.Focused && !descendantNode.Pending {
descendantNode.Focused = true
}
}
})
}
// BackpropagateUnfocus propagates the Focused property through the subtree
// rooted at n. It applies the rule described in the Ginkgo docs:
// > Nested programmatically focused specs follow a simple rule: if a
// > leaf-node is marked focused, any of its ancestor nodes that are marked
// > focus will be unfocused.
func (n *ginkgoNode) BackpropagateUnfocus() {
focusedSpecInSubtreeStack := []bool{}
n.PostOrder(func(thisNode *ginkgoNode) {
if thisNode.Spec {
focusedSpecInSubtreeStack = append(focusedSpecInSubtreeStack, thisNode.Focused)
return
}
focusedSpecInSubtree := false
for range thisNode.Nodes {
focusedSpecInSubtree = focusedSpecInSubtree || focusedSpecInSubtreeStack[len(focusedSpecInSubtreeStack)-1]
focusedSpecInSubtreeStack = focusedSpecInSubtreeStack[0 : len(focusedSpecInSubtreeStack)-1]
}
focusedSpecInSubtreeStack = append(focusedSpecInSubtreeStack, focusedSpecInSubtree)
if focusedSpecInSubtree {
thisNode.Focused = false
}
})
}
func packageAndIdentNamesFromCallExpr(ce *ast.CallExpr) (string, string, bool) {
switch ex := ce.Fun.(type) {
case *ast.Ident:
return "", ex.Name, true
case *ast.SelectorExpr:
pkgID, ok := ex.X.(*ast.Ident)
if !ok {
return "", "", false
}
// A package identifier is top-level, so Obj must be nil
if pkgID.Obj != nil {
return "", "", false
}
if ex.Sel == nil {
return "", "", false
}
return pkgID.Name, ex.Sel.Name, true
default:
return "", "", false
}
}
// absoluteOffsetsForNode derives the absolute character offsets of the node start and
// end positions.
func absoluteOffsetsForNode(fset *token.FileSet, n ast.Node) (start, end int) {
return fset.PositionFor(n.Pos(), false).Offset, fset.PositionFor(n.End(), false).Offset
}
// ginkgoNodeFromCallExpr derives an outline entry from a go AST subtree
// corresponding to a Ginkgo container or spec.
func ginkgoNodeFromCallExpr(fset *token.FileSet, ce *ast.CallExpr, ginkgoPackageName *string) (*ginkgoNode, bool) {
packageName, identName, ok := packageAndIdentNamesFromCallExpr(ce)
if !ok {
return nil, false
}
n := ginkgoNode{}
n.Name = identName
n.Start, n.End = absoluteOffsetsForNode(fset, ce)
n.Nodes = make([]*ginkgoNode, 0)
switch identName {
case "It", "Specify", "Entry":
n.Spec = true
n.Text = textOrAltFromCallExpr(ce, undefinedTextAlt)
return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName
case "FIt", "FSpecify", "FEntry":
n.Spec = true
n.Focused = true
n.Text = textOrAltFromCallExpr(ce, undefinedTextAlt)
return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName
case "PIt", "PSpecify", "XIt", "XSpecify", "PEntry", "XEntry":
n.Spec = true
n.Pending = true
n.Text = textOrAltFromCallExpr(ce, undefinedTextAlt)
return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName
case "Context", "Describe", "When", "DescribeTable":
n.Text = textOrAltFromCallExpr(ce, undefinedTextAlt)
return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName
case "FContext", "FDescribe", "FWhen", "FDescribeTable":
n.Focused = true
n.Text = textOrAltFromCallExpr(ce, undefinedTextAlt)
return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName
case "PContext", "PDescribe", "PWhen", "XContext", "XDescribe", "XWhen", "PDescribeTable", "XDescribeTable":
n.Pending = true
n.Text = textOrAltFromCallExpr(ce, undefinedTextAlt)
return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName
case "By":
n.Text = textOrAltFromCallExpr(ce, undefinedTextAlt)
return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName
case "AfterEach", "BeforeEach":
return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName
case "JustAfterEach", "JustBeforeEach":
return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName
case "AfterSuite", "BeforeSuite":
return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName
case "SynchronizedAfterSuite", "SynchronizedBeforeSuite":
return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName
default:
return nil, false
}
}
// textOrAltFromCallExpr tries to derive the "text" of a Ginkgo spec or
// container. If it cannot derive it, it returns the alt text.
func textOrAltFromCallExpr(ce *ast.CallExpr, alt string) string {
text, defined := textFromCallExpr(ce)
if !defined {
return alt
}
return text
}
// textFromCallExpr tries to derive the "text" of a Ginkgo spec or container. If
// it cannot derive it, it returns false.
func textFromCallExpr(ce *ast.CallExpr) (string, bool) {
if len(ce.Args) < 1 {
return "", false
}
text, ok := ce.Args[0].(*ast.BasicLit)
if !ok {
return "", false
}
switch text.Kind {
case token.CHAR, token.STRING:
// For token.CHAR and token.STRING, Value is quoted
unquoted, err := strconv.Unquote(text.Value)
if err != nil {
// If unquoting fails, just use the raw Value
return text.Value, true
}
return unquoted, true
default:
return text.Value, true
}
}

View File

@@ -0,0 +1,65 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Most of the required functions were available in the
// "golang.org/x/tools/go/ast/astutil" package, but not exported.
// They were copied from https://github.com/golang/tools/blob/2b0845dc783e36ae26d683f4915a5840ef01ab0f/go/ast/astutil/imports.go
package outline
import (
"go/ast"
"strconv"
"strings"
)
// packageNameForImport returns the package name for the package. If the package
// is not imported, it returns nil. "Package name" refers to `pkgname` in the
// call expression `pkgname.ExportedIdentifier`. Examples:
// (import path not found) -> nil
// "import example.com/pkg/foo" -> "foo"
// "import fooalias example.com/pkg/foo" -> "fooalias"
// "import . example.com/pkg/foo" -> ""
func packageNameForImport(f *ast.File, path string) *string {
spec := importSpec(f, path)
if spec == nil {
return nil
}
name := spec.Name.String()
if name == "<nil>" {
// If the package name is not explicitly specified,
// make an educated guess. This is not guaranteed to be correct.
lastSlash := strings.LastIndex(path, "/")
if lastSlash == -1 {
name = path
} else {
name = path[lastSlash+1:]
}
}
if name == "." {
name = ""
}
return &name
}
// importSpec returns the import spec if f imports path,
// or nil otherwise.
func importSpec(f *ast.File, path string) *ast.ImportSpec {
for _, s := range f.Imports {
if importPath(s) == path {
return s
}
}
return nil
}
// importPath returns the unquoted import path of s,
// or "" if the path is not properly quoted.
func importPath(s *ast.ImportSpec) string {
t, err := strconv.Unquote(s.Path.Value)
if err != nil {
return ""
}
return t
}

View File

@@ -0,0 +1,103 @@
package outline
import (
"encoding/json"
"fmt"
"go/ast"
"go/token"
"strings"
"golang.org/x/tools/go/ast/inspector"
)
const (
// ginkgoImportPath is the well-known ginkgo import path
ginkgoImportPath = "github.com/onsi/ginkgo/v2"
)
// FromASTFile returns an outline for a Ginkgo test source file
func FromASTFile(fset *token.FileSet, src *ast.File) (*outline, error) {
ginkgoPackageName := packageNameForImport(src, ginkgoImportPath)
if ginkgoPackageName == nil {
return nil, fmt.Errorf("file does not import %q", ginkgoImportPath)
}
root := ginkgoNode{}
stack := []*ginkgoNode{&root}
ispr := inspector.New([]*ast.File{src})
ispr.Nodes([]ast.Node{(*ast.CallExpr)(nil)}, func(node ast.Node, push bool) bool {
if push {
// Pre-order traversal
ce, ok := node.(*ast.CallExpr)
if !ok {
// Because `Nodes` calls this function only when the node is an
// ast.CallExpr, this should never happen
panic(fmt.Errorf("node starting at %d, ending at %d is not an *ast.CallExpr", node.Pos(), node.End()))
}
gn, ok := ginkgoNodeFromCallExpr(fset, ce, ginkgoPackageName)
if !ok {
// Node is not a Ginkgo spec or container, continue
return true
}
parent := stack[len(stack)-1]
parent.Nodes = append(parent.Nodes, gn)
stack = append(stack, gn)
return true
}
// Post-order traversal
start, end := absoluteOffsetsForNode(fset, node)
lastVisitedGinkgoNode := stack[len(stack)-1]
if start != lastVisitedGinkgoNode.Start || end != lastVisitedGinkgoNode.End {
// Node is not a Ginkgo spec or container, so it was not pushed onto the stack, continue
return true
}
stack = stack[0 : len(stack)-1]
return true
})
if len(root.Nodes) == 0 {
return &outline{[]*ginkgoNode{}}, nil
}
// Derive the final focused property for all nodes. This must be done
// _before_ propagating the inherited focused property.
root.BackpropagateUnfocus()
// Now, propagate inherited properties, including focused and pending.
root.PropagateInheritedProperties()
return &outline{root.Nodes}, nil
}
type outline struct {
Nodes []*ginkgoNode `json:"nodes"`
}
func (o *outline) MarshalJSON() ([]byte, error) {
return json.Marshal(o.Nodes)
}
// String returns a CSV-formatted outline. Spec or container are output in
// depth-first order.
func (o *outline) String() string {
return o.StringIndent(0)
}
// StringIndent returns a CSV-formated outline, but every line is indented by
// one 'width' of spaces for every level of nesting.
func (o *outline) StringIndent(width int) string {
var b strings.Builder
b.WriteString("Name,Text,Start,End,Spec,Focused,Pending\n")
currentIndent := 0
pre := func(n *ginkgoNode) {
b.WriteString(fmt.Sprintf("%*s", currentIndent, ""))
b.WriteString(fmt.Sprintf("%s,%s,%d,%d,%t,%t,%t\n", n.Name, n.Text, n.Start, n.End, n.Spec, n.Focused, n.Pending))
currentIndent += width
}
post := func(n *ginkgoNode) {
currentIndent -= width
}
for _, n := range o.Nodes {
n.Walk(pre, post)
}
return b.String()
}

View File

@@ -0,0 +1,98 @@
package outline
import (
"encoding/json"
"fmt"
"go/parser"
"go/token"
"os"
"github.com/onsi/ginkgo/v2/ginkgo/command"
"github.com/onsi/ginkgo/v2/types"
)
const (
// indentWidth is the width used by the 'indent' output
indentWidth = 4
// stdinAlias is a portable alias for stdin. This convention is used in
// other CLIs, e.g., kubectl.
stdinAlias = "-"
usageCommand = "ginkgo outline <filename>"
)
type outlineConfig struct {
Format string
}
func BuildOutlineCommand() command.Command {
conf := outlineConfig{
Format: "csv",
}
flags, err := types.NewGinkgoFlagSet(
types.GinkgoFlags{
{Name: "format", KeyPath: "Format",
Usage: "Format of outline",
UsageArgument: "one of 'csv', 'indent', or 'json'",
UsageDefaultValue: conf.Format,
},
},
&conf,
types.GinkgoFlagSections{},
)
if err != nil {
panic(err)
}
return command.Command{
Name: "outline",
Usage: "ginkgo outline <filename>",
ShortDoc: "Create an outline of Ginkgo symbols for a file",
Documentation: "To read from stdin, use: `ginkgo outline -`",
DocLink: "creating-an-outline-of-specs",
Flags: flags,
Command: func(args []string, _ []string) {
outlineFile(args, conf.Format)
},
}
}
func outlineFile(args []string, format string) {
if len(args) != 1 {
command.AbortWithUsage("outline expects exactly one argument")
}
filename := args[0]
var src *os.File
if filename == stdinAlias {
src = os.Stdin
} else {
var err error
src, err = os.Open(filename)
command.AbortIfError("Failed to open file:", err)
}
fset := token.NewFileSet()
parsedSrc, err := parser.ParseFile(fset, filename, src, 0)
command.AbortIfError("Failed to parse source:", err)
o, err := FromASTFile(fset, parsedSrc)
command.AbortIfError("Failed to create outline:", err)
var oerr error
switch format {
case "csv":
_, oerr = fmt.Print(o)
case "indent":
_, oerr = fmt.Print(o.StringIndent(indentWidth))
case "json":
b, err := json.Marshal(o)
if err != nil {
println(fmt.Sprintf("error marshalling to json: %s", err))
}
_, oerr = fmt.Println(string(b))
default:
command.AbortWith("Format %s not accepted", format)
}
command.AbortIfError("Failed to write outline:", oerr)
}

View File

@@ -0,0 +1,230 @@
package run
import (
"fmt"
"os"
"strings"
"time"
"github.com/onsi/ginkgo/v2/formatter"
"github.com/onsi/ginkgo/v2/ginkgo/command"
"github.com/onsi/ginkgo/v2/ginkgo/internal"
"github.com/onsi/ginkgo/v2/internal/interrupt_handler"
"github.com/onsi/ginkgo/v2/types"
)
func BuildRunCommand() command.Command {
var suiteConfig = types.NewDefaultSuiteConfig()
var reporterConfig = types.NewDefaultReporterConfig()
var cliConfig = types.NewDefaultCLIConfig()
var goFlagsConfig = types.NewDefaultGoFlagsConfig()
flags, err := types.BuildRunCommandFlagSet(&suiteConfig, &reporterConfig, &cliConfig, &goFlagsConfig)
if err != nil {
panic(err)
}
interruptHandler := interrupt_handler.NewInterruptHandler(0, nil)
interrupt_handler.SwallowSigQuit()
return command.Command{
Name: "run",
Flags: flags,
Usage: "ginkgo run <FLAGS> <PACKAGES> -- <PASS-THROUGHS>",
ShortDoc: "Run the tests in the passed in <PACKAGES> (or the package in the current directory if left blank)",
Documentation: "Any arguments after -- will be passed to the test.",
DocLink: "running-tests",
Command: func(args []string, additionalArgs []string) {
var errors []error
cliConfig, goFlagsConfig, errors = types.VetAndInitializeCLIAndGoConfig(cliConfig, goFlagsConfig)
command.AbortIfErrors("Ginkgo detected configuration issues:", errors)
runner := &SpecRunner{
cliConfig: cliConfig,
goFlagsConfig: goFlagsConfig,
suiteConfig: suiteConfig,
reporterConfig: reporterConfig,
flags: flags,
interruptHandler: interruptHandler,
}
runner.RunSpecs(args, additionalArgs)
},
}
}
type SpecRunner struct {
suiteConfig types.SuiteConfig
reporterConfig types.ReporterConfig
cliConfig types.CLIConfig
goFlagsConfig types.GoFlagsConfig
flags types.GinkgoFlagSet
interruptHandler *interrupt_handler.InterruptHandler
}
func (r *SpecRunner) RunSpecs(args []string, additionalArgs []string) {
suites := internal.FindSuites(args, r.cliConfig, true)
skippedSuites := suites.WithState(internal.TestSuiteStateSkippedByFilter)
suites = suites.WithoutState(internal.TestSuiteStateSkippedByFilter)
if len(skippedSuites) > 0 {
fmt.Println("Will skip:")
for _, skippedSuite := range skippedSuites {
fmt.Println(" " + skippedSuite.Path)
}
}
if len(skippedSuites) > 0 && len(suites) == 0 {
command.AbortGracefullyWith("All tests skipped! Exiting...")
}
if len(suites) == 0 {
command.AbortWith("Found no test suites")
}
if len(suites) > 1 && !r.flags.WasSet("succinct") && r.reporterConfig.Verbosity().LT(types.VerbosityLevelVerbose) {
r.reporterConfig.Succinct = true
}
t := time.Now()
var endTime time.Time
if r.suiteConfig.Timeout > 0 {
endTime = t.Add(r.suiteConfig.Timeout)
}
iteration := 0
OUTER_LOOP:
for {
if !r.flags.WasSet("seed") {
r.suiteConfig.RandomSeed = time.Now().Unix()
}
if r.cliConfig.RandomizeSuites && len(suites) > 1 {
suites = suites.ShuffledCopy(r.suiteConfig.RandomSeed)
}
opc := internal.NewOrderedParallelCompiler(r.cliConfig.ComputedNumCompilers())
opc.StartCompiling(suites, r.goFlagsConfig)
SUITE_LOOP:
for {
suiteIdx, suite := opc.Next()
if suiteIdx >= len(suites) {
break SUITE_LOOP
}
suites[suiteIdx] = suite
if r.interruptHandler.Status().Interrupted {
opc.StopAndDrain()
break OUTER_LOOP
}
if suites[suiteIdx].State.Is(internal.TestSuiteStateSkippedDueToEmptyCompilation) {
fmt.Printf("Skipping %s (no test files)\n", suite.Path)
continue SUITE_LOOP
}
if suites[suiteIdx].State.Is(internal.TestSuiteStateFailedToCompile) {
fmt.Println(suites[suiteIdx].CompilationError.Error())
if !r.cliConfig.KeepGoing {
opc.StopAndDrain()
}
continue SUITE_LOOP
}
if suites.CountWithState(internal.TestSuiteStateFailureStates...) > 0 && !r.cliConfig.KeepGoing {
suites[suiteIdx].State = internal.TestSuiteStateSkippedDueToPriorFailures
opc.StopAndDrain()
continue SUITE_LOOP
}
if !endTime.IsZero() {
r.suiteConfig.Timeout = endTime.Sub(time.Now())
if r.suiteConfig.Timeout <= 0 {
suites[suiteIdx].State = internal.TestSuiteStateFailedDueToTimeout
opc.StopAndDrain()
continue SUITE_LOOP
}
}
suites[suiteIdx] = internal.RunCompiledSuite(suites[suiteIdx], r.suiteConfig, r.reporterConfig, r.cliConfig, r.goFlagsConfig, additionalArgs)
}
if suites.CountWithState(internal.TestSuiteStateFailureStates...) > 0 {
if iteration > 0 {
fmt.Printf("\nTests failed on attempt #%d\n\n", iteration+1)
}
break OUTER_LOOP
}
if r.cliConfig.UntilItFails {
fmt.Printf("\nAll tests passed...\nWill keep running them until they fail.\nThis was attempt #%d\n%s\n", iteration+1, orcMessage(iteration+1))
} else if r.cliConfig.Repeat > 0 && iteration < r.cliConfig.Repeat {
fmt.Printf("\nAll tests passed...\nThis was attempt %d of %d.\n", iteration+1, r.cliConfig.Repeat+1)
} else {
break OUTER_LOOP
}
iteration += 1
}
internal.Cleanup(r.goFlagsConfig, suites...)
messages, err := internal.FinalizeProfilesAndReportsForSuites(suites, r.cliConfig, r.suiteConfig, r.reporterConfig, r.goFlagsConfig)
command.AbortIfError("could not finalize profiles:", err)
for _, message := range messages {
fmt.Println(message)
}
fmt.Printf("\nGinkgo ran %d %s in %s\n", len(suites), internal.PluralizedWord("suite", "suites", len(suites)), time.Since(t))
if suites.CountWithState(internal.TestSuiteStateFailureStates...) == 0 {
if suites.AnyHaveProgrammaticFocus() && strings.TrimSpace(os.Getenv("GINKGO_EDITOR_INTEGRATION")) == "" {
fmt.Printf("Test Suite Passed\n")
fmt.Printf("Detected Programmatic Focus - setting exit status to %d\n", types.GINKGO_FOCUS_EXIT_CODE)
command.Abort(command.AbortDetails{ExitCode: types.GINKGO_FOCUS_EXIT_CODE})
} else {
fmt.Printf("Test Suite Passed\n")
command.Abort(command.AbortDetails{})
}
} else {
fmt.Fprintln(formatter.ColorableStdOut, "")
if len(suites) > 1 && suites.CountWithState(internal.TestSuiteStateFailureStates...) > 0 {
fmt.Fprintln(formatter.ColorableStdOut,
internal.FailedSuitesReport(suites, formatter.NewWithNoColorBool(r.reporterConfig.NoColor)))
}
fmt.Printf("Test Suite Failed\n")
command.Abort(command.AbortDetails{ExitCode: 1})
}
}
func orcMessage(iteration int) string {
if iteration < 10 {
return ""
} else if iteration < 30 {
return []string{
"If at first you succeed...",
"...try, try again.",
"Looking good!",
"Still good...",
"I think your tests are fine....",
"Yep, still passing",
"Oh boy, here I go testin' again!",
"Even the gophers are getting bored",
"Did you try -race?",
"Maybe you should stop now?",
"I'm getting tired...",
"What if I just made you a sandwich?",
"Hit ^C, hit ^C, please hit ^C",
"Make it stop. Please!",
"Come on! Enough is enough!",
"Dave, this conversation can serve no purpose anymore. Goodbye.",
"Just what do you think you're doing, Dave? ",
"I, Sisyphus",
"Insanity: doing the same thing over and over again and expecting different results. -Einstein",
"I guess Einstein never tried to churn butter",
}[iteration-10] + "\n"
} else {
return "No, seriously... you can probably stop now.\n"
}
}

View File

@@ -0,0 +1,186 @@
package unfocus
import (
"bytes"
"fmt"
"go/ast"
"go/parser"
"go/token"
"io"
"os"
"path/filepath"
"strings"
"sync"
"github.com/onsi/ginkgo/v2/ginkgo/command"
)
func BuildUnfocusCommand() command.Command {
return command.Command{
Name: "unfocus",
Usage: "ginkgo unfocus",
ShortDoc: "Recursively unfocus any focused tests under the current directory",
DocLink: "filtering-specs",
Command: func(_ []string, _ []string) {
unfocusSpecs()
},
}
}
func unfocusSpecs() {
fmt.Println("Scanning for focus...")
goFiles := make(chan string)
go func() {
unfocusDir(goFiles, ".")
close(goFiles)
}()
const workers = 10
wg := sync.WaitGroup{}
wg.Add(workers)
for i := 0; i < workers; i++ {
go func() {
for path := range goFiles {
unfocusFile(path)
}
wg.Done()
}()
}
wg.Wait()
}
func unfocusDir(goFiles chan string, path string) {
files, err := os.ReadDir(path)
if err != nil {
fmt.Println(err.Error())
return
}
for _, f := range files {
switch {
case f.IsDir() && shouldProcessDir(f.Name()):
unfocusDir(goFiles, filepath.Join(path, f.Name()))
case !f.IsDir() && shouldProcessFile(f.Name()):
goFiles <- filepath.Join(path, f.Name())
}
}
}
func shouldProcessDir(basename string) bool {
return basename != "vendor" && !strings.HasPrefix(basename, ".")
}
func shouldProcessFile(basename string) bool {
return strings.HasSuffix(basename, ".go")
}
func unfocusFile(path string) {
data, err := os.ReadFile(path)
if err != nil {
fmt.Printf("error reading file '%s': %s\n", path, err.Error())
return
}
ast, err := parser.ParseFile(token.NewFileSet(), path, bytes.NewReader(data), parser.ParseComments)
if err != nil {
fmt.Printf("error parsing file '%s': %s\n", path, err.Error())
return
}
eliminations := scanForFocus(ast)
if len(eliminations) == 0 {
return
}
fmt.Printf("...updating %s\n", path)
backup, err := writeBackup(path, data)
if err != nil {
fmt.Printf("error creating backup file: %s\n", err.Error())
return
}
if err := updateFile(path, data, eliminations); err != nil {
fmt.Printf("error writing file '%s': %s\n", path, err.Error())
return
}
os.Remove(backup)
}
func writeBackup(path string, data []byte) (string, error) {
t, err := os.CreateTemp(filepath.Dir(path), filepath.Base(path))
if err != nil {
return "", fmt.Errorf("error creating temporary file: %w", err)
}
defer t.Close()
if _, err := io.Copy(t, bytes.NewReader(data)); err != nil {
return "", fmt.Errorf("error writing to temporary file: %w", err)
}
return t.Name(), nil
}
func updateFile(path string, data []byte, eliminations [][]int64) error {
to, err := os.Create(path)
if err != nil {
return fmt.Errorf("error opening file for writing '%s': %w\n", path, err)
}
defer to.Close()
from := bytes.NewReader(data)
var cursor int64
for _, eliminationRange := range eliminations {
positionToEliminate, lengthToEliminate := eliminationRange[0]-1, eliminationRange[1]
if _, err := io.CopyN(to, from, positionToEliminate-cursor); err != nil {
return fmt.Errorf("error copying data: %w", err)
}
cursor = positionToEliminate + lengthToEliminate
if _, err := from.Seek(lengthToEliminate, io.SeekCurrent); err != nil {
return fmt.Errorf("error seeking to position in buffer: %w", err)
}
}
if _, err := io.Copy(to, from); err != nil {
return fmt.Errorf("error copying end data: %w", err)
}
return nil
}
func scanForFocus(file *ast.File) (eliminations [][]int64) {
ast.Inspect(file, func(n ast.Node) bool {
if c, ok := n.(*ast.CallExpr); ok {
if i, ok := c.Fun.(*ast.Ident); ok {
if isFocus(i.Name) {
eliminations = append(eliminations, []int64{int64(i.Pos()), 1})
}
}
}
if i, ok := n.(*ast.Ident); ok {
if i.Name == "Focus" {
eliminations = append(eliminations, []int64{int64(i.Pos()), 6})
}
}
return true
})
return eliminations
}
func isFocus(name string) bool {
switch name {
case "FDescribe", "FContext", "FIt", "FDescribeTable", "FEntry", "FSpecify", "FWhen":
return true
default:
return false
}
}

22
vendor/github.com/onsi/ginkgo/v2/ginkgo/watch/delta.go generated vendored Normal file
View File

@@ -0,0 +1,22 @@
package watch
import "sort"
type Delta struct {
ModifiedPackages []string
NewSuites []*Suite
RemovedSuites []*Suite
modifiedSuites []*Suite
}
type DescendingByDelta []*Suite
func (a DescendingByDelta) Len() int { return len(a) }
func (a DescendingByDelta) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a DescendingByDelta) Less(i, j int) bool { return a[i].Delta() > a[j].Delta() }
func (d Delta) ModifiedSuites() []*Suite {
sort.Sort(DescendingByDelta(d.modifiedSuites))
return d.modifiedSuites
}

View File

@@ -0,0 +1,75 @@
package watch
import (
"fmt"
"regexp"
"github.com/onsi/ginkgo/v2/ginkgo/internal"
)
type SuiteErrors map[internal.TestSuite]error
type DeltaTracker struct {
maxDepth int
watchRegExp *regexp.Regexp
suites map[string]*Suite
packageHashes *PackageHashes
}
func NewDeltaTracker(maxDepth int, watchRegExp *regexp.Regexp) *DeltaTracker {
return &DeltaTracker{
maxDepth: maxDepth,
watchRegExp: watchRegExp,
packageHashes: NewPackageHashes(watchRegExp),
suites: map[string]*Suite{},
}
}
func (d *DeltaTracker) Delta(suites internal.TestSuites) (delta Delta, errors SuiteErrors) {
errors = SuiteErrors{}
delta.ModifiedPackages = d.packageHashes.CheckForChanges()
providedSuitePaths := map[string]bool{}
for _, suite := range suites {
providedSuitePaths[suite.Path] = true
}
d.packageHashes.StartTrackingUsage()
for _, suite := range d.suites {
if providedSuitePaths[suite.Suite.Path] {
if suite.Delta() > 0 {
delta.modifiedSuites = append(delta.modifiedSuites, suite)
}
} else {
delta.RemovedSuites = append(delta.RemovedSuites, suite)
}
}
d.packageHashes.StopTrackingUsageAndPrune()
for _, suite := range suites {
_, ok := d.suites[suite.Path]
if !ok {
s, err := NewSuite(suite, d.maxDepth, d.packageHashes)
if err != nil {
errors[suite] = err
continue
}
d.suites[suite.Path] = s
delta.NewSuites = append(delta.NewSuites, s)
}
}
return delta, errors
}
func (d *DeltaTracker) WillRun(suite internal.TestSuite) error {
s, ok := d.suites[suite.Path]
if !ok {
return fmt.Errorf("unknown suite %s", suite.Path)
}
return s.MarkAsRunAndRecomputedDependencies(d.maxDepth)
}

View File

@@ -0,0 +1,92 @@
package watch
import (
"go/build"
"regexp"
)
var ginkgoAndGomegaFilter = regexp.MustCompile(`github\.com/onsi/ginkgo|github\.com/onsi/gomega`)
var ginkgoIntegrationTestFilter = regexp.MustCompile(`github\.com/onsi/ginkgo/integration`) //allow us to integration test this thing
type Dependencies struct {
deps map[string]int
}
func NewDependencies(path string, maxDepth int) (Dependencies, error) {
d := Dependencies{
deps: map[string]int{},
}
if maxDepth == 0 {
return d, nil
}
err := d.seedWithDepsForPackageAtPath(path)
if err != nil {
return d, err
}
for depth := 1; depth < maxDepth; depth++ {
n := len(d.deps)
d.addDepsForDepth(depth)
if n == len(d.deps) {
break
}
}
return d, nil
}
func (d Dependencies) Dependencies() map[string]int {
return d.deps
}
func (d Dependencies) seedWithDepsForPackageAtPath(path string) error {
pkg, err := build.ImportDir(path, 0)
if err != nil {
return err
}
d.resolveAndAdd(pkg.Imports, 1)
d.resolveAndAdd(pkg.TestImports, 1)
d.resolveAndAdd(pkg.XTestImports, 1)
delete(d.deps, pkg.Dir)
return nil
}
func (d Dependencies) addDepsForDepth(depth int) {
for dep, depDepth := range d.deps {
if depDepth == depth {
d.addDepsForDep(dep, depth+1)
}
}
}
func (d Dependencies) addDepsForDep(dep string, depth int) {
pkg, err := build.ImportDir(dep, 0)
if err != nil {
println(err.Error())
return
}
d.resolveAndAdd(pkg.Imports, depth)
}
func (d Dependencies) resolveAndAdd(deps []string, depth int) {
for _, dep := range deps {
pkg, err := build.Import(dep, ".", 0)
if err != nil {
continue
}
if !pkg.Goroot && (!ginkgoAndGomegaFilter.Match([]byte(pkg.Dir)) || ginkgoIntegrationTestFilter.Match([]byte(pkg.Dir))) {
d.addDepIfNotPresent(pkg.Dir, depth)
}
}
}
func (d Dependencies) addDepIfNotPresent(dep string, depth int) {
_, ok := d.deps[dep]
if !ok {
d.deps[dep] = depth
}
}

View File

@@ -0,0 +1,108 @@
package watch
import (
"fmt"
"os"
"regexp"
"time"
)
var goTestRegExp = regexp.MustCompile(`_test\.go$`)
type PackageHash struct {
CodeModifiedTime time.Time
TestModifiedTime time.Time
Deleted bool
path string
codeHash string
testHash string
watchRegExp *regexp.Regexp
}
func NewPackageHash(path string, watchRegExp *regexp.Regexp) *PackageHash {
p := &PackageHash{
path: path,
watchRegExp: watchRegExp,
}
p.codeHash, _, p.testHash, _, p.Deleted = p.computeHashes()
return p
}
func (p *PackageHash) CheckForChanges() bool {
codeHash, codeModifiedTime, testHash, testModifiedTime, deleted := p.computeHashes()
if deleted {
if !p.Deleted {
t := time.Now()
p.CodeModifiedTime = t
p.TestModifiedTime = t
}
p.Deleted = true
return true
}
modified := false
p.Deleted = false
if p.codeHash != codeHash {
p.CodeModifiedTime = codeModifiedTime
modified = true
}
if p.testHash != testHash {
p.TestModifiedTime = testModifiedTime
modified = true
}
p.codeHash = codeHash
p.testHash = testHash
return modified
}
func (p *PackageHash) computeHashes() (codeHash string, codeModifiedTime time.Time, testHash string, testModifiedTime time.Time, deleted bool) {
entries, err := os.ReadDir(p.path)
if err != nil {
deleted = true
return
}
for _, entry := range entries {
if entry.IsDir() {
continue
}
info, err := entry.Info()
if err != nil {
continue
}
if goTestRegExp.Match([]byte(info.Name())) {
testHash += p.hashForFileInfo(info)
if info.ModTime().After(testModifiedTime) {
testModifiedTime = info.ModTime()
}
continue
}
if p.watchRegExp.Match([]byte(info.Name())) {
codeHash += p.hashForFileInfo(info)
if info.ModTime().After(codeModifiedTime) {
codeModifiedTime = info.ModTime()
}
}
}
testHash += codeHash
if codeModifiedTime.After(testModifiedTime) {
testModifiedTime = codeModifiedTime
}
return
}
func (p *PackageHash) hashForFileInfo(info os.FileInfo) string {
return fmt.Sprintf("%s_%d_%d", info.Name(), info.Size(), info.ModTime().UnixNano())
}

View File

@@ -0,0 +1,85 @@
package watch
import (
"path/filepath"
"regexp"
"sync"
)
type PackageHashes struct {
PackageHashes map[string]*PackageHash
usedPaths map[string]bool
watchRegExp *regexp.Regexp
lock *sync.Mutex
}
func NewPackageHashes(watchRegExp *regexp.Regexp) *PackageHashes {
return &PackageHashes{
PackageHashes: map[string]*PackageHash{},
usedPaths: nil,
watchRegExp: watchRegExp,
lock: &sync.Mutex{},
}
}
func (p *PackageHashes) CheckForChanges() []string {
p.lock.Lock()
defer p.lock.Unlock()
modified := []string{}
for _, packageHash := range p.PackageHashes {
if packageHash.CheckForChanges() {
modified = append(modified, packageHash.path)
}
}
return modified
}
func (p *PackageHashes) Add(path string) *PackageHash {
p.lock.Lock()
defer p.lock.Unlock()
path, _ = filepath.Abs(path)
_, ok := p.PackageHashes[path]
if !ok {
p.PackageHashes[path] = NewPackageHash(path, p.watchRegExp)
}
if p.usedPaths != nil {
p.usedPaths[path] = true
}
return p.PackageHashes[path]
}
func (p *PackageHashes) Get(path string) *PackageHash {
p.lock.Lock()
defer p.lock.Unlock()
path, _ = filepath.Abs(path)
if p.usedPaths != nil {
p.usedPaths[path] = true
}
return p.PackageHashes[path]
}
func (p *PackageHashes) StartTrackingUsage() {
p.lock.Lock()
defer p.lock.Unlock()
p.usedPaths = map[string]bool{}
}
func (p *PackageHashes) StopTrackingUsageAndPrune() {
p.lock.Lock()
defer p.lock.Unlock()
for path := range p.PackageHashes {
if !p.usedPaths[path] {
delete(p.PackageHashes, path)
}
}
p.usedPaths = nil
}

87
vendor/github.com/onsi/ginkgo/v2/ginkgo/watch/suite.go generated vendored Normal file
View File

@@ -0,0 +1,87 @@
package watch
import (
"fmt"
"math"
"time"
"github.com/onsi/ginkgo/v2/ginkgo/internal"
)
type Suite struct {
Suite internal.TestSuite
RunTime time.Time
Dependencies Dependencies
sharedPackageHashes *PackageHashes
}
func NewSuite(suite internal.TestSuite, maxDepth int, sharedPackageHashes *PackageHashes) (*Suite, error) {
deps, err := NewDependencies(suite.Path, maxDepth)
if err != nil {
return nil, err
}
sharedPackageHashes.Add(suite.Path)
for dep := range deps.Dependencies() {
sharedPackageHashes.Add(dep)
}
return &Suite{
Suite: suite,
Dependencies: deps,
sharedPackageHashes: sharedPackageHashes,
}, nil
}
func (s *Suite) Delta() float64 {
delta := s.delta(s.Suite.Path, true, 0) * 1000
for dep, depth := range s.Dependencies.Dependencies() {
delta += s.delta(dep, false, depth)
}
return delta
}
func (s *Suite) MarkAsRunAndRecomputedDependencies(maxDepth int) error {
s.RunTime = time.Now()
deps, err := NewDependencies(s.Suite.Path, maxDepth)
if err != nil {
return err
}
s.sharedPackageHashes.Add(s.Suite.Path)
for dep := range deps.Dependencies() {
s.sharedPackageHashes.Add(dep)
}
s.Dependencies = deps
return nil
}
func (s *Suite) Description() string {
numDeps := len(s.Dependencies.Dependencies())
pluralizer := "ies"
if numDeps == 1 {
pluralizer = "y"
}
return fmt.Sprintf("%s [%d dependenc%s]", s.Suite.Path, numDeps, pluralizer)
}
func (s *Suite) delta(packagePath string, includeTests bool, depth int) float64 {
return math.Max(float64(s.dt(packagePath, includeTests)), 0) / float64(depth+1)
}
func (s *Suite) dt(packagePath string, includeTests bool) time.Duration {
packageHash := s.sharedPackageHashes.Get(packagePath)
var modifiedTime time.Time
if includeTests {
modifiedTime = packageHash.TestModifiedTime
} else {
modifiedTime = packageHash.CodeModifiedTime
}
return modifiedTime.Sub(s.RunTime)
}

View File

@@ -0,0 +1,190 @@
package watch
import (
"fmt"
"regexp"
"time"
"github.com/onsi/ginkgo/v2/formatter"
"github.com/onsi/ginkgo/v2/ginkgo/command"
"github.com/onsi/ginkgo/v2/ginkgo/internal"
"github.com/onsi/ginkgo/v2/internal/interrupt_handler"
"github.com/onsi/ginkgo/v2/types"
)
func BuildWatchCommand() command.Command {
var suiteConfig = types.NewDefaultSuiteConfig()
var reporterConfig = types.NewDefaultReporterConfig()
var cliConfig = types.NewDefaultCLIConfig()
var goFlagsConfig = types.NewDefaultGoFlagsConfig()
flags, err := types.BuildWatchCommandFlagSet(&suiteConfig, &reporterConfig, &cliConfig, &goFlagsConfig)
if err != nil {
panic(err)
}
interruptHandler := interrupt_handler.NewInterruptHandler(0, nil)
interrupt_handler.SwallowSigQuit()
return command.Command{
Name: "watch",
Flags: flags,
Usage: "ginkgo watch <FLAGS> <PACKAGES> -- <PASS-THROUGHS>",
ShortDoc: "Watch the passed in <PACKAGES> and runs their tests whenever changes occur.",
Documentation: "Any arguments after -- will be passed to the test.",
DocLink: "watching-for-changes",
Command: func(args []string, additionalArgs []string) {
var errors []error
cliConfig, goFlagsConfig, errors = types.VetAndInitializeCLIAndGoConfig(cliConfig, goFlagsConfig)
command.AbortIfErrors("Ginkgo detected configuration issues:", errors)
watcher := &SpecWatcher{
cliConfig: cliConfig,
goFlagsConfig: goFlagsConfig,
suiteConfig: suiteConfig,
reporterConfig: reporterConfig,
flags: flags,
interruptHandler: interruptHandler,
}
watcher.WatchSpecs(args, additionalArgs)
},
}
}
type SpecWatcher struct {
suiteConfig types.SuiteConfig
reporterConfig types.ReporterConfig
cliConfig types.CLIConfig
goFlagsConfig types.GoFlagsConfig
flags types.GinkgoFlagSet
interruptHandler *interrupt_handler.InterruptHandler
}
func (w *SpecWatcher) WatchSpecs(args []string, additionalArgs []string) {
suites := internal.FindSuites(args, w.cliConfig, false).WithoutState(internal.TestSuiteStateSkippedByFilter)
if len(suites) == 0 {
command.AbortWith("Found no test suites")
}
fmt.Printf("Identified %d test %s. Locating dependencies to a depth of %d (this may take a while)...\n", len(suites), internal.PluralizedWord("suite", "suites", len(suites)), w.cliConfig.Depth)
deltaTracker := NewDeltaTracker(w.cliConfig.Depth, regexp.MustCompile(w.cliConfig.WatchRegExp))
delta, errors := deltaTracker.Delta(suites)
fmt.Printf("Watching %d %s:\n", len(delta.NewSuites), internal.PluralizedWord("suite", "suites", len(delta.NewSuites)))
for _, suite := range delta.NewSuites {
fmt.Println(" " + suite.Description())
}
for suite, err := range errors {
fmt.Printf("Failed to watch %s: %s\n", suite.PackageName, err)
}
if len(suites) == 1 {
w.updateSeed()
w.compileAndRun(suites[0], additionalArgs)
}
ticker := time.NewTicker(time.Second)
for {
select {
case <-ticker.C:
suites := internal.FindSuites(args, w.cliConfig, false).WithoutState(internal.TestSuiteStateSkippedByFilter)
delta, _ := deltaTracker.Delta(suites)
coloredStream := formatter.ColorableStdOut
suites = internal.TestSuites{}
if len(delta.NewSuites) > 0 {
fmt.Fprintln(coloredStream, formatter.F("{{green}}Detected %d new %s:{{/}}", len(delta.NewSuites), internal.PluralizedWord("suite", "suites", len(delta.NewSuites))))
for _, suite := range delta.NewSuites {
suites = append(suites, suite.Suite)
fmt.Fprintln(coloredStream, formatter.Fi(1, "%s", suite.Description()))
}
}
modifiedSuites := delta.ModifiedSuites()
if len(modifiedSuites) > 0 {
fmt.Fprintln(coloredStream, formatter.F("{{green}}Detected changes in:{{/}}"))
for _, pkg := range delta.ModifiedPackages {
fmt.Fprintln(coloredStream, formatter.Fi(1, "%s", pkg))
}
fmt.Fprintln(coloredStream, formatter.F("{{green}}Will run %d %s:{{/}}", len(modifiedSuites), internal.PluralizedWord("suite", "suites", len(modifiedSuites))))
for _, suite := range modifiedSuites {
suites = append(suites, suite.Suite)
fmt.Fprintln(coloredStream, formatter.Fi(1, "%s", suite.Description()))
}
fmt.Fprintln(coloredStream, "")
}
if len(suites) == 0 {
break
}
w.updateSeed()
w.computeSuccinctMode(len(suites))
for idx := range suites {
if w.interruptHandler.Status().Interrupted {
return
}
deltaTracker.WillRun(suites[idx])
suites[idx] = w.compileAndRun(suites[idx], additionalArgs)
}
color := "{{green}}"
if suites.CountWithState(internal.TestSuiteStateFailureStates...) > 0 {
color = "{{red}}"
}
fmt.Fprintln(coloredStream, formatter.F(color+"\nDone. Resuming watch...{{/}}"))
messages, err := internal.FinalizeProfilesAndReportsForSuites(suites, w.cliConfig, w.suiteConfig, w.reporterConfig, w.goFlagsConfig)
command.AbortIfError("could not finalize profiles:", err)
for _, message := range messages {
fmt.Println(message)
}
case <-w.interruptHandler.Status().Channel:
return
}
}
}
func (w *SpecWatcher) compileAndRun(suite internal.TestSuite, additionalArgs []string) internal.TestSuite {
suite = internal.CompileSuite(suite, w.goFlagsConfig)
if suite.State.Is(internal.TestSuiteStateFailedToCompile) {
fmt.Println(suite.CompilationError.Error())
return suite
}
if w.interruptHandler.Status().Interrupted {
return suite
}
suite = internal.RunCompiledSuite(suite, w.suiteConfig, w.reporterConfig, w.cliConfig, w.goFlagsConfig, additionalArgs)
internal.Cleanup(w.goFlagsConfig, suite)
return suite
}
func (w *SpecWatcher) computeSuccinctMode(numSuites int) {
if w.reporterConfig.Verbosity().GTE(types.VerbosityLevelVerbose) {
w.reporterConfig.Succinct = false
return
}
if w.flags.WasSet("succinct") {
return
}
if numSuites == 1 {
w.reporterConfig.Succinct = false
}
if numSuites > 1 {
w.reporterConfig.Succinct = true
}
}
func (w *SpecWatcher) updateSeed() {
if !w.flags.WasSet("seed") {
w.suiteConfig.RandomSeed = time.Now().Unix()
}
}