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 }