github.com/operator-framework/operator-lifecycle-manager@v0.30.0/test/e2e/split/main.go (about) 1 package main 2 3 import ( 4 "flag" 5 "fmt" 6 "io" 7 "log" 8 "math" 9 "os" 10 "path/filepath" 11 "regexp" 12 "sort" 13 "strings" 14 15 "github.com/sirupsen/logrus" 16 ) 17 18 type options struct { 19 numChunks int 20 printChunk int 21 printDebug bool 22 writer io.Writer 23 logLevel string 24 } 25 26 func main() { 27 opts := options{ 28 writer: os.Stdout, 29 } 30 flag.IntVar(&opts.numChunks, "chunks", 1, "Number of chunks to create focus regexps for") 31 flag.IntVar(&opts.printChunk, "print-chunk", 0, "Chunk to print a regexp for") 32 flag.BoolVar(&opts.printDebug, "print-debug", false, "Print all spec prefixes in non-regexp format. Use for debugging") 33 flag.StringVar(&opts.logLevel, "log-level", logrus.ErrorLevel.String(), "Configure the logging level") 34 flag.Parse() 35 36 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)) 38 } 39 40 dir := flag.Arg(0) 41 if dir == "" { 42 exitIfErr(fmt.Errorf("test directory required as the argument")) 43 } 44 45 // Clean dir. 46 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 } 56 57 func exitIfErr(err error) { 58 if err != nil { 59 log.Fatal(err) 60 } 61 } 62 63 func (opts options) run(dir string) error { 64 level, err := logrus.ParseLevel(opts.logLevel) 65 if err != nil { 66 return fmt.Errorf("failed to parse the %s log level: %v", opts.logLevel, err) 67 } 68 logger := logrus.New() 69 logger.SetLevel(level) 70 71 describes, err := findDescribes(logger, dir) 72 if err != nil { 73 return err 74 } 75 76 // Find minimal prefixes for all spec strings so no spec runs are duplicated across chunks. 77 prefixes := findMinimalWordPrefixes(describes) 78 sort.Strings(prefixes) 79 80 var out string 81 if opts.printDebug { 82 out = strings.Join(prefixes, "\n") 83 } else { 84 out, err = createChunkRegexp(opts.numChunks, opts.printChunk, prefixes) 85 if err != nil { 86 return err 87 } 88 } 89 90 fmt.Fprint(opts.writer, out) 91 return nil 92 } 93 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{}) 103 matches, err := filepath.Glob(filepath.Join(dir, "*_test.go")) 104 if err != nil { 105 return nil, err 106 } 107 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++ 135 } 136 return describes, nil 137 } 138 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 } 161 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 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 }