Skip to content

Commit 5cdbcc1

Browse files
intel352claude
andcommitted
feat: add BDD support to wfctl test + update testing docs
cmd/wfctl/test.go: - Add --coverage flag: calls bdd.CalculateCoverage(config, features-dir) and prints pipeline + scenario coverage report. - Add --strict flag: with --coverage, fails if any pipelines are uncovered. - Detect .feature files in targets and print go test guidance. docs/testing.md: - Add full BDD / Gherkin section covering: quick start, all pre-built step definitions (engine, mock, HTTP, trigger, state, assertion), strict mode, and pipeline coverage with wfctl test --coverage examples. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 548cce5 commit 5cdbcc1

2 files changed

Lines changed: 320 additions & 11 deletions

File tree

cmd/wfctl/test.go

Lines changed: 119 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,24 +37,32 @@ import (
3737
pluginsecrets "github.com/GoCodeAlone/workflow/plugins/secrets"
3838
pluginsm "github.com/GoCodeAlone/workflow/plugins/statemachine"
3939
pluginstorage "github.com/GoCodeAlone/workflow/plugins/storage"
40+
"github.com/GoCodeAlone/workflow/wftest/bdd"
4041
"gopkg.in/yaml.v3"
4142
)
4243

4344
func runTest(args []string) error {
4445
fs := flag.NewFlagSet("test", flag.ContinueOnError)
4546
verbose := fs.Bool("v", false, "Verbose output (print each assertion)")
47+
coverage := fs.Bool("coverage", false, "Print pipeline + scenario coverage report (requires <config> <features-dir>)")
48+
strict := fs.Bool("strict", false, "Fail if any pipelines are uncovered (with --coverage)")
4649
fs.Usage = func() {
4750
fmt.Fprintf(fs.Output(), `Usage: wfctl test [options] <file_or_dir> [file_or_dir ...]
4851
49-
Run YAML-based workflow integration tests.
52+
Run YAML-based workflow integration tests or report BDD coverage.
5053
5154
Each *_test.yaml file defines a workflow config and a set of named test cases.
5255
Results are printed as PASS/FAIL with timing. Exit code is non-zero on failure.
5356
57+
BDD .feature files are detected automatically. They must be run via go test
58+
using the wftest/bdd package (see: wfctl test --help-bdd for details).
59+
5460
Examples:
5561
wfctl test tests/
5662
wfctl test tests/pipeline_test.yaml
5763
wfctl test -v tests/
64+
wfctl test --coverage config.yaml features/
65+
wfctl test --coverage --strict config.yaml features/
5866
5967
Options:
6068
`)
@@ -70,36 +78,58 @@ Options:
7078
return fmt.Errorf("at least one file or directory is required")
7179
}
7280

73-
// Collect all *_test.yaml files from the given targets.
74-
var files []string
81+
// Coverage mode: static pipeline + scenario coverage analysis.
82+
if *coverage {
83+
return runBDDCoverage(targets, *strict)
84+
}
85+
86+
// Separate YAML test files from .feature files.
87+
var yamlFiles []string
88+
var featureFiles []string
7589
for _, target := range targets {
7690
info, err := os.Stat(target)
7791
if err != nil {
7892
return fmt.Errorf("cannot access %s: %w", target, err)
7993
}
8094
if info.IsDir() {
81-
matches, err := filepath.Glob(filepath.Join(target, "*_test.yaml"))
95+
yMatches, err := filepath.Glob(filepath.Join(target, "*_test.yaml"))
96+
if err != nil {
97+
return fmt.Errorf("glob %s: %w", target, err)
98+
}
99+
yamlFiles = append(yamlFiles, yMatches...)
100+
101+
fMatches, err := filepath.Glob(filepath.Join(target, "*.feature"))
82102
if err != nil {
83103
return fmt.Errorf("glob %s: %w", target, err)
84104
}
85-
files = append(files, matches...)
105+
featureFiles = append(featureFiles, fMatches...)
106+
} else if strings.HasSuffix(target, ".feature") {
107+
featureFiles = append(featureFiles, target)
86108
} else {
87-
files = append(files, target)
109+
yamlFiles = append(yamlFiles, target)
88110
}
89111
}
90112

91-
if len(files) == 0 {
92-
fmt.Println("No *_test.yaml files found.")
113+
// Feature files require go test — print guidance.
114+
if len(featureFiles) > 0 {
115+
printBDDGuidance(featureFiles)
116+
}
117+
118+
if len(yamlFiles) == 0 && len(featureFiles) == 0 {
119+
fmt.Println("No *_test.yaml or *.feature files found.")
120+
return nil
121+
}
122+
if len(yamlFiles) == 0 {
93123
return nil
94124
}
95125

96-
// Run all files and collect results.
126+
// Run all YAML test files and collect results.
97127
var (
98128
totalPass int
99129
totalFail int
100130
)
101131

102-
for _, f := range files {
132+
for _, f := range yamlFiles {
103133
pass, fail, err := runTestFile(f, *verbose)
104134
if err != nil {
105135
fmt.Fprintf(os.Stderr, "error: %s: %v\n", f, err)
@@ -111,7 +141,7 @@ Options:
111141
}
112142

113143
// Print summary when more than one file was processed.
114-
if len(files) > 1 {
144+
if len(yamlFiles) > 1 {
115145
fmt.Printf("\n--- Summary ---\n")
116146
fmt.Printf(" %d passed, %d failed\n", totalPass, totalFail)
117147
}
@@ -122,6 +152,84 @@ Options:
122152
return nil
123153
}
124154

155+
// runBDDCoverage performs static pipeline + scenario coverage analysis.
156+
// Expects exactly 2 positional args: <config-file> <features-dir>.
157+
func runBDDCoverage(args []string, strict bool) error {
158+
if len(args) != 2 {
159+
return fmt.Errorf("--coverage requires exactly 2 arguments: <config-file> <features-dir>\n example: wfctl test --coverage config.yaml features/")
160+
}
161+
configPath := args[0]
162+
featureDir := args[1]
163+
164+
report, err := bdd.CalculateCoverage(configPath, featureDir)
165+
if err != nil {
166+
return fmt.Errorf("coverage: %w", err)
167+
}
168+
169+
covered := len(report.CoveredPipelines)
170+
total := report.TotalPipelines
171+
pct := 0.0
172+
if total > 0 {
173+
pct = float64(covered) / float64(total) * 100
174+
}
175+
fmt.Printf("\nPipeline Coverage: %d/%d (%.1f%%)\n", covered, total, pct)
176+
177+
if covered > 0 {
178+
fmt.Println("\nCOVERED:")
179+
for _, e := range report.CoveredPipelines {
180+
tag := fmt.Sprintf("(%s)", e.Via)
181+
fmt.Printf(" %-36s %s:%d %s\n", e.Pipeline, filepath.Base(e.FeatureFile), e.Line, tag)
182+
}
183+
}
184+
if len(report.UncoveredPipelines) > 0 {
185+
fmt.Println("\nUNCOVERED:")
186+
for _, name := range report.UncoveredPipelines {
187+
fmt.Printf(" %s\n", name)
188+
}
189+
}
190+
191+
fmt.Printf("\nScenario Coverage:\n")
192+
fmt.Printf(" Total: %d\n", report.TotalScenarios)
193+
if report.TotalScenarios > 0 {
194+
fmt.Printf(" With pipeline: %d (%.1f%%)\n",
195+
report.ImplementedScenarios,
196+
float64(report.ImplementedScenarios)/float64(report.TotalScenarios)*100)
197+
fmt.Printf(" Without: %d\n", report.UndefinedScenarios)
198+
}
199+
200+
if strict && len(report.UncoveredPipelines) > 0 {
201+
return fmt.Errorf("strict: %d pipeline(s) have no feature coverage: %s",
202+
len(report.UncoveredPipelines), strings.Join(report.UncoveredPipelines, ", "))
203+
}
204+
return nil
205+
}
206+
207+
// printBDDGuidance prints instructions for running .feature files via go test.
208+
func printBDDGuidance(featureFiles []string) {
209+
fmt.Printf("Found %d .feature file(s) — BDD tests must be run via go test.\n", len(featureFiles))
210+
fmt.Println()
211+
fmt.Println("To run BDD feature tests, create a Go test file in your package:")
212+
fmt.Println()
213+
fmt.Println(` // features_test.go`)
214+
fmt.Println(` package myapp_test`)
215+
fmt.Println()
216+
fmt.Println(` import (`)
217+
fmt.Println(` "testing"`)
218+
fmt.Println(` "github.com/GoCodeAlone/workflow/wftest/bdd"`)
219+
fmt.Println(` )`)
220+
fmt.Println()
221+
fmt.Println(` func TestFeatures(t *testing.T) {`)
222+
fmt.Println(` bdd.RunFeatures(t, "features/",`)
223+
fmt.Println(` bdd.WithConfig("config.yaml"),`)
224+
fmt.Println(` )`)
225+
fmt.Println(` }`)
226+
fmt.Println()
227+
fmt.Println("Then run: go test ./... -run TestFeatures")
228+
fmt.Println()
229+
fmt.Println("For coverage analysis: wfctl test --coverage config.yaml features/")
230+
fmt.Println()
231+
}
232+
125233
// testFile mirrors wftest.TestFile without the testing.T dependency.
126234
type testFile struct {
127235
Config string `yaml:"config"`

0 commit comments

Comments
 (0)