@@ -3,18 +3,26 @@ 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
- "math"
9
11
"os"
10
12
"path/filepath"
11
- "regexp"
12
13
"sort"
13
14
"strings"
14
15
15
16
"github.com/sirupsen/logrus"
16
17
)
17
18
19
+ const (
20
+ ginkgoDescribeFunctionName = "Describe"
21
+ ginkgoLabelFunctionName = "Label"
22
+ )
23
+
24
+ var logger = logrus .New ()
25
+
18
26
type options struct {
19
27
numChunks int
20
28
printChunk int
@@ -34,200 +42,105 @@ func main() {
34
42
flag .Parse ()
35
43
36
44
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 ))
45
+ 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
46
}
39
47
40
48
dir := flag .Arg (0 )
41
49
if dir == "" {
42
- exitIfErr (fmt .Errorf ("test directory required as the argument" ))
50
+ log . Fatal (fmt .Errorf ("test directory required as the argument" ))
43
51
}
44
52
45
- // Clean dir.
46
53
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
- }
54
+ level , err := logrus .ParseLevel (opts .logLevel )
55
+ if err != nil {
56
+ log .Fatal (err )
57
+ }
58
+ logger .SetLevel (level )
56
59
57
- func exitIfErr ( err error ) {
60
+ dir , err = getPathRelativeToCwd ( dir )
58
61
if err != nil {
59
62
log .Fatal (err )
60
63
}
64
+
65
+ if err := opts .run (dir ); err != nil {
66
+ log .Fatal (err )
67
+ }
61
68
}
62
69
63
- func ( opts options ) run ( dir string ) error {
64
- level , err := logrus . ParseLevel ( opts . logLevel )
70
+ func getPathRelativeToCwd ( path string ) ( string , error ) {
71
+ path , err := filepath . Abs ( path )
65
72
if err != nil {
66
- return fmt . Errorf ( "failed to parse the %s log level: %v " , opts . logLevel , err )
73
+ return " " , err
67
74
}
68
- logger := logrus .New ()
69
- logger .SetLevel (level )
70
75
71
- describes , err := findDescribes ( logger , dir )
76
+ wd , err := os . Getwd ( )
72
77
if err != nil {
73
- return err
78
+ return "" , err
74
79
}
80
+ return filepath .Rel (wd , path )
81
+ }
75
82
76
- // Find minimal prefixes for all spec strings so no spec runs are duplicated across chunks.
77
- prefixes := findMinimalWordPrefixes (describes )
78
- sort .Strings (prefixes )
83
+ func (opts options ) run (dir string ) error {
84
+ // Get all test labels
85
+ labels , err := findLabels (dir )
86
+ if err != nil {
87
+ return err
88
+ }
89
+ sort .Strings (labels )
79
90
80
91
var out string
81
92
if opts .printDebug {
82
- out = strings .Join (prefixes , "\n " )
93
+ out = strings .Join (labels , "\n " )
83
94
} else {
84
- out , err = createChunkRegexp (opts .numChunks , opts .printChunk , prefixes )
85
- if err != nil {
86
- return err
87
- }
95
+ out = strings .Join (labels , " || " )
88
96
}
89
97
90
98
fmt .Fprint (opts .writer , out )
91
99
return nil
92
100
}
93
101
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 {})
102
+ func findLabels (dir string ) ([]string , error ) {
103
+ var labels []string
104
+ logger .Infof ("Finding labels for ginkgo tests in path: %s" , dir )
103
105
matches , err := filepath .Glob (filepath .Join (dir , "*_test.go" ))
104
106
if err != nil {
105
107
return nil , err
106
108
}
107
109
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 ))
122
- }
123
- continue
124
- }
125
- describe := strings .TrimSpace (string (possibleNames [1 ]))
126
- describeTable [describe ] = struct {}{}
127
- }
128
- }
129
-
130
- describes := make ([]string , len (describeTable ))
131
- i := 0
132
- for describeKey := range describeTable {
133
- describes [i ] = describeKey
134
- i ++
110
+ labels = append (labels , extractLabelsFromFile (match )... )
135
111
}
136
- return describes , nil
112
+ return labels , nil
137
113
}
138
114
139
- func createChunkRegexp (numChunks , printChunk int , specs []string ) (string , error ) {
140
- numSpecs := len (specs )
141
- if numSpecs < numChunks {
142
- return "" , fmt .Errorf ("have more desired chunks (%d) than specs (%d)" , numChunks , numSpecs )
143
- }
144
-
145
- // Create chunks of size ceil(number of specs/number of chunks) in alphanumeric order.
146
- // This is deterministic on inputs.
147
- chunks := make ([][]string , numChunks )
148
- interval := int (math .Ceil (float64 (numSpecs ) / float64 (numChunks )))
149
- currIdx := 0
150
- for chunkIdx := 0 ; chunkIdx < numChunks ; chunkIdx ++ {
151
- nextIdx := int (math .Min (float64 (currIdx + interval ), float64 (numSpecs )))
152
- chunks [chunkIdx ] = specs [currIdx :nextIdx ]
153
- currIdx = nextIdx
154
- }
155
-
156
- chunk := chunks [printChunk ]
157
- if len (chunk ) == 0 {
158
- // This is a panic because the caller may skip this error, resulting in missed test specs.
159
- panic (fmt .Sprintf ("bug: chunk %d has no elements" , printChunk ))
160
- }
115
+ func extractLabelsFromFile (filename string ) []string {
116
+ var labels []string
161
117
162
- // Write out the regexp to focus chunk specs via `ginkgo -focus <re>`.
163
- var reStr string
164
- if len (chunk ) == 1 {
165
- reStr = fmt .Sprintf ("%s .*" , chunk [0 ])
166
- } else {
167
- sb := strings.Builder {}
168
- sb .WriteString (chunk [0 ])
169
- for _ , test := range chunk [1 :] {
170
- sb .WriteString ("|" )
171
- sb .WriteString (test )
172
- }
173
- reStr = fmt .Sprintf ("(%s) .*" , sb .String ())
174
- }
175
-
176
- return reStr , nil
177
- }
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
118
+ // Create a Go source file set
119
+ fs := token .NewFileSet ()
120
+ node , err := parser .ParseFile (fs , filename , nil , parser .AllErrors )
121
+ if err != nil {
122
+ fmt .Printf ("Error parsing file %s: %v\n " , filename , err )
123
+ return labels
124
+ }
125
+
126
+ ast .Inspect (node , func (n ast.Node ) bool {
127
+ if callExpr , ok := n .(* ast.CallExpr ); ok {
128
+ if fun , ok := callExpr .Fun .(* ast.Ident ); ok && fun .Name == ginkgoDescribeFunctionName {
129
+ for _ , arg := range callExpr .Args {
130
+ if ce , ok := arg .(* ast.CallExpr ); ok {
131
+ if labelFunc , ok := ce .Fun .(* ast.Ident ); ok && labelFunc .Name == ginkgoLabelFunctionName {
132
+ for _ , arg := range ce .Args {
133
+ if lit , ok := arg .(* ast.BasicLit ); ok && lit .Kind == token .STRING {
134
+ labels = append (labels , strings .Trim (lit .Value , "\" " ))
135
+ }
136
+ }
137
+ }
138
+ }
139
+ }
196
140
}
197
- goto next
198
141
}
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
- }
142
+ return true
143
+ })
206
144
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 {}
145
+ return labels
233
146
}
0 commit comments