Skip to content

Commit 7f001eb

Browse files
author
Per Goncalves da Silva
committed
Refactor e2e split tool for labels
Signed-off-by: Per Goncalves da Silva <[email protected]>
1 parent 58f57d8 commit 7f001eb

File tree

5 files changed

+104
-277
lines changed

5 files changed

+104
-277
lines changed

test/e2e/split/integration_test.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ function get_total_specs() {
55
}
66

77
unfocused_specs=$(get_total_specs)
8-
regexp=$(go run ./test/e2e/split/... -chunks 1 -print-chunk 0 ./test/e2e)
9-
focused_specs=$(get_total_specs -focus "$regexp")
8+
label_filter=$(go run ./test/e2e/split/... -chunks 1 -print-chunk 0 ./test/e2e)
9+
focused_specs=$(get_total_specs -label-filter "$label_filter")
1010

1111
if ! [ $unfocused_specs -eq $focused_specs ]; then
1212
echo "expected number of unfocused specs $unfocused_specs to equal focus specs $focused_specs"

test/e2e/split/main.go

Lines changed: 70 additions & 157 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,26 @@ package main
33
import (
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+
1826
type 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

Comments
 (0)