github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/tools/test_matrix_generator/matrix.go (about)

     1  package main
     2  
     3  import (
     4  	"bytes"
     5  	_ "embed"
     6  	"encoding/json"
     7  	"fmt"
     8  	"strings"
     9  
    10  	"github.com/spf13/pflag"
    11  	"golang.org/x/tools/go/packages"
    12  )
    13  
    14  var (
    15  	//go:embed default-test-matrix-config.json
    16  	defaultTestMatrixConfig string
    17  
    18  	//go:embed insecure-module-test-matrix-config.json
    19  	insecureModuleTestMatrixConfig string
    20  
    21  	//go:embed integration-module-test-matrix-config.json
    22  	integrationModuleTestMatrixConfig string
    23  
    24  	matrixConfigFile string
    25  )
    26  
    27  const (
    28  	flowPackagePrefix = "github.com/onflow/flow-go/"
    29  	ciMatrixName      = "dynamicMatrix"
    30  	defaultCIRunner   = "ubuntu-20.04"
    31  )
    32  
    33  // flowGoPackage configuration for a package to be tested.
    34  type flowGoPackage struct {
    35  	// Name the name of the package where test are located.
    36  	Name string `json:"name"`
    37  	// Runner the runner used for the top level github actions job that runs the tests all the tests in the parent package.
    38  	Runner string `json:"runner,omitempty"`
    39  	// Exclude list of packages to exclude from top level parent package test matrix.
    40  	Exclude []string `json:"exclude,omitempty"`
    41  	// Subpackages list of subpackages of the parent package that should be run in their own github actions job.
    42  	Subpackages []*subpackage `json:"subpackages,omitempty"`
    43  }
    44  
    45  // subpackage configuration for a subpackage.
    46  type subpackage struct {
    47  	Name   string `json:"name"`
    48  	Runner string `json:"runner,omitempty"`
    49  }
    50  
    51  // config the test matrix configuration for a package.
    52  type config struct {
    53  	// PackagesPath director where to load packages from.
    54  	PackagesPath string `json:"packagesPath,omitempty"`
    55  	// IncludeOthers when set to true will put all packages and subpackages of the packages path into a test matrix that will run in a job called others.
    56  	IncludeOthers bool `json:"includeOthers,omitempty"`
    57  	// Packages configurations for all packages that test should be run from.
    58  	Packages []*flowGoPackage `json:"packages"`
    59  }
    60  
    61  // testMatrix represents a single GitHub Actions test matrix combination that consists of a name and a list of flow-go packages associated with that name.
    62  type testMatrix struct {
    63  	Name     string `json:"name"`
    64  	Packages string `json:"packages"`
    65  	Runner   string `json:"runner"`
    66  }
    67  
    68  // newTestMatrix returns a new testMatrix, if runner is empty "" set the runner to the defaultCIRunner.
    69  func newTestMatrix(name, runner, pkgs string) *testMatrix {
    70  	t := &testMatrix{
    71  		Name:     name,
    72  		Packages: pkgs,
    73  		Runner:   runner,
    74  	}
    75  
    76  	if t.Runner == "" {
    77  		t.Runner = defaultCIRunner
    78  	}
    79  
    80  	return t
    81  }
    82  
    83  // Generates a list of packages to test that will be passed to GitHub Actions
    84  func main() {
    85  	pflag.Parse()
    86  
    87  	var configFile string
    88  	switch matrixConfigFile {
    89  	case "insecure":
    90  		configFile = insecureModuleTestMatrixConfig
    91  	case "integration":
    92  		configFile = integrationModuleTestMatrixConfig
    93  	default:
    94  		configFile = defaultTestMatrixConfig
    95  	}
    96  
    97  	packageConfig := loadPackagesConfig(configFile)
    98  
    99  	testMatrices := buildTestMatrices(packageConfig, listAllFlowPackages)
   100  	printCIString(testMatrices)
   101  }
   102  
   103  // printCIString encodes the test matrices and prints the json string to stdout. The CI runner will read this json string
   104  // and make the data available for our github workflows.
   105  func printCIString(testMatrices []*testMatrix) {
   106  	// generate JSON output that will be read in by CI matrix
   107  	// can't use json.MarshalIndent because fromJSON() in CI can’t read JSON with any spaces
   108  	b, err := json.Marshal(testMatrices)
   109  	if err != nil {
   110  		panic(fmt.Errorf("failed to marshal test matrices json: %w", err))
   111  	}
   112  	// this string will be read by CI to generate groups of tests to run in separate CI jobs
   113  	testMatrixStr := "::set-output name=" + ciMatrixName + "::" + string(b)
   114  	// very important to add newline character at the end of the compacted JSON - otherwise fromJSON() in CI will throw unmarshalling error
   115  	fmt.Println(testMatrixStr)
   116  }
   117  
   118  // buildTestMatrices builds the test matrices.
   119  func buildTestMatrices(packageConfig *config, flowPackages func(dir string) []*packages.Package) []*testMatrix {
   120  	testMatrices := make([]*testMatrix, 0)
   121  	seenPaths := make(map[string]struct{})
   122  	seenPath := func(p string) {
   123  		seenPaths[p] = struct{}{}
   124  	}
   125  	seen := func(p string) bool {
   126  		_, seen := seenPaths[p]
   127  		return seen
   128  	}
   129  
   130  	for _, topLevelPkg := range packageConfig.Packages {
   131  		allPackages := flowPackages(topLevelPkg.Name)
   132  		// first build test matrix for each of the subpackages and mark all complete paths seen
   133  		subPkgMatrices := processSubpackages(topLevelPkg.Subpackages, allPackages, seenPath)
   134  		testMatrices = append(testMatrices, subPkgMatrices...)
   135  		// now build top level test matrix
   136  		topLevelTestMatrix := processTopLevelPackage(topLevelPkg, allPackages, seenPath, seen)
   137  		testMatrices = append(testMatrices, topLevelTestMatrix)
   138  	}
   139  
   140  	// any packages left out of the explicit Packages field will be run together as "others" from the config PackagesPath
   141  	if packageConfig.IncludeOthers {
   142  		allPkgs := flowPackages(packageConfig.PackagesPath)
   143  		if othersTestMatrix := buildOthersTestMatrix(allPkgs, seen); othersTestMatrix != nil {
   144  			testMatrices = append(testMatrices, othersTestMatrix)
   145  		}
   146  	}
   147  	return testMatrices
   148  }
   149  
   150  // processSubpackages creates a test matrix for all subpackages provided.
   151  func processSubpackages(subPkgs []*subpackage, allPkgs []*packages.Package, seenPath func(p string)) []*testMatrix {
   152  	testMatrices := make([]*testMatrix, 0)
   153  	for _, subPkg := range subPkgs {
   154  		pkgPath := fullGoPackagePath(subPkg.Name)
   155  		// this is the list of allPackages that used with the go test command
   156  		var testPkgStrBuilder strings.Builder
   157  		for _, p := range allPkgs {
   158  			if strings.HasPrefix(p.PkgPath, pkgPath) {
   159  				testPkgStrBuilder.WriteString(fmt.Sprintf("%s ", p.PkgPath))
   160  				seenPath(p.PkgPath)
   161  			}
   162  		}
   163  		testMatrices = append(testMatrices, newTestMatrix(subPkg.Name, subPkg.Runner, testPkgStrBuilder.String()))
   164  	}
   165  	return testMatrices
   166  }
   167  
   168  // processTopLevelPackages creates test matrix for the top level package excluding any packages from the exclude list.
   169  func processTopLevelPackage(pkg *flowGoPackage, allPkgs []*packages.Package, seenPath func(p string), seen func(p string) bool) *testMatrix {
   170  	var topLevelTestPkgStrBuilder strings.Builder
   171  	for _, p := range allPkgs {
   172  		if !seen(p.PkgPath) {
   173  			includePkg := true
   174  			for _, exclude := range pkg.Exclude {
   175  				if strings.HasPrefix(p.PkgPath, fullGoPackagePath(exclude)) {
   176  					includePkg = false
   177  				}
   178  			}
   179  
   180  			if includePkg && strings.HasPrefix(p.PkgPath, fullGoPackagePath(pkg.Name)) {
   181  				topLevelTestPkgStrBuilder.WriteString(fmt.Sprintf("%s ", p.PkgPath))
   182  				seenPath(p.PkgPath)
   183  			}
   184  		}
   185  	}
   186  	return newTestMatrix(pkg.Name, pkg.Runner, topLevelTestPkgStrBuilder.String())
   187  }
   188  
   189  // buildOthersTestMatrix builds an others test matrix that includes all packages in a path not explicitly set in the packages list of a config.
   190  func buildOthersTestMatrix(allPkgs []*packages.Package, seen func(p string) bool) *testMatrix {
   191  	var othersTestPkgStrBuilder strings.Builder
   192  	for _, otherPkg := range allPkgs {
   193  		if !seen(otherPkg.PkgPath) {
   194  			othersTestPkgStrBuilder.WriteString(fmt.Sprintf("%s ", otherPkg.PkgPath))
   195  		}
   196  	}
   197  
   198  	if othersTestPkgStrBuilder.Len() > 0 {
   199  		return newTestMatrix("others", "", othersTestPkgStrBuilder.String())
   200  	}
   201  
   202  	return nil
   203  }
   204  
   205  func listAllFlowPackages(dir string) []*packages.Package {
   206  	flowPackages, err := packages.Load(&packages.Config{Dir: dir}, "./...")
   207  	if err != nil {
   208  		panic(err)
   209  	}
   210  	return flowPackages
   211  }
   212  
   213  func loadPackagesConfig(configFile string) *config {
   214  	var packageConfig config
   215  	buf := bytes.NewBufferString(configFile)
   216  	err := json.NewDecoder(buf).Decode(&packageConfig)
   217  	if err != nil {
   218  		panic(fmt.Errorf("failed to decode package config json %w: %s", err, configFile))
   219  	}
   220  	return &packageConfig
   221  }
   222  
   223  func fullGoPackagePath(pkg string) string {
   224  	return fmt.Sprintf("%s%s", flowPackagePrefix, pkg)
   225  }
   226  
   227  func init() {
   228  	// Add flags to the FlagSet
   229  	pflag.StringVarP(&matrixConfigFile, "config", "c", "", "the config file used to generate the test matrix")
   230  }