@@ -3,18 +3,26 @@ package main
33import (
44 "flag"
55 "fmt"
6+ "go/ast"
7+ "go/parser"
8+ "go/token"
69 "io"
710 "log"
8- "math"
911 "os"
1012 "path/filepath"
11- "regexp"
1213 "sort"
1314 "strings"
1415
1516 "github.com/sirupsen/logrus"
1617)
1718
19+ const (
20+ ginkgoDescribeFunctionName = "Describe"
21+ ginkgoLabelFunctionName = "Label"
22+ )
23+
24+ var logger = logrus .New ()
25+
1826type options struct {
1927 numChunks int
2028 printChunk int
@@ -34,200 +42,105 @@ func main() {
3442 flag .Parse ()
3543
3644 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 ))
3846 }
3947
4048 dir := flag .Arg (0 )
4149 if dir == "" {
42- exitIfErr (fmt .Errorf ("test directory required as the argument" ))
50+ log . Fatal (fmt .Errorf ("test directory required as the argument" ))
4351 }
4452
45- // Clean dir.
4653 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 )
5659
57- func exitIfErr ( err error ) {
60+ dir , err = getPathRelativeToCwd ( dir )
5861 if err != nil {
5962 log .Fatal (err )
6063 }
64+
65+ if err := opts .run (dir ); err != nil {
66+ log .Fatal (err )
67+ }
6168}
6269
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 )
6572 if err != nil {
66- return fmt . Errorf ( "failed to parse the %s log level: %v " , opts . logLevel , err )
73+ return " " , err
6774 }
68- logger := logrus .New ()
69- logger .SetLevel (level )
7075
71- describes , err := findDescribes ( logger , dir )
76+ wd , err := os . Getwd ( )
7277 if err != nil {
73- return err
78+ return "" , err
7479 }
80+ return filepath .Rel (wd , path )
81+ }
7582
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 )
7990
8091 var out string
8192 if opts .printDebug {
82- out = strings .Join (prefixes , "\n " )
93+ out = strings .Join (labels , "\n " )
8394 } else {
84- out , err = createChunkRegexp (opts .numChunks , opts .printChunk , prefixes )
85- if err != nil {
86- return err
87- }
95+ out = strings .Join (labels , " || " )
8896 }
8997
9098 fmt .Fprint (opts .writer , out )
9199 return nil
92100}
93101
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 )
103105 matches , err := filepath .Glob (filepath .Join (dir , "*_test.go" ))
104106 if err != nil {
105107 return nil , err
106108 }
107109 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 )... )
135111 }
136- return describes , nil
112+ return labels , nil
137113}
138114
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
161117
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+ }
196140 }
197- goto next
198141 }
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+ })
206144
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
233146}
0 commit comments