@@ -3,18 +3,27 @@ package main
33import (
44 "flag"
55 "fmt"
6+ "go/ast"
7+ "go/parser"
8+ "go/token"
69 "io"
710 "log"
811 "math"
912 "os"
1013 "path/filepath"
11- "regexp"
1214 "sort"
1315 "strings"
1416
1517 "github.com/sirupsen/logrus"
1618)
1719
20+ const (
21+ ginkgoDescribeFunctionName = "Describe"
22+ ginkgoLabelFunctionName = "Label"
23+ )
24+
25+ var logger = logrus .New ()
26+
1827type options struct {
1928 numChunks int
2029 printChunk int
@@ -34,54 +43,57 @@ func main() {
3443 flag .Parse ()
3544
3645 if opts .printChunk >= opts .numChunks {
37- exitIfErr (fmt .Errorf ("the chunk to print (%d) must be a smaller number than the number of chunks (%d)" , opts .printChunk , opts .numChunks ))
46+ log . Fatal (fmt .Errorf ("the chunk to print (%d) must be a smaller number than the number of chunks (%d)" , opts .printChunk , opts .numChunks ))
3847 }
3948
4049 dir := flag .Arg (0 )
4150 if dir == "" {
42- exitIfErr (fmt .Errorf ("test directory required as the argument" ))
51+ log . Fatal (fmt .Errorf ("test directory required as the argument" ))
4352 }
4453
45- // Clean dir.
4654 var err error
47- dir , err = filepath .Abs (dir )
48- exitIfErr (err )
49- wd , err := os .Getwd ()
50- exitIfErr (err )
51- dir , err = filepath .Rel (wd , dir )
52- exitIfErr (err )
53-
54- exitIfErr (opts .run (dir ))
55- }
55+ level , err := logrus .ParseLevel (opts .logLevel )
56+ if err != nil {
57+ log .Fatal (err )
58+ }
59+ logger .SetLevel (level )
5660
57- func exitIfErr ( err error ) {
61+ dir , err = getPathRelativeToCwd ( dir )
5862 if err != nil {
5963 log .Fatal (err )
6064 }
65+
66+ if err := opts .run (dir ); err != nil {
67+ log .Fatal (err )
68+ }
6169}
6270
63- func ( opts options ) run ( dir string ) error {
64- level , err := logrus . ParseLevel ( opts . logLevel )
71+ func getPathRelativeToCwd ( path string ) ( string , error ) {
72+ path , err := filepath . Abs ( path )
6573 if err != nil {
66- return fmt . Errorf ( "failed to parse the %s log level: %v " , opts . logLevel , err )
74+ return " " , err
6775 }
68- logger := logrus .New ()
69- logger .SetLevel (level )
7076
71- describes , err := findDescribes ( logger , dir )
77+ wd , err := os . Getwd ( )
7278 if err != nil {
73- return err
79+ return "" , err
7480 }
81+ return filepath .Rel (wd , path )
82+ }
7583
76- // Find minimal prefixes for all spec strings so no spec runs are duplicated across chunks.
77- prefixes := findMinimalWordPrefixes (describes )
78- sort .Strings (prefixes )
84+ func (opts options ) run (dir string ) error {
85+ // Get all test labels
86+ labels , err := findLabels (dir )
87+ if err != nil {
88+ return err
89+ }
90+ sort .Strings (labels )
7991
8092 var out string
8193 if opts .printDebug {
82- out = strings .Join (prefixes , "\n " )
94+ out = strings .Join (labels , "\n " )
8395 } else {
84- out , err = createChunkRegexp (opts .numChunks , opts .printChunk , prefixes )
96+ out , err = createFilterLabelChunk (opts .numChunks , opts .printChunk , labels )
8597 if err != nil {
8698 return err
8799 }
@@ -91,52 +103,53 @@ func (opts options) run(dir string) error {
91103 return nil
92104}
93105
94- // TODO: this is hacky because top-level tests may be defined elsewise.
95- // A better strategy would be to use the output of `ginkgo -noColor -dryRun`
96- // like https://github.com/operator-framework/operator-lifecycle-manager/pull/1476 does.
97- var topDescribeRE = regexp .MustCompile (`var _ = Describe\("(.+)", func\(.*` )
98-
99- func findDescribes (logger logrus.FieldLogger , dir string ) ([]string , error ) {
100- // Find all Ginkgo specs in dir's test files.
101- // These can be grouped independently.
102- describeTable := make (map [string ]struct {})
106+ func findLabels (dir string ) ([]string , error ) {
107+ var labels []string
108+ logger .Infof ("Finding labels for ginkgo tests in path: %s" , dir )
103109 matches , err := filepath .Glob (filepath .Join (dir , "*_test.go" ))
104110 if err != nil {
105111 return nil , err
106112 }
107113 for _ , match := range matches {
108- b , err := os .ReadFile (match )
109- if err != nil {
110- return nil , err
111- }
112- specNames := topDescribeRE .FindAllSubmatch (b , - 1 )
113- if len (specNames ) == 0 {
114- logger .Warnf ("%s: found no top level describes, skipping" , match )
115- continue
116- }
117- for _ , possibleNames := range specNames {
118- if len (possibleNames ) != 2 {
119- logger .Debugf ("%s: expected to find 2 submatch, found %d:" , match , len (possibleNames ))
120- for _ , name := range possibleNames {
121- logger .Debugf ("\t %s\n " , string (name ))
114+ labels = append (labels , extractLabelsFromFile (match )... )
115+ }
116+ return labels , nil
117+ }
118+
119+ func extractLabelsFromFile (filename string ) []string {
120+ var labels []string
121+
122+ // Create a Go source file set
123+ fs := token .NewFileSet ()
124+ node , err := parser .ParseFile (fs , filename , nil , parser .AllErrors )
125+ if err != nil {
126+ fmt .Printf ("Error parsing file %s: %v\n " , filename , err )
127+ return labels
128+ }
129+
130+ ast .Inspect (node , func (n ast.Node ) bool {
131+ if callExpr , ok := n .(* ast.CallExpr ); ok {
132+ if fun , ok := callExpr .Fun .(* ast.Ident ); ok && fun .Name == ginkgoDescribeFunctionName {
133+ for _ , arg := range callExpr .Args {
134+ if ce , ok := arg .(* ast.CallExpr ); ok {
135+ if labelFunc , ok := ce .Fun .(* ast.Ident ); ok && labelFunc .Name == ginkgoLabelFunctionName {
136+ for _ , arg := range ce .Args {
137+ if lit , ok := arg .(* ast.BasicLit ); ok && lit .Kind == token .STRING {
138+ labels = append (labels , strings .Trim (lit .Value , "\" " ))
139+ }
140+ }
141+ }
142+ }
122143 }
123- continue
124144 }
125- describe := strings .TrimSpace (string (possibleNames [1 ]))
126- describeTable [describe ] = struct {}{}
127145 }
128- }
146+ return true
147+ })
129148
130- describes := make ([]string , len (describeTable ))
131- i := 0
132- for describeKey := range describeTable {
133- describes [i ] = describeKey
134- i ++
135- }
136- return describes , nil
149+ return labels
137150}
138151
139- func createChunkRegexp (numChunks , printChunk int , specs []string ) (string , error ) {
152+ func createFilterLabelChunk (numChunks , printChunk int , specs []string ) (string , error ) {
140153 numSpecs := len (specs )
141154 if numSpecs < numChunks {
142155 return "" , fmt .Errorf ("have more desired chunks (%d) than specs (%d)" , numChunks , numSpecs )
@@ -162,72 +175,16 @@ func createChunkRegexp(numChunks, printChunk int, specs []string) (string, error
162175 // Write out the regexp to focus chunk specs via `ginkgo -focus <re>`.
163176 var reStr string
164177 if len (chunk ) == 1 {
165- reStr = fmt .Sprintf ("%s .* " , chunk [0 ])
178+ reStr = fmt .Sprintf ("%s" , chunk [0 ])
166179 } else {
167180 sb := strings.Builder {}
168181 sb .WriteString (chunk [0 ])
169182 for _ , test := range chunk [1 :] {
170- sb .WriteString ("| " )
183+ sb .WriteString (" || " )
171184 sb .WriteString (test )
172185 }
173- reStr = fmt .Sprintf ("(%s) .* " , sb .String ())
186+ reStr = fmt .Sprintf ("%s " , sb .String ())
174187 }
175188
176189 return reStr , nil
177190}
178-
179- func findMinimalWordPrefixes (specs []string ) (prefixes []string ) {
180- // Create a word trie of all spec strings.
181- t := make (wordTrie )
182- for _ , spec := range specs {
183- t .push (spec )
184- }
185-
186- // Now find the first branch point for each path in the trie by DFS.
187- for word , node := range t {
188- var prefixElements []string
189- next:
190- if word != "" {
191- prefixElements = append (prefixElements , word )
192- }
193- if len (node .children ) == 1 {
194- for nextWord , nextNode := range node .children {
195- word , node = nextWord , nextNode
196- }
197- goto next
198- }
199- // TODO: this might need to be joined by "\s+"
200- // in case multiple spaces were used in the spec name.
201- prefixes = append (prefixes , strings .Join (prefixElements , " " ))
202- }
203-
204- return prefixes
205- }
206-
207- // wordTrie is a trie of word nodes, instead of individual characters.
208- type wordTrie map [string ]* wordTrieNode
209-
210- type wordTrieNode struct {
211- word string
212- children map [string ]* wordTrieNode
213- }
214-
215- // push creates s branch of the trie from each word in s.
216- func (t wordTrie ) push (s string ) {
217- split := strings .Split (s , " " )
218-
219- curr := & wordTrieNode {word : "" , children : t }
220- for _ , sp := range split {
221- if sp = strings .TrimSpace (sp ); sp == "" {
222- continue
223- }
224- next , hasNext := curr .children [sp ]
225- if ! hasNext {
226- next = & wordTrieNode {word : sp , children : make (map [string ]* wordTrieNode )}
227- curr .children [sp ] = next
228- }
229- curr = next
230- }
231- // Add termination node so "foo" and "foo bar" have a branching point of "foo".
232- curr .children ["" ] = & wordTrieNode {}
233- }
0 commit comments