Skip to content

Commit 800d6a1

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 800d6a1

File tree

5 files changed

+117
-178
lines changed

5 files changed

+117
-178
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: 77 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,27 @@ package main
33
import (
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+
1827
type 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

Comments
 (0)