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  }