blob: 0f51e63b48f779b5ffa13f97548385cac9eeba1d [file] [log] [blame]
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",
},
{
name: "karma_web_test_suite",
attrName: "deps",
},
{
name: "boq_jswire_test_library",
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
}