go.mod: bump all deps

Bump all transitive and direct dependencies.

Signed-off-by: Casey Callendrello <c1@caseyc.net>
This commit is contained in:
Casey Callendrello
2023-04-04 16:30:47 +02:00
parent 63235a2531
commit bc5f3defe7
338 changed files with 33887 additions and 2915 deletions

View File

@ -1,3 +1,136 @@
## 2.9.2
### Maintenance
- Bump github.com/go-task/slim-sprig (#1167) [3fcc5bf]
- Bump github.com/onsi/gomega from 1.27.3 to 1.27.4 (#1163) [6143ffe]
## 2.9.1
### Fixes
This release fixes a longstanding issue where `ginkgo -coverpkg=./...` would not work. This is now resolved and fixes [#1161](https://github.com/onsi/ginkgo/issues/1161) and [#995](https://github.com/onsi/ginkgo/issues/995)
- Support -coverpkg=./... [26ca1b5]
- document coverpkg a bit more clearly [fc44c3b]
### Maintenance
- bump various dependencies
- Improve Documentation and fix typo (#1158) [93de676]
## 2.9.0
### Features
- AttachProgressReporter is an experimental feature that allows users to provide arbitrary information when a ProgressReport is requested [28801fe]
- GinkgoT() has been expanded to include several Ginkgo-specific methods [2bd5a3b]
The intent is to enable the development of third-party libraries that integrate deeply with Ginkgo using `GinkgoT()` to access Ginkgo's functionality.
## 2.8.4
### Features
- Add OmitSuiteSetupNodes to JunitReportConfig (#1147) [979fbc2]
- Add a reference to ginkgolinter in docs.index.md (#1143) [8432589]
### Fixes
- rename tools hack to see if it fixes things for downstream users [a8bb39a]
### Maintenance
- Bump golang.org/x/text (#1144) [41b2a8a]
- Bump github.com/onsi/gomega from 1.27.0 to 1.27.1 (#1142) [7c4f583]
## 2.8.3
Released to fix security issue in golang.org/x/net dependency
### Maintenance
- Bump golang.org/x/net from 0.6.0 to 0.7.0 (#1141) [fc1a02e]
- remove tools.go hack from documentation [0718693]
## 2.8.2
Ginkgo now includes a `tools.go` file in the root directory of the `ginkgo` package. This should allow modules that simply `go get github.com/onsi/ginkgo/v2` to also pull in the CLI dependencies. This obviates the need for consumers of Ginkgo to have their own `tools.go` file and makes it simpler to ensure that the version of the `ginkgo` CLI being used matches the version of the library. You can simply run `go run github.com/onsi/ginkgo/v2/ginkgo` to run the version of the cli associated with your package go.mod.
### Maintenance
- Bump github.com/onsi/gomega from 1.26.0 to 1.27.0 (#1139) [5767b0a]
- Fix minor typos (#1138) [e1e9723]
- Fix link in V2 Migration Guide (#1137) [a588f60]
## 2.8.1
### Fixes
- lock around default report output to avoid triggering the race detector when calling By from goroutines [2d5075a]
- don't run ReportEntries through sprintf [febbe38]
### Maintenance
- Bump golang.org/x/tools from 0.5.0 to 0.6.0 (#1135) [11a4860]
- test: update matrix for Go 1.20 (#1130) [4890a62]
- Bump golang.org/x/sys from 0.4.0 to 0.5.0 (#1133) [a774638]
- Bump github.com/onsi/gomega from 1.25.0 to 1.26.0 (#1120) [3f233bd]
- Bump github-pages from 227 to 228 in /docs (#1131) [f9b8649]
- Bump activesupport from 6.0.6 to 6.0.6.1 in /docs (#1127) [6f8c042]
- Update index.md with instructions on how to upgrade Ginkgo [833a75e]
## 2.8.0
### Features
- Introduce GinkgoHelper() to track and exclude helper functions from potential CodeLocations [e19f556]
Modeled after `testing.T.Helper()`. Now, rather than write code like:
```go
func helper(model Model) {
Expect(model).WithOffset(1).To(BeValid())
Expect(model.SerialNumber).WithOffset(1).To(MatchRegexp(/[a-f0-9]*/))
}
```
you can stop tracking offsets (which makes nesting composing helpers nearly impossible) and simply write:
```go
func helper(model Model) {
GinkgoHelper()
Expect(model).To(BeValid())
Expect(model.SerialNumber).To(MatchRegexp(/[a-f0-9]*/))
}
```
- Introduce GinkgoLabelFilter() and Label().MatchesLabelFilter() to make it possible to programmatically match filters (fixes #1119) [2f6597c]
You can now write code like this:
```go
BeforeSuite(func() {
if Label("slow").MatchesLabelFilter(GinkgoLabelFilter()) {
// do slow setup
}
if Label("fast").MatchesLabelFilter(GinkgoLabelFilter()) {
// do fast setup
}
})
```
to programmatically check whether a given set of labels will match the configured `--label-filter`.
### Maintenance
- Bump webrick from 1.7.0 to 1.8.1 in /docs (#1125) [ea4966e]
- cdeql: add ruby language (#1124) [9dd275b]
- dependabot: add bundler package-ecosystem for docs (#1123) [14e7bdd]
## 2.7.1
### Fixes
- Bring back SuiteConfig.EmitSpecProgress to avoid compilation issue for consumers that set it manually [d2a1cb0]
### Maintenance
- Bump github.com/onsi/gomega from 1.24.2 to 1.25.0 (#1118) [cafece6]
- Bump golang.org/x/tools from 0.4.0 to 0.5.0 (#1111) [eda66c2]
- Bump golang.org/x/sys from 0.3.0 to 0.4.0 (#1112) [ac5ccaa]
- Bump github.com/onsi/gomega from 1.24.1 to 1.24.2 (#1097) [eee6480]
## 2.7.0
### Features
@ -89,7 +222,7 @@ to build tooling on top of as it has stronger guarantees to be stable from versi
### Fixes
- correcting some typos (#1064) [1403d3c]
- fix flaky internal_integration interupt specs [2105ba3]
- fix flaky internal_integration interrupt specs [2105ba3]
- Correct busted link in README [be6b5b9]
### Maintenance

View File

@ -92,11 +92,11 @@ type GinkgoWriterInterface interface {
}
/*
SpecContext is the context object passed into nodes that are subject to a timeout or need to be notified of an interrupt. It implements the standard context.Context interface but also contains additional helpers to provide an extensibility point for Ginkgo. (As an example, Gomega's Eventually can use the methods defined on SpecContext to provide deeper integratoin with Ginkgo).
SpecContext is the context object passed into nodes that are subject to a timeout or need to be notified of an interrupt. It implements the standard context.Context interface but also contains additional helpers to provide an extensibility point for Ginkgo. (As an example, Gomega's Eventually can use the methods defined on SpecContext to provide deeper integration with Ginkgo).
You can do anything with SpecContext that you do with a typical context.Context including wrapping it with any of the context.With* methods.
Ginkgo will cancel the SpecContext when a node is interrupted (e.g. by the user sending an interupt signal) or when a node has exceeded it's allowed run-time. Note, however, that even in cases where a node has a deadline, SpecContext will not return a deadline via .Deadline(). This is because Ginkgo does not use a WithDeadline() context to model node deadlines as Ginkgo needs control over the precise timing of the context cancellation to ensure it can provide an accurate progress report at the moment of cancellation.
Ginkgo will cancel the SpecContext when a node is interrupted (e.g. by the user sending an interrupt signal) or when a node has exceeded its allowed run-time. Note, however, that even in cases where a node has a deadline, SpecContext will not return a deadline via .Deadline(). This is because Ginkgo does not use a WithDeadline() context to model node deadlines as Ginkgo needs control over the precise timing of the context cancellation to ensure it can provide an accurate progress report at the moment of cancellation.
*/
type SpecContext = internal.SpecContext
@ -163,6 +163,29 @@ func GinkgoParallelProcess() int {
return suiteConfig.ParallelProcess
}
/*
GinkgoHelper marks the function it's called in as a test helper. When a failure occurs inside a helper function, Ginkgo will skip the helper when analyzing the stack trace to identify where the failure occurred.
This is an alternative, simpler, mechanism to passing in a skip offset when calling Fail or using Gomega.
*/
func GinkgoHelper() {
types.MarkAsHelper(1)
}
/*
GinkgoLabelFilter() returns the label filter configured for this suite via `--label-filter`.
You can use this to manually check if a set of labels would satisfy the filter via:
if (Label("cat", "dog").MatchesLabelFilter(GinkgoLabelFilter())) {
//...
}
*/
func GinkgoLabelFilter() string {
suiteConfig, _ := GinkgoConfiguration()
return suiteConfig.LabelFilter
}
/*
PauseOutputInterception() pauses Ginkgo's output interception. This is only relevant
when running in parallel and output to stdout/stderr is being intercepted. You generally
@ -517,7 +540,7 @@ and will simply log the passed in text to the GinkgoWriter. If By is handed a f
By will also generate and attach a ReportEntry to the spec. This will ensure that By annotations appear in Ginkgo's machine-readable reports.
Note that By does not generate a new Ginkgo node - rather it is simply synctactic sugar around GinkgoWriter and AddReportEntry
Note that By does not generate a new Ginkgo node - rather it is simply syntactic sugar around GinkgoWriter and AddReportEntry
You can learn more about By here: https://onsi.github.io/ginkgo/#documenting-complex-specs-by
*/
func By(text string, callback ...func()) {
@ -720,7 +743,7 @@ For example:
os.SetEnv("FOO", "BAR")
})
will register a cleanup handler that will set the environment variable "FOO" to it's current value (obtained by os.GetEnv("FOO")) after the spec runs and then sets the environment variable "FOO" to "BAR" for the current spec.
will register a cleanup handler that will set the environment variable "FOO" to its current value (obtained by os.GetEnv("FOO")) after the spec runs and then sets the environment variable "FOO" to "BAR" for the current spec.
Similarly:
@ -748,3 +771,24 @@ func DeferCleanup(args ...interface{}) {
}
pushNode(internal.NewCleanupNode(deprecationTracker, fail, args...))
}
/*
AttachProgressReporter allows you to register a function that will be called whenever Ginkgo generates a Progress Report. The contents returned by the function will be included in the report.
**This is an experimental feature and the public-facing interface may change in a future minor version of Ginkgo**
Progress Reports are generated:
- whenever the user explicitly requests one (via `SIGINFO` or `SIGUSR1`)
- on nodes decorated with PollProgressAfter
- on suites run with --poll-progress-after
- whenever a test times out
Ginkgo uses Progress Reports to convey the current state of the test suite, including any running goroutines. By attaching a progress reporter you are able to supplement these reports with additional information.
# AttachProgressReporter returns a function that can be called to detach the progress reporter
You can learn more about AttachProgressReporter here: https://onsi.github.io/ginkgo/#attaching-additional-information-to-progress-reports
*/
func AttachProgressReporter(reporter func() string) func() {
return global.Suite.AttachProgressReporter(reporter)
}

View File

@ -120,7 +120,10 @@ func (f Formatter) Fi(indentation uint, format string, args ...interface{}) stri
}
func (f Formatter) Fiw(indentation uint, maxWidth uint, format string, args ...interface{}) string {
out := fmt.Sprintf(f.style(format), args...)
out := f.style(format)
if len(args) > 0 {
out = fmt.Sprintf(out, args...)
}
if indentation == 0 && maxWidth == 0 {
return out

View File

@ -0,0 +1,63 @@
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")
}
internal.VerifyCLIAndFrameworkVersion(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,133 @@
package generators
import (
"bytes"
"encoding/json"
"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"},
{Name: "template-data", KeyPath: "CustomTemplateData",
UsageArgument: "template-data-file",
Usage: "If specified, generate will use the contents of the file passed as data to be rendered in 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
CustomData map[string]any
}
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)
if conf.CustomTemplateData != "" {
var tplCustomDataMap map[string]any
tplCustomData, err := os.ReadFile(conf.CustomTemplateData)
command.AbortIfError("Failed to read custom boostrap data file:", err)
if !json.Valid([]byte(tplCustomData)) {
command.AbortWith("Invalid JSON object in custom data file.")
}
//create map from the custom template data
json.Unmarshal(tplCustomData, &tplCustomDataMap)
data.CustomData = tplCustomDataMap
}
} else if conf.Agouti {
templateText = agoutiBootstrapText
} else {
templateText = bootstrapText
}
//Setting the option to explicitly fail if template is rendered trying to access missing key
bootstrapTemplate, err := template.New("bootstrap").Funcs(sprig.TxtFuncMap()).Option("missingkey=error").Parse(templateText)
command.AbortIfError("Failed to parse bootstrap template:", err)
buf := &bytes.Buffer{}
//Being explicit about failing sooner during template rendering
//when accessing custom data rather than during the go fmt command
err = bootstrapTemplate.Execute(buf, data)
command.AbortIfError("Failed to render bootstrap template:", err)
buf.WriteTo(f)
internal.GoFmt(targetFile)
}

View File

@ -0,0 +1,259 @@
package generators
import (
"bytes"
"encoding/json"
"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"},
{Name: "template-data", KeyPath: "CustomTemplateData",
UsageArgument: "template-data-file",
Usage: "If specified, generate will use the contents of the file passed as data to be rendered in 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
CustomData map[string]any
}
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)
if conf.CustomTemplateData != "" {
var tplCustomDataMap map[string]any
tplCustomData, err := os.ReadFile(conf.CustomTemplateData)
command.AbortIfError("Failed to read custom template data file:", err)
if !json.Valid([]byte(tplCustomData)) {
command.AbortWith("Invalid JSON object in custom data file.")
}
//create map from the custom template data
json.Unmarshal(tplCustomData, &tplCustomDataMap)
data.CustomData = tplCustomDataMap
}
} else if conf.Agouti {
templateText = agoutiSpecText
} else {
templateText = specText
}
//Setting the option to explicitly fail if template is rendered trying to access missing key
specTemplate, err := template.New("spec").Funcs(sprig.TxtFuncMap()).Option("missingkey=error").Parse(templateText)
command.AbortIfError("Failed to read parse test template:", err)
//Being explicit about failing sooner during template rendering
//when accessing custom data rather than during the go fmt command
err = specTemplate.Execute(f, data)
command.AbortIfError("Failed to render bootstrap template:", err)
internal.GoFmt(targetFile)
}
func formatSubject(name string) string {
name = strings.ReplaceAll(name, "-", "_")
name = strings.ReplaceAll(name, " ", "_")
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.ReplaceAll(workingDir, modRoot, "")
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,64 @@
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
CustomTemplateData string
}
func getPackageAndFormattedName() (string, string, string) {
path, err := os.Getwd()
command.AbortIfError("Could not get current working directory:", err)
dirName := strings.ReplaceAll(filepath.Base(path), "-", "_")
dirName = strings.ReplaceAll(dirName, " ", "_")
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.ReplaceAll(name, "-", " ")
name = strings.ReplaceAll(name, "_", " ")
name = strings.Title(name)
name = strings.ReplaceAll(name, " ", "")
return name
}
func determinePackageName(name string, internal bool) string {
if internal {
return name
}
return name + "_test"
}

View File

@ -0,0 +1,161 @@
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
}
ginkgoInvocationPath, _ := os.Getwd()
ginkgoInvocationPath, _ = filepath.Abs(ginkgoInvocationPath)
packagePath := suite.AbsPath()
pathToInvocationPath, err := filepath.Rel(packagePath, ginkgoInvocationPath)
if err != nil {
suite.State = TestSuiteStateFailedToCompile
suite.CompilationError = fmt.Errorf("Failed to get relative path from package to the current working directory:\n%s", err.Error())
return suite
}
args, err := types.GenerateGoTestCompileArgs(goFlagsConfig, path, "./", pathToInvocationPath)
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
}

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

@ -0,0 +1,355 @@
package internal
import (
"bytes"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"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 {
// As we run the go test from the suite directory, make sure the cover profile is absolute
// and placed into the expected output directory when one is configured.
if goFlagsConfig.Cover && !filepath.IsAbs(goFlagsConfig.CoverProfile) {
goFlagsConfig.CoverProfile = AbsPathForGeneratedAsset(goFlagsConfig.CoverProfile, suite, cliConfig, 0)
}
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.ReplaceAll(command, "(ginkgo-suite-passed)", passed)
command = strings.ReplaceAll(command, "(ginkgo-suite-name)", suite.PackageName)
// 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,54 @@
package internal
import (
"fmt"
"os/exec"
"regexp"
"strings"
"github.com/onsi/ginkgo/v2/formatter"
"github.com/onsi/ginkgo/v2/types"
)
var versiorRe = regexp.MustCompile(`v(\d+\.\d+\.\d+)`)
func VerifyCLIAndFrameworkVersion(suites TestSuites) {
cliVersion := types.VERSION
mismatches := map[string][]string{}
for _, suite := range suites {
cmd := exec.Command("go", "list", "-m", "github.com/onsi/ginkgo/v2")
cmd.Dir = suite.Path
output, err := cmd.CombinedOutput()
if err != nil {
continue
}
components := strings.Split(string(output), " ")
if len(components) != 2 {
continue
}
matches := versiorRe.FindStringSubmatch(components[1])
if matches == nil || len(matches) != 2 {
continue
}
libraryVersion := matches[1]
if cliVersion != libraryVersion {
mismatches[libraryVersion] = append(mismatches[libraryVersion], suite.PackageName)
}
}
if len(mismatches) == 0 {
return
}
fmt.Println(formatter.F("{{red}}{{bold}}Ginkgo detected a version mismatch between the Ginkgo CLI and the version of Ginkgo imported by your packages:{{/}}"))
fmt.Println(formatter.Fi(1, "Ginkgo CLI Version:"))
fmt.Println(formatter.Fi(2, "{{bold}}%s{{/}}", cliVersion))
fmt.Println(formatter.Fi(1, "Mismatched package versions found:"))
for version, packages := range mismatches {
fmt.Println(formatter.Fi(2, "{{bold}}%s{{/}} used by %s", version, strings.Join(packages, ", ")))
}
fmt.Println("")
fmt.Println(formatter.Fiw(1, formatter.COLS, "{{gray}}Ginkgo will continue to attempt to run but you may see errors (including flag parsing errors) and should either update your go.mod or your version of the Ginkgo CLI to match.\n\nTo install the matching version of the CLI run\n {{bold}}go install github.com/onsi/ginkgo/v2/ginkgo{{/}}{{gray}}\nfrom a path that contains a go.mod file. Alternatively you can use\n {{bold}}go run github.com/onsi/ginkgo/v2/ginkgo{{/}}{{gray}}\nfrom a path that contains a go.mod file to invoke the matching version of the Ginkgo CLI.\n\nIf you are attempting to test multiple packages that each have a different version of the Ginkgo library with a single Ginkgo CLI that is currently unsupported.\n{{/}}"))
}

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,302 @@
package outline
import (
"github.com/onsi/ginkgo/v2/types"
"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"`
Labels []string `json:"labels"`
}
// 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)
n.Labels = labelFromCallExpr(ce)
n.Pending = pendingFromCallExpr(ce)
return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName
case "FIt", "FSpecify", "FEntry":
n.Spec = true
n.Focused = true
n.Text = textOrAltFromCallExpr(ce, undefinedTextAlt)
n.Labels = labelFromCallExpr(ce)
return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName
case "PIt", "PSpecify", "XIt", "XSpecify", "PEntry", "XEntry":
n.Spec = true
n.Pending = true
n.Text = textOrAltFromCallExpr(ce, undefinedTextAlt)
n.Labels = labelFromCallExpr(ce)
return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName
case "Context", "Describe", "When", "DescribeTable":
n.Text = textOrAltFromCallExpr(ce, undefinedTextAlt)
n.Labels = labelFromCallExpr(ce)
n.Pending = pendingFromCallExpr(ce)
return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName
case "FContext", "FDescribe", "FWhen", "FDescribeTable":
n.Focused = true
n.Text = textOrAltFromCallExpr(ce, undefinedTextAlt)
n.Labels = labelFromCallExpr(ce)
return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName
case "PContext", "PDescribe", "PWhen", "XContext", "XDescribe", "XWhen", "PDescribeTable", "XDescribeTable":
n.Pending = true
n.Text = textOrAltFromCallExpr(ce, undefinedTextAlt)
n.Labels = labelFromCallExpr(ce)
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
}
}
func labelFromCallExpr(ce *ast.CallExpr) []string {
labels := []string{}
if len(ce.Args) < 2 {
return labels
}
for _, arg := range ce.Args[1:] {
switch expr := arg.(type) {
case *ast.CallExpr:
id, ok := expr.Fun.(*ast.Ident)
if !ok {
// to skip over cases where the expr.Fun. is actually *ast.SelectorExpr
continue
}
if id.Name == "Label" {
ls := extractLabels(expr)
for _, label := range ls {
labels = append(labels, label)
}
}
}
}
return labels
}
func extractLabels(expr *ast.CallExpr) []string {
out := []string{}
for _, arg := range expr.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
}
func pendingFromCallExpr(ce *ast.CallExpr) bool {
pending := false
if len(ce.Args) < 2 {
return pending
}
for _, arg := range ce.Args[1:] {
switch expr := arg.(type) {
case *ast.CallExpr:
id, ok := expr.Fun.(*ast.Ident)
if !ok {
// to skip over cases where the expr.Fun. is actually *ast.SelectorExpr
continue
}
if id.Name == "Pending" {
pending = true
}
case *ast.Ident:
if expr.Name == "Pending" {
pending = true
}
}
}
return pending
}

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 strings.HasPrefix(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,110 @@
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,Labels\n")
currentIndent := 0
pre := func(n *ginkgoNode) {
b.WriteString(fmt.Sprintf("%*s", currentIndent, ""))
var labels string
if len(n.Labels) == 1 {
labels = n.Labels[0]
} else {
labels = strings.Join(n.Labels, ", ")
}
//enclosing labels in a double quoted comma separate listed so that when inmported into a CSV app the Labels column has comma separate strings
b.WriteString(fmt.Sprintf("%s,%s,%d,%d,%t,%t,%t,\"%s\"\n", n.Name, n.Text, n.Start, n.End, n.Spec, n.Focused, n.Pending, labels))
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,232 @@
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(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)
internal.VerifyCLIAndFrameworkVersion(suites)
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,192 @@
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(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)
internal.VerifyCLIAndFrameworkVersion(suites)
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()
}
}

View File

@ -0,0 +1,8 @@
//go:build ginkgoclidependencies
// +build ginkgoclidependencies
package ginkgo
import (
_ "github.com/onsi/ginkgo/v2/ginkgo"
)

View File

@ -1,26 +1,42 @@
package ginkgo
import "github.com/onsi/ginkgo/v2/internal/testingtproxy"
import (
"github.com/onsi/ginkgo/v2/internal/testingtproxy"
)
/*
GinkgoT() implements an interface analogous to *testing.T and can be used with
third-party libraries that accept *testing.T through an interface.
GinkgoT() implements an interface that allows third party libraries to integrate with and build on top of Ginkgo.
GinkgoT() is analogous to *testing.T and implements the majority of *testing.T's methods. It can be typically be used a a drop-in replacement with third-party libraries that accept *testing.T through an interface.
GinkgoT() takes an optional offset argument that can be used to get the
correct line number associated with the failure.
correct line number associated with the failure - though you do not need to use this if you call GinkgoHelper() or GinkgoT().Helper() appropriately
You can learn more here: https://onsi.github.io/ginkgo/#using-third-party-libraries
*/
func GinkgoT(optionalOffset ...int) GinkgoTInterface {
func GinkgoT(optionalOffset ...int) FullGinkgoTInterface {
offset := 3
if len(optionalOffset) > 0 {
offset = optionalOffset[0]
}
return testingtproxy.New(GinkgoWriter, Fail, Skip, DeferCleanup, CurrentSpecReport, offset)
return testingtproxy.New(
GinkgoWriter,
Fail,
Skip,
DeferCleanup,
CurrentSpecReport,
AddReportEntry,
GinkgoRecover,
AttachProgressReporter,
suiteConfig.RandomSeed,
suiteConfig.ParallelProcess,
suiteConfig.ParallelTotal,
reporterConfig.NoColor,
offset)
}
/*
The interface returned by GinkgoT(). This covers most of the methods in the testing package's T.
The portion of the interface returned by GinkgoT() that maps onto methods in the testing package's T.
*/
type GinkgoTInterface interface {
Cleanup(func())
@ -43,3 +59,33 @@ type GinkgoTInterface interface {
Skipped() bool
TempDir() string
}
/*
Additional methods returned by GinkgoT() that provide deeper integration points into Ginkgo
*/
type FullGinkgoTInterface interface {
GinkgoTInterface
AddReportEntryVisibilityAlways(name string, args ...any)
AddReportEntryVisibilityFailureOrVerbose(name string, args ...any)
AddReportEntryVisibilityNever(name string, args ...any)
//Prints to the GinkgoWriter
Print(a ...interface{})
Printf(format string, a ...interface{})
Println(a ...interface{})
//Provides access to Ginkgo's color formatting, correctly configured to match the color settings specified in the invocation of ginkgo
F(format string, args ...any) string
Fi(indentation uint, format string, args ...any) string
Fiw(indentation uint, maxWidth uint, format string, args ...any) string
GinkgoRecover()
DeferCleanup(args ...any)
RandomSeed() int64
ParallelProcess() int
ParallelTotal() int
AttachProgressReporter(func() string) func()
}

View File

@ -93,6 +93,10 @@ type NodeTimeout time.Duration
type SpecTimeout time.Duration
type GracePeriod time.Duration
func (l Labels) MatchesLabelFilter(query string) bool {
return types.MustParseLabelFilter(query)(l)
}
func UnionOfLabels(labels ...Labels) Labels {
out := Labels{}
seen := map[string]bool{}

View File

@ -0,0 +1,79 @@
package internal
import (
"context"
"sort"
"strings"
"sync"
"github.com/onsi/ginkgo/v2/types"
)
type ProgressReporterManager struct {
lock *sync.Mutex
progressReporters map[int]func() string
prCounter int
}
func NewProgressReporterManager() *ProgressReporterManager {
return &ProgressReporterManager{
progressReporters: map[int]func() string{},
lock: &sync.Mutex{},
}
}
func (prm *ProgressReporterManager) AttachProgressReporter(reporter func() string) func() {
prm.lock.Lock()
defer prm.lock.Unlock()
prm.prCounter += 1
prCounter := prm.prCounter
prm.progressReporters[prCounter] = reporter
return func() {
prm.lock.Lock()
defer prm.lock.Unlock()
delete(prm.progressReporters, prCounter)
}
}
func (prm *ProgressReporterManager) QueryProgressReporters(ctx context.Context, failer *Failer) []string {
prm.lock.Lock()
keys := []int{}
for key := range prm.progressReporters {
keys = append(keys, key)
}
sort.Ints(keys)
reporters := []func() string{}
for _, key := range keys {
reporters = append(reporters, prm.progressReporters[key])
}
prm.lock.Unlock()
if len(reporters) == 0 {
return nil
}
out := []string{}
for _, reporter := range reporters {
reportC := make(chan string, 1)
go func() {
defer func() {
e := recover()
if e != nil {
failer.Panic(types.NewCodeLocationWithStackTrace(1), e)
reportC <- "failed to query attached progress reporter"
}
}()
reportC <- reporter()
}()
var report string
select {
case report = <-reportC:
case <-ctx.Done():
return out
}
if strings.TrimSpace(report) != "" {
out = append(out, report)
}
}
return out
}

View File

@ -2,8 +2,6 @@ package internal
import (
"context"
"sort"
"sync"
"github.com/onsi/ginkgo/v2/types"
)
@ -17,11 +15,9 @@ type SpecContext interface {
type specContext struct {
context.Context
*ProgressReporterManager
cancel context.CancelFunc
lock *sync.Mutex
progressReporters map[int]func() string
prCounter int
cancel context.CancelFunc
suite *Suite
}
@ -36,11 +32,9 @@ This is because Ginkgo needs finer control over when the context is canceled. S
func NewSpecContext(suite *Suite) *specContext {
ctx, cancel := context.WithCancel(context.Background())
sc := &specContext{
cancel: cancel,
suite: suite,
lock: &sync.Mutex{},
prCounter: 0,
progressReporters: map[int]func() string{},
cancel: cancel,
suite: suite,
ProgressReporterManager: NewProgressReporterManager(),
}
ctx = context.WithValue(ctx, "GINKGO_SPEC_CONTEXT", sc) //yes, yes, the go docs say don't use a string for a key... but we'd rather avoid a circular dependency between Gomega and Ginkgo
sc.Context = ctx //thank goodness for garbage collectors that can handle circular dependencies
@ -51,40 +45,3 @@ func NewSpecContext(suite *Suite) *specContext {
func (sc *specContext) SpecReport() types.SpecReport {
return sc.suite.CurrentSpecReport()
}
func (sc *specContext) AttachProgressReporter(reporter func() string) func() {
sc.lock.Lock()
defer sc.lock.Unlock()
sc.prCounter += 1
prCounter := sc.prCounter
sc.progressReporters[prCounter] = reporter
return func() {
sc.lock.Lock()
defer sc.lock.Unlock()
delete(sc.progressReporters, prCounter)
}
}
func (sc *specContext) QueryProgressReporters() []string {
sc.lock.Lock()
keys := []int{}
for key := range sc.progressReporters {
keys = append(keys, key)
}
sort.Ints(keys)
reporters := []func() string{}
for _, key := range keys {
reporters = append(reporters, sc.progressReporters[key])
}
sc.lock.Unlock()
if len(reporters) == 0 {
return nil
}
out := []string{}
for _, reporter := range reporters {
out = append(out, reporter())
}
return out
}

View File

@ -9,6 +9,7 @@ import (
"github.com/onsi/ginkgo/v2/internal/parallel_support"
"github.com/onsi/ginkgo/v2/reporters"
"github.com/onsi/ginkgo/v2/types"
"golang.org/x/net/context"
)
type Phase uint
@ -19,10 +20,14 @@ const (
PhaseRun
)
var PROGRESS_REPORTER_DEADLING = 5 * time.Second
type Suite struct {
tree *TreeNode
topLevelContainers Nodes
*ProgressReporterManager
phase Phase
suiteNodes Nodes
@ -64,8 +69,9 @@ type Suite struct {
func NewSuite() *Suite {
return &Suite{
tree: &TreeNode{},
phase: PhaseBuildTopLevel,
tree: &TreeNode{},
phase: PhaseBuildTopLevel,
ProgressReporterManager: NewProgressReporterManager(),
selectiveLock: &sync.Mutex{},
}
@ -338,10 +344,13 @@ func (suite *Suite) generateProgressReport(fullReport bool) types.ProgressReport
suite.selectiveLock.Lock()
defer suite.selectiveLock.Unlock()
deadline, cancel := context.WithTimeout(context.Background(), PROGRESS_REPORTER_DEADLING)
defer cancel()
var additionalReports []string
if suite.currentSpecContext != nil {
additionalReports = suite.currentSpecContext.QueryProgressReporters()
additionalReports = append(additionalReports, suite.currentSpecContext.QueryProgressReporters(deadline, suite.failer)...)
}
additionalReports = append(additionalReports, suite.QueryProgressReporters(deadline, suite.failer)...)
gwOutput := suite.currentSpecReport.CapturedGinkgoWriterOutput + string(suite.writer.Bytes())
pr, err := NewProgressReport(suite.isRunningInParallel(), suite.currentSpecReport, suite.currentNode, suite.currentNodeStartTime, suite.currentByStep, gwOutput, timelineLocation, additionalReports, suite.config.SourceRoots, fullReport)

View File

@ -5,35 +5,62 @@ import (
"io"
"os"
"github.com/onsi/ginkgo/v2/formatter"
"github.com/onsi/ginkgo/v2/internal"
"github.com/onsi/ginkgo/v2/types"
)
type failFunc func(message string, callerSkip ...int)
type skipFunc func(message string, callerSkip ...int)
type cleanupFunc func(args ...interface{})
type cleanupFunc func(args ...any)
type reportFunc func() types.SpecReport
type addReportEntryFunc func(names string, args ...any)
type ginkgoWriterInterface interface {
io.Writer
func New(writer io.Writer, fail failFunc, skip skipFunc, cleanup cleanupFunc, report reportFunc, offset int) *ginkgoTestingTProxy {
Print(a ...interface{})
Printf(format string, a ...interface{})
Println(a ...interface{})
}
type ginkgoRecoverFunc func()
type attachProgressReporterFunc func(func() string) func()
func New(writer ginkgoWriterInterface, fail failFunc, skip skipFunc, cleanup cleanupFunc, report reportFunc, addReportEntry addReportEntryFunc, ginkgoRecover ginkgoRecoverFunc, attachProgressReporter attachProgressReporterFunc, randomSeed int64, parallelProcess int, parallelTotal int, noColor bool, offset int) *ginkgoTestingTProxy {
return &ginkgoTestingTProxy{
fail: fail,
offset: offset,
writer: writer,
skip: skip,
cleanup: cleanup,
report: report,
fail: fail,
offset: offset,
writer: writer,
skip: skip,
cleanup: cleanup,
report: report,
addReportEntry: addReportEntry,
ginkgoRecover: ginkgoRecover,
attachProgressReporter: attachProgressReporter,
randomSeed: randomSeed,
parallelProcess: parallelProcess,
parallelTotal: parallelTotal,
f: formatter.NewWithNoColorBool(noColor),
}
}
type ginkgoTestingTProxy struct {
fail failFunc
skip skipFunc
cleanup cleanupFunc
report reportFunc
offset int
writer io.Writer
fail failFunc
skip skipFunc
cleanup cleanupFunc
report reportFunc
offset int
writer ginkgoWriterInterface
addReportEntry addReportEntryFunc
ginkgoRecover ginkgoRecoverFunc
attachProgressReporter attachProgressReporterFunc
randomSeed int64
parallelProcess int
parallelTotal int
f formatter.Formatter
}
// basic testing.T support
func (t *ginkgoTestingTProxy) Cleanup(f func()) {
t.cleanup(f, internal.Offset(1))
}
@ -81,7 +108,7 @@ func (t *ginkgoTestingTProxy) Fatalf(format string, args ...interface{}) {
}
func (t *ginkgoTestingTProxy) Helper() {
// No-op
types.MarkAsHelper(1)
}
func (t *ginkgoTestingTProxy) Log(args ...interface{}) {
@ -126,3 +153,54 @@ func (t *ginkgoTestingTProxy) TempDir() string {
return tmpDir
}
// FullGinkgoTInterface
func (t *ginkgoTestingTProxy) AddReportEntryVisibilityAlways(name string, args ...any) {
finalArgs := []any{internal.Offset(1), types.ReportEntryVisibilityAlways}
t.addReportEntry(name, append(finalArgs, args...)...)
}
func (t *ginkgoTestingTProxy) AddReportEntryVisibilityFailureOrVerbose(name string, args ...any) {
finalArgs := []any{internal.Offset(1), types.ReportEntryVisibilityFailureOrVerbose}
t.addReportEntry(name, append(finalArgs, args...)...)
}
func (t *ginkgoTestingTProxy) AddReportEntryVisibilityNever(name string, args ...any) {
finalArgs := []any{internal.Offset(1), types.ReportEntryVisibilityNever}
t.addReportEntry(name, append(finalArgs, args...)...)
}
func (t *ginkgoTestingTProxy) Print(a ...any) {
t.writer.Print(a...)
}
func (t *ginkgoTestingTProxy) Printf(format string, a ...any) {
t.writer.Printf(format, a...)
}
func (t *ginkgoTestingTProxy) Println(a ...any) {
t.writer.Println(a...)
}
func (t *ginkgoTestingTProxy) F(format string, args ...any) string {
return t.f.F(format, args...)
}
func (t *ginkgoTestingTProxy) Fi(indentation uint, format string, args ...any) string {
return t.f.Fi(indentation, format, args...)
}
func (t *ginkgoTestingTProxy) Fiw(indentation uint, maxWidth uint, format string, args ...any) string {
return t.f.Fiw(indentation, maxWidth, format, args...)
}
func (t *ginkgoTestingTProxy) GinkgoRecover() {
t.ginkgoRecover()
}
func (t *ginkgoTestingTProxy) DeferCleanup(args ...any) {
finalArgs := []any{internal.Offset(1)}
t.cleanup(append(finalArgs, args...)...)
}
func (t *ginkgoTestingTProxy) RandomSeed() int64 {
return t.randomSeed
}
func (t *ginkgoTestingTProxy) ParallelProcess() int {
return t.parallelProcess
}
func (t *ginkgoTestingTProxy) ParallelTotal() int {
return t.parallelTotal
}
func (t *ginkgoTestingTProxy) AttachProgressReporter(f func() string) func() {
return t.attachProgressReporter(f)
}

View File

@ -12,6 +12,7 @@ import (
"io"
"runtime"
"strings"
"sync"
"time"
"github.com/onsi/ginkgo/v2/formatter"
@ -23,7 +24,7 @@ type DefaultReporter struct {
writer io.Writer
// managing the emission stream
lastChar string
lastCharWasNewline bool
lastEmissionWasDelimiter bool
// rendering
@ -32,6 +33,7 @@ type DefaultReporter struct {
formatter formatter.Formatter
runningInParallel bool
lock *sync.Mutex
}
func NewDefaultReporterUnderTest(conf types.ReporterConfig, writer io.Writer) *DefaultReporter {
@ -46,12 +48,13 @@ func NewDefaultReporter(conf types.ReporterConfig, writer io.Writer) *DefaultRep
conf: conf,
writer: writer,
lastChar: "\n",
lastCharWasNewline: true,
lastEmissionWasDelimiter: false,
specDenoter: "•",
retryDenoter: "↺",
formatter: formatter.NewWithNoColorBool(conf.NoColor),
lock: &sync.Mutex{},
}
if runtime.GOOS == "windows" {
reporter.specDenoter = "+"
@ -528,7 +531,7 @@ func (r *DefaultReporter) EmitReportEntry(entry types.ReportEntry) {
}
func (r *DefaultReporter) emitReportEntry(indent uint, entry types.ReportEntry) {
r.emitBlock(r.fi(indent, "{{bold}}"+entry.Name+"{{gray}} - %s @ %s{{/}}", entry.Location, entry.Time.Format(types.GINKGO_TIME_FORMAT)))
r.emitBlock(r.fi(indent, "{{bold}}"+entry.Name+"{{gray}} "+fmt.Sprintf("- %s @ %s{{/}}", entry.Location, entry.Time.Format(types.GINKGO_TIME_FORMAT))))
if representation := entry.StringRepresentation(); representation != "" {
r.emitBlock(r.fi(indent+1, representation))
}
@ -619,31 +622,37 @@ func (r *DefaultReporter) emitSource(indent uint, fc types.FunctionCall) {
/* Emitting to the writer */
func (r *DefaultReporter) emit(s string) {
if len(s) > 0 {
r.lastChar = s[len(s)-1:]
r.lastEmissionWasDelimiter = false
r.writer.Write([]byte(s))
}
r._emit(s, false, false)
}
func (r *DefaultReporter) emitBlock(s string) {
if len(s) > 0 {
if r.lastChar != "\n" {
r.emit("\n")
}
r.emit(s)
if r.lastChar != "\n" {
r.emit("\n")
}
}
r._emit(s, true, false)
}
func (r *DefaultReporter) emitDelimiter(indent uint) {
if r.lastEmissionWasDelimiter {
r._emit(r.fi(indent, "{{gray}}%s{{/}}", strings.Repeat("-", 30)), true, true)
}
// a bit ugly - but we're trying to minimize locking on this hot codepath
func (r *DefaultReporter) _emit(s string, block bool, isDelimiter bool) {
if len(s) == 0 {
return
}
r.emitBlock(r.fi(indent, "{{gray}}%s{{/}}", strings.Repeat("-", 30)))
r.lastEmissionWasDelimiter = true
r.lock.Lock()
defer r.lock.Unlock()
if isDelimiter && r.lastEmissionWasDelimiter {
return
}
if block && !r.lastCharWasNewline {
r.writer.Write([]byte("\n"))
}
r.lastCharWasNewline = (s[len(s)-1:] == "\n")
r.writer.Write([]byte(s))
if block && !r.lastCharWasNewline {
r.writer.Write([]byte("\n"))
r.lastCharWasNewline = true
}
r.lastEmissionWasDelimiter = isDelimiter
}
/* Rendering text */

View File

@ -36,6 +36,9 @@ type JunitReportConfig struct {
// Enable OmitLeafNodeType to prevent the spec leaf node type from appearing in the spec name
OmitLeafNodeType bool
// Enable OmitSuiteSetupNodes to prevent the creation of testcase entries for setup nodes
OmitSuiteSetupNodes bool
}
type JUnitTestSuites struct {
@ -177,6 +180,9 @@ func GenerateJUnitReportWithConfig(report types.Report, dst string, config Junit
},
}
for _, spec := range report.SpecReports {
if config.OmitSuiteSetupNodes && spec.LeafNodeType != types.NodeTypeIt {
continue
}
name := fmt.Sprintf("[%s]", spec.LeafNodeType)
if config.OmitLeafNodeType {
name = ""

View File

@ -13,7 +13,7 @@ import (
/*
The EntryDescription decorator allows you to pass a format string to DescribeTable() and Entry(). This format string is used to generate entry names via:
fmt.Sprintf(formatString, parameters...)
fmt.Sprintf(formatString, parameters...)
where parameters are the parameters passed into the entry.
@ -32,19 +32,20 @@ DescribeTable describes a table-driven spec.
For example:
DescribeTable("a simple table",
func(x int, y int, expected bool) {
Ω(x > y).Should(Equal(expected))
},
Entry("x > y", 1, 0, true),
Entry("x == y", 0, 0, false),
Entry("x < y", 0, 1, false),
)
DescribeTable("a simple table",
func(x int, y int, expected bool) {
Ω(x > y).Should(Equal(expected))
},
Entry("x > y", 1, 0, true),
Entry("x == y", 0, 0, false),
Entry("x < y", 0, 1, false),
)
You can learn more about DescribeTable here: https://onsi.github.io/ginkgo/#table-specs
And can explore some Table patterns here: https://onsi.github.io/ginkgo/#table-specs-patterns
*/
func DescribeTable(description string, args ...interface{}) bool {
GinkgoHelper()
generateTable(description, args...)
return true
}
@ -53,6 +54,7 @@ func DescribeTable(description string, args ...interface{}) bool {
You can focus a table with `FDescribeTable`. This is equivalent to `FDescribe`.
*/
func FDescribeTable(description string, args ...interface{}) bool {
GinkgoHelper()
args = append(args, internal.Focus)
generateTable(description, args...)
return true
@ -62,6 +64,7 @@ func FDescribeTable(description string, args ...interface{}) bool {
You can mark a table as pending with `PDescribeTable`. This is equivalent to `PDescribe`.
*/
func PDescribeTable(description string, args ...interface{}) bool {
GinkgoHelper()
args = append(args, internal.Pending)
generateTable(description, args...)
return true
@ -95,26 +98,29 @@ If you want to generate interruptible specs simply write a Table function that a
You can learn more about Entry here: https://onsi.github.io/ginkgo/#table-specs
*/
func Entry(description interface{}, args ...interface{}) TableEntry {
GinkgoHelper()
decorations, parameters := internal.PartitionDecorations(args...)
return TableEntry{description: description, decorations: decorations, parameters: parameters, codeLocation: types.NewCodeLocation(1)}
return TableEntry{description: description, decorations: decorations, parameters: parameters, codeLocation: types.NewCodeLocation(0)}
}
/*
You can focus a particular entry with FEntry. This is equivalent to FIt.
*/
func FEntry(description interface{}, args ...interface{}) TableEntry {
GinkgoHelper()
decorations, parameters := internal.PartitionDecorations(args...)
decorations = append(decorations, internal.Focus)
return TableEntry{description: description, decorations: decorations, parameters: parameters, codeLocation: types.NewCodeLocation(1)}
return TableEntry{description: description, decorations: decorations, parameters: parameters, codeLocation: types.NewCodeLocation(0)}
}
/*
You can mark a particular entry as pending with PEntry. This is equivalent to PIt.
*/
func PEntry(description interface{}, args ...interface{}) TableEntry {
GinkgoHelper()
decorations, parameters := internal.PartitionDecorations(args...)
decorations = append(decorations, internal.Pending)
return TableEntry{description: description, decorations: decorations, parameters: parameters, codeLocation: types.NewCodeLocation(1)}
return TableEntry{description: description, decorations: decorations, parameters: parameters, codeLocation: types.NewCodeLocation(0)}
}
/*
@ -126,7 +132,8 @@ var contextType = reflect.TypeOf(new(context.Context)).Elem()
var specContextType = reflect.TypeOf(new(SpecContext)).Elem()
func generateTable(description string, args ...interface{}) {
cl := types.NewCodeLocation(2)
GinkgoHelper()
cl := types.NewCodeLocation(0)
containerNodeArgs := []interface{}{cl}
entries := []TableEntry{}

View File

@ -7,6 +7,7 @@ import (
"runtime"
"runtime/debug"
"strings"
"sync"
)
type CodeLocation struct {
@ -38,6 +39,73 @@ func (codeLocation CodeLocation) ContentsOfLine() string {
return lines[codeLocation.LineNumber-1]
}
type codeLocationLocator struct {
pcs map[uintptr]bool
helpers map[string]bool
lock *sync.Mutex
}
func (c *codeLocationLocator) addHelper(pc uintptr) {
c.lock.Lock()
defer c.lock.Unlock()
if c.pcs[pc] {
return
}
c.lock.Unlock()
f := runtime.FuncForPC(pc)
c.lock.Lock()
if f == nil {
return
}
c.helpers[f.Name()] = true
c.pcs[pc] = true
}
func (c *codeLocationLocator) hasHelper(name string) bool {
c.lock.Lock()
defer c.lock.Unlock()
return c.helpers[name]
}
func (c *codeLocationLocator) getCodeLocation(skip int) CodeLocation {
pc := make([]uintptr, 40)
n := runtime.Callers(skip+2, pc)
if n == 0 {
return CodeLocation{}
}
pc = pc[:n]
frames := runtime.CallersFrames(pc)
for {
frame, more := frames.Next()
if !c.hasHelper(frame.Function) {
return CodeLocation{FileName: frame.File, LineNumber: frame.Line}
}
if !more {
break
}
}
return CodeLocation{}
}
var clLocator = &codeLocationLocator{
pcs: map[uintptr]bool{},
helpers: map[string]bool{},
lock: &sync.Mutex{},
}
// MarkAsHelper is used by GinkgoHelper to mark the caller (appropriately offset by skip)as a helper. You can use this directly if you need to provide an optional `skip` to mark functions further up the call stack as helpers.
func MarkAsHelper(optionalSkip ...int) {
skip := 1
if len(optionalSkip) > 0 {
skip += optionalSkip[0]
}
pc, _, _, ok := runtime.Caller(skip)
if ok {
clLocator.addHelper(pc)
}
}
func NewCustomCodeLocation(message string) CodeLocation {
return CodeLocation{
CustomMessage: message,
@ -45,14 +113,13 @@ func NewCustomCodeLocation(message string) CodeLocation {
}
func NewCodeLocation(skip int) CodeLocation {
_, file, line, _ := runtime.Caller(skip + 1)
return CodeLocation{FileName: file, LineNumber: line}
return clLocator.getCodeLocation(skip + 1)
}
func NewCodeLocationWithStackTrace(skip int) CodeLocation {
_, file, line, _ := runtime.Caller(skip + 1)
stackTrace := PruneStack(string(debug.Stack()), skip+1)
return CodeLocation{FileName: file, LineNumber: line, FullStackTrace: stackTrace}
cl := clLocator.getCodeLocation(skip + 1)
cl.FullStackTrace = PruneStack(string(debug.Stack()), skip+1)
return cl
}
// PruneStack removes references to functions that are internal to Ginkgo

View File

@ -8,6 +8,7 @@ package types
import (
"flag"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
@ -30,6 +31,7 @@ type SuiteConfig struct {
PollProgressAfter time.Duration
PollProgressInterval time.Duration
Timeout time.Duration
EmitSpecProgress bool // this is deprecated but its removal is causing compile issue for some users that were setting it manually
OutputInterceptorMode string
SourceRoots []string
GracePeriod time.Duration
@ -599,13 +601,29 @@ func VetAndInitializeCLIAndGoConfig(cliConfig CLIConfig, goFlagsConfig GoFlagsCo
}
// GenerateGoTestCompileArgs is used by the Ginkgo CLI to generate command line arguments to pass to the go test -c command when compiling the test
func GenerateGoTestCompileArgs(goFlagsConfig GoFlagsConfig, destination string, packageToBuild string) ([]string, error) {
func GenerateGoTestCompileArgs(goFlagsConfig GoFlagsConfig, destination string, packageToBuild string, pathToInvocationPath string) ([]string, error) {
// if the user has set the CoverProfile run-time flag make sure to set the build-time cover flag to make sure
// the built test binary can generate a coverprofile
if goFlagsConfig.CoverProfile != "" {
goFlagsConfig.Cover = true
}
if goFlagsConfig.CoverPkg != "" {
coverPkgs := strings.Split(goFlagsConfig.CoverPkg, ",")
adjustedCoverPkgs := make([]string, len(coverPkgs))
for i, coverPkg := range coverPkgs {
coverPkg = strings.Trim(coverPkg, " ")
if strings.HasPrefix(coverPkg, "./") {
// this is a relative coverPkg - we need to reroot it
adjustedCoverPkgs[i] = "./" + filepath.Join(pathToInvocationPath, strings.TrimPrefix(coverPkg, "./"))
} else {
// this is a package name - don't touch it
adjustedCoverPkgs[i] = coverPkg
}
}
goFlagsConfig.CoverPkg = strings.Join(adjustedCoverPkgs, ",")
}
args := []string{"test", "-c", "-o", destination, packageToBuild}
goArgs, err := GenerateFlagArgs(
GoBuildFlags,

View File

@ -272,12 +272,23 @@ func tokenize(input string) func() (*treeNode, error) {
}
}
func MustParseLabelFilter(input string) LabelFilter {
filter, err := ParseLabelFilter(input)
if err != nil {
panic(err)
}
return filter
}
func ParseLabelFilter(input string) (LabelFilter, error) {
if DEBUG_LABEL_FILTER_PARSING {
fmt.Println("\n==============")
fmt.Println("Input: ", input)
fmt.Print("Tokens: ")
}
if input == "" {
return func(_ []string) bool { return true }, nil
}
nextToken := tokenize(input)
root := &treeNode{token: lfTokenRoot}

View File

@ -1,3 +1,3 @@
package types
const VERSION = "2.7.0"
const VERSION = "2.9.2"