| package updater |
| |
| import ( |
| "context" |
| "fmt" |
| "path/filepath" |
| |
| "github.com/bazelbuild/buildtools/build" |
| "github.com/bazelbuild/buildtools/edit" |
| "github.com/bazelbuild/rules_typescript/ts_auto_deps/platform" |
| ) |
| |
| // isAllTestLibrary identifies testonly ts_libraries named "all_tests". Taze |
| // will register tests with these rules instead of |
| // ts_config/ts_development_sources rules to allow users to set up their builds |
| // differently. |
| func isAllTestLibrary(bld *build.File, r *build.Rule) bool { |
| if !ruleMatches(bld, r, "ts_library", ruleTypeTest) { |
| return false |
| } |
| |
| if r.Name() != "all_tests" { |
| return false |
| } |
| |
| return true |
| } |
| |
| func getAllTestLibraries(bld *build.File) []*build.Rule { |
| var allTestRules []*build.Rule |
| for _, r := range buildRules(bld, "ts_library") { |
| if isAllTestLibrary(bld, r) { |
| allTestRules = append(allTestRules, r) |
| } |
| } |
| return allTestRules |
| } |
| |
| // RegisterTestRules registers ts_library test targets with the project's |
| // ts_config and ts_development_sources rules. It may also register the tests |
| // with a testonly ts_library named "all_tests", which allows users to set up |
| // their own BUILD layout. It's separated from UpdateBUILD since it's |
| // non-local, multiple packages may all need to make writes to the same |
| // ts_config. It returns a set of the paths for the packages that were updated. |
| func (upd *Updater) RegisterTestRules(ctx context.Context, paths ...string) (bool, map[string]bool, error) { |
| reg := &buildRegistry{make(map[string]*build.File), make(map[*build.File]bool)} |
| var g3root string |
| updatedAncestorPackages := make(map[string]bool) |
| for _, path := range paths { |
| // declare variables manually so that g3root doesn't get overwritten by a := |
| // declaration |
| var err error |
| var buildPath string |
| g3root, buildPath, _, err = getBUILDPath(ctx, path) |
| if err != nil { |
| return false, nil, err |
| } |
| bld, err := reg.readBUILD(ctx, g3root, buildPath) |
| if err != nil { |
| return false, nil, err |
| } |
| for _, tr := range getRules(bld, "ts_library", ruleTypeTest) { |
| // don't register all_test libraries themselves |
| if isAllTestLibrary(bld, tr) { |
| continue |
| } |
| platform.Infof("Registering test rule in closest ts_config & ts_development_sources") |
| target := AbsoluteBazelTarget(bld, tr.Name()) |
| ancestorBuild, err := reg.registerTestRule(ctx, bld, tsConfig, g3root, target) |
| if err != nil { |
| return false, nil, err |
| } |
| if ancestorBuild != "" { |
| updatedAncestorPackages[ancestorBuild] = true |
| } |
| // NodeJS rules should not be added to ts_development_sources automatically, because |
| // they typically do not run in the browser. |
| if tr.AttrString("runtime") != "nodejs" { |
| ancestorBuild, err := reg.registerTestRule(ctx, bld, tsDevSrcs, g3root, target) |
| if err != nil { |
| return false, nil, err |
| } |
| if ancestorBuild != "" { |
| updatedAncestorPackages[ancestorBuild] = true |
| } |
| } |
| } |
| } |
| |
| updated := false |
| for b := range reg.filesToUpdate { |
| fmt.Printf("Registered test(s) in %s\n", b.Path) |
| fileChanged, err := upd.maybeWriteBUILD(ctx, filepath.Join(g3root, b.Path), b) |
| if err != nil { |
| return false, nil, err |
| } |
| updated = updated || fileChanged |
| } |
| |
| return updated, updatedAncestorPackages, nil |
| } |
| |
| // buildRegistry buffers reads and writes done while registering ts_libraries |
| // with ts_config and ts_development_sources rules, so that registers from |
| // multiple packages all get applied at once. |
| type buildRegistry struct { |
| bldFiles map[string]*build.File |
| filesToUpdate map[*build.File]bool |
| } |
| |
| func (reg *buildRegistry) readBUILD(ctx context.Context, workspaceRoot, buildFilePath string) (*build.File, error) { |
| normalizedG3Path, err := getWorkspaceRelativePath(workspaceRoot, buildFilePath) |
| if err != nil { |
| return nil, err |
| } |
| |
| if bld, ok := reg.bldFiles[normalizedG3Path]; ok { |
| return bld, nil |
| } |
| |
| bld, err := readBUILD(ctx, buildFilePath, normalizedG3Path) |
| if err != nil { |
| return nil, err |
| } |
| if bld == nil { |
| // The BUILD file didn't exist, so create a new, empty one. |
| bld = &build.File{Path: normalizedG3Path, Type: build.TypeBuild} |
| } |
| |
| reg.bldFiles[normalizedG3Path] = bld |
| |
| return bld, nil |
| } |
| |
| func (reg *buildRegistry) registerForPossibleUpdate(bld *build.File) { |
| reg.filesToUpdate[bld] = true |
| } |
| |
| type registerTarget int |
| |
| const ( |
| tsConfig registerTarget = iota |
| tsDevSrcs |
| ) |
| |
| func (rt registerTarget) kind() string { |
| if rt == tsConfig { |
| return "ts_config" |
| } |
| |
| return "ts_development_sources" |
| } |
| |
| func (rt registerTarget) ruleType() ruleType { |
| if rt == tsConfig { |
| return ruleTypeAny |
| } |
| |
| return ruleTypeTest |
| } |
| |
| // registerTestRule searches ancestor packages for a rule matching the register |
| // target and adds the given target to it. If an all_tests library is found, the |
| // rule is registered with it, instead of specified register target. Prints a |
| // warning if no rule is found, but only returns an error if adding the |
| // dependency fails. |
| func (reg *buildRegistry) registerTestRule(ctx context.Context, bld *build.File, rt registerTarget, g3root, target string) (string, error) { |
| if buildHasDisableTaze(bld) { |
| return "", nil |
| } |
| |
| var ruleToRegister *build.Rule |
| for _, r := range bld.Rules("") { |
| if isAllTestLibrary(bld, r) { |
| if hasDependency(bld, r, target) { |
| return "", nil |
| } |
| |
| // an all_tests library takes presidence over a registerTarget, and there |
| // can only be one, since there can only be one rule with a given name, so |
| // can just break after finding |
| ruleToRegister = r |
| break |
| } |
| if ruleMatches(bld, r, rt.kind(), rt.ruleType()) { |
| if hasDependency(bld, r, target) { |
| return "", nil |
| } |
| |
| // keep overwriting ruleToRegister so the last match in the BUILD gets |
| // used |
| ruleToRegister = r |
| } |
| } |
| |
| if ruleToRegister != nil { |
| addDep(bld, ruleToRegister, target) |
| reg.registerForPossibleUpdate(bld) |
| return filepath.Dir(bld.Path), nil |
| } |
| |
| parentDir := filepath.Dir(filepath.Dir(bld.Path)) |
| for parentDir != "." && parentDir != "/" { |
| buildFile := filepath.Join(g3root, parentDir, "BUILD") |
| if _, err := platform.Stat(ctx, buildFile); err == nil { |
| parent, err := reg.readBUILD(ctx, g3root, buildFile) |
| if err != nil { |
| return "", err |
| } |
| return reg.registerTestRule(ctx, parent, rt, g3root, target) |
| } |
| parentDir = filepath.Dir(parentDir) |
| } |
| fmt.Printf("WARNING: no %s rule in parent packages of %s to register with.\n", |
| rt.kind(), target) |
| return "", nil |
| } |
| |
| var wellKnownBuildRules = []struct { |
| name string |
| attrName string |
| }{ |
| { |
| name: "karma_polymer_test", |
| attrName: "test_ts_deps", |
| }, |
| { |
| name: "wct_closure_test_suite", |
| attrName: "js_deps", |
| }, |
| { |
| name: "jasmine_node_test", |
| attrName: "deps", |
| }, |
| } |
| |
| // isRegisteredWithAlternateTestRule returns true if the rule is already |
| // registered with a well known test rule, such as karma_polymer_test, |
| // wct_closure_test_suite or jasmine_node_test. |
| func isRegisteredWithAlternateTestRule(bld *build.File, r *build.Rule, dep string) bool { |
| pkg := filepath.Dir(bld.Path) |
| for _, wkbr := range wellKnownBuildRules { |
| if isKind(r, wkbr.name) { |
| testTsDeps := r.Attr(wkbr.attrName) |
| if edit.ListFind(testTsDeps, dep, pkg) != nil { |
| return true |
| } |
| } |
| } |
| return false |
| } |