@@ -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
4344func 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
5154Each *_test.yaml file defines a workflow config and a set of named test cases.
5255Results 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+
5460Examples:
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
5967Options:
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 ("\n Pipeline Coverage: %d/%d (%.1f%%)\n " , covered , total , pct )
176+
177+ if covered > 0 {
178+ fmt .Println ("\n COVERED:" )
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 ("\n UNCOVERED:" )
186+ for _ , name := range report .UncoveredPipelines {
187+ fmt .Printf (" %s\n " , name )
188+ }
189+ }
190+
191+ fmt .Printf ("\n Scenario 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.
126234type testFile struct {
127235 Config string `yaml:"config"`
0 commit comments