@@ -3,18 +3,27 @@ package main
3
3
import (
4
4
"flag"
5
5
"fmt"
6
+ "go/ast"
7
+ "go/parser"
8
+ "go/token"
6
9
"io"
7
10
"log"
8
11
"math"
9
12
"os"
10
13
"path/filepath"
11
- "regexp"
12
14
"sort"
13
15
"strings"
14
16
15
17
"github.com/sirupsen/logrus"
16
18
)
17
19
20
+ const (
21
+ ginkgoDescribeFunctionName = "Describe"
22
+ ginkgoLabelFunctionName = "Label"
23
+ )
24
+
25
+ var logger = logrus .New ()
26
+
18
27
type options struct {
19
28
numChunks int
20
29
printChunk int
@@ -34,54 +43,57 @@ func main() {
34
43
flag .Parse ()
35
44
36
45
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 ))
38
47
}
39
48
40
49
dir := flag .Arg (0 )
41
50
if dir == "" {
42
- exitIfErr (fmt .Errorf ("test directory required as the argument" ))
51
+ log . Fatal (fmt .Errorf ("test directory required as the argument" ))
43
52
}
44
53
45
- // Clean dir.
46
54
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 )
56
60
57
- func exitIfErr ( err error ) {
61
+ dir , err = getPathRelativeToCwd ( dir )
58
62
if err != nil {
59
63
log .Fatal (err )
60
64
}
65
+
66
+ if err := opts .run (dir ); err != nil {
67
+ log .Fatal (err )
68
+ }
61
69
}
62
70
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 )
65
73
if err != nil {
66
- return fmt . Errorf ( "failed to parse the %s log level: %v " , opts . logLevel , err )
74
+ return " " , err
67
75
}
68
- logger := logrus .New ()
69
- logger .SetLevel (level )
70
76
71
- describes , err := findDescribes ( logger , dir )
77
+ wd , err := os . Getwd ( )
72
78
if err != nil {
73
- return err
79
+ return "" , err
74
80
}
81
+ return filepath .Rel (wd , path )
82
+ }
75
83
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 )
79
91
80
92
var out string
81
93
if opts .printDebug {
82
- out = strings .Join (prefixes , "\n " )
94
+ out = strings .Join (labels , "\n " )
83
95
} else {
84
- out , err = createChunkRegexp (opts .numChunks , opts .printChunk , prefixes )
96
+ out , err = createFilterLabelChunk (opts .numChunks , opts .printChunk , labels )
85
97
if err != nil {
86
98
return err
87
99
}
@@ -91,52 +103,53 @@ func (opts options) run(dir string) error {
91
103
return nil
92
104
}
93
105
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 )
103
109
matches , err := filepath .Glob (filepath .Join (dir , "*_test.go" ))
104
110
if err != nil {
105
111
return nil , err
106
112
}
107
113
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
+ }
122
143
}
123
- continue
124
144
}
125
- describe := strings .TrimSpace (string (possibleNames [1 ]))
126
- describeTable [describe ] = struct {}{}
127
145
}
128
- }
146
+ return true
147
+ })
129
148
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
137
150
}
138
151
139
- func createChunkRegexp (numChunks , printChunk int , specs []string ) (string , error ) {
152
+ func createFilterLabelChunk (numChunks , printChunk int , specs []string ) (string , error ) {
140
153
numSpecs := len (specs )
141
154
if numSpecs < numChunks {
142
155
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
162
175
// Write out the regexp to focus chunk specs via `ginkgo -focus <re>`.
163
176
var reStr string
164
177
if len (chunk ) == 1 {
165
- reStr = fmt .Sprintf ("%s .* " , chunk [0 ])
178
+ reStr = fmt .Sprintf ("%s" , chunk [0 ])
166
179
} else {
167
180
sb := strings.Builder {}
168
181
sb .WriteString (chunk [0 ])
169
182
for _ , test := range chunk [1 :] {
170
- sb .WriteString ("| " )
183
+ sb .WriteString (" || " )
171
184
sb .WriteString (test )
172
185
}
173
- reStr = fmt .Sprintf ("(%s) .* " , sb .String ())
186
+ reStr = fmt .Sprintf ("%s " , sb .String ())
174
187
}
175
188
176
189
return reStr , nil
177
190
}
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