Files
build_soong/android/bazel_handler_test.go
Cole Faust a20d947329 Initial implementation of the bazel sandwich
The "bazel sandwich" is a mechanism for bazel to depend on make/soong
outputs. The name comes from the fact that bazel is now at the top
and bottom of the build graph. This is intended to allow us to work
on converting the partition builds to bazel while not all of the
dependencies of the partition have been converted.

It works by adding the bazel_sandwich_import_file rule, which emits a
dangling symlink that starts with bazel_sandwich:, and includes
information that the aquery handler in soong reads. The aquery handler
rewrites the symlink so that it points to a file generated by
make/soong, and adds a ninja dependency from the symlink to the file
it's targeting.

This allows us to depend on make-built files from bazel, but notably
it doesn't allow us to depend on analysis-time information from make.
This shouldn't be a problem for the partitions, but limits the use of
the bazel sandwich to similar, less complicated types of builds.

go/roboleaf-bazel-sandwich

Bug: 265127181
Test: m bazel_sandwich
Change-Id: Ic41bae7be0b55f251d04a6a95f846c50ce897adc
2023-07-31 11:53:41 -07:00

427 lines
14 KiB
Go

package android
import (
"encoding/json"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"android/soong/bazel"
"android/soong/bazel/cquery"
analysis_v2_proto "prebuilts/bazel/common/proto/analysis_v2"
"github.com/google/blueprint/metrics"
"google.golang.org/protobuf/proto"
)
var testConfig = TestConfig("out", nil, "", nil)
type testInvokeBazelContext struct{}
type mockBazelRunner struct {
testHelper *testing.T
// Stores mock behavior. If an issueBazelCommand request is made for command
// k, and {k:v} is present in this map, then the mock will return v.
bazelCommandResults map[bazelCommand]string
// Requests actually made of the mockBazelRunner with issueBazelCommand,
// keyed by the command they represent.
bazelCommandRequests map[bazelCommand]bazel.CmdRequest
}
func (r *mockBazelRunner) bazelCommandForRequest(cmdRequest bazel.CmdRequest) bazelCommand {
for _, arg := range cmdRequest.Argv {
for _, cmdType := range allBazelCommands {
if arg == cmdType.command {
return cmdType
}
}
}
r.testHelper.Fatalf("Unrecognized bazel request: %s", cmdRequest)
return cqueryCmd
}
func (r *mockBazelRunner) issueBazelCommand(cmdRequest bazel.CmdRequest, paths *bazelPaths, eventHandler *metrics.EventHandler) (string, string, error) {
command := r.bazelCommandForRequest(cmdRequest)
r.bazelCommandRequests[command] = cmdRequest
return r.bazelCommandResults[command], "", nil
}
func (t *testInvokeBazelContext) GetEventHandler() *metrics.EventHandler {
return &metrics.EventHandler{}
}
func TestRequestResultsAfterInvokeBazel(t *testing.T) {
label_foo := "@//foo:foo"
label_bar := "@//foo:bar"
apexKey := ApexConfigKey{
WithinApex: true,
ApexSdkVersion: "29",
ApiDomain: "myapex",
}
cfg_foo := configKey{"arm64_armv8-a", Android, apexKey}
cfg_bar := configKey{arch: "arm64_armv8-a", osType: Android}
cmd_results := []string{
`@//foo:foo|arm64_armv8-a|android|within_apex|29|myapex>>out/foo/foo.txt`,
`@//foo:bar|arm64_armv8-a|android>>out/foo/bar.txt`,
}
bazelContext, _ := testBazelContext(t, map[bazelCommand]string{cqueryCmd: strings.Join(cmd_results, "\n")})
bazelContext.QueueBazelRequest(label_foo, cquery.GetOutputFiles, cfg_foo)
bazelContext.QueueBazelRequest(label_bar, cquery.GetOutputFiles, cfg_bar)
err := bazelContext.InvokeBazel(testConfig, &testInvokeBazelContext{})
if err != nil {
t.Fatalf("Did not expect error invoking Bazel, but got %s", err)
}
verifyCqueryResult(t, bazelContext, label_foo, cfg_foo, "out/foo/foo.txt")
verifyCqueryResult(t, bazelContext, label_bar, cfg_bar, "out/foo/bar.txt")
}
func verifyCqueryResult(t *testing.T, ctx *mixedBuildBazelContext, label string, cfg configKey, result string) {
g, err := ctx.GetOutputFiles(label, cfg)
if err != nil {
t.Errorf("Expected cquery results after running InvokeBazel(), but got err %v", err)
} else if w := []string{result}; !reflect.DeepEqual(w, g) {
t.Errorf("Expected output %s, got %s", w, g)
}
}
func TestInvokeBazelWritesBazelFiles(t *testing.T) {
bazelContext, baseDir := testBazelContext(t, map[bazelCommand]string{})
err := bazelContext.InvokeBazel(testConfig, &testInvokeBazelContext{})
if err != nil {
t.Fatalf("Did not expect error invoking Bazel, but got %s", err)
}
if _, err := os.Stat(filepath.Join(baseDir, "soong_injection", "mixed_builds", "main.bzl")); os.IsNotExist(err) {
t.Errorf("Expected main.bzl to exist, but it does not")
} else if err != nil {
t.Errorf("Unexpected error stating main.bzl %s", err)
}
if _, err := os.Stat(filepath.Join(baseDir, "soong_injection", "mixed_builds", "BUILD.bazel")); os.IsNotExist(err) {
t.Errorf("Expected BUILD.bazel to exist, but it does not")
} else if err != nil {
t.Errorf("Unexpected error stating BUILD.bazel %s", err)
}
if _, err := os.Stat(filepath.Join(baseDir, "soong_injection", "WORKSPACE.bazel")); os.IsNotExist(err) {
t.Errorf("Expected WORKSPACE.bazel to exist, but it does not")
} else if err != nil {
t.Errorf("Unexpected error stating WORKSPACE.bazel %s", err)
}
}
func TestInvokeBazelPopulatesBuildStatements(t *testing.T) {
type testCase struct {
input string
command string
}
var testCases = []testCase{
{`
{
"artifacts": [
{ "id": 1, "path_fragment_id": 1 },
{ "id": 2, "path_fragment_id": 2 }],
"actions": [{
"target_Id": 1,
"action_Key": "x",
"mnemonic": "x",
"arguments": ["touch", "foo"],
"input_dep_set_ids": [1],
"output_Ids": [1],
"primary_output_id": 1
}],
"dep_set_of_files": [
{ "id": 1, "direct_artifact_ids": [1, 2] }],
"path_fragments": [
{ "id": 1, "label": "one" },
{ "id": 2, "label": "two" }]
}`,
"cd 'test/exec_root' && rm -rf 'one' && touch foo",
}, {`
{
"artifacts": [
{ "id": 1, "path_fragment_id": 10 },
{ "id": 2, "path_fragment_id": 20 }],
"actions": [{
"target_Id": 100,
"action_Key": "x",
"mnemonic": "x",
"arguments": ["bogus", "command"],
"output_Ids": [1, 2],
"primary_output_id": 1
}],
"path_fragments": [
{ "id": 10, "label": "one", "parent_id": 30 },
{ "id": 20, "label": "one.d", "parent_id": 30 },
{ "id": 30, "label": "parent" }]
}`,
`cd 'test/exec_root' && rm -rf 'parent/one' && bogus command && sed -i'' -E 's@(^|\s|")bazel-out/@\1test/bazel_out/@g' 'parent/one.d'`,
},
}
for i, testCase := range testCases {
data, err := JsonToActionGraphContainer(testCase.input)
if err != nil {
t.Error(err)
}
bazelContext, _ := testBazelContext(t, map[bazelCommand]string{aqueryCmd: string(data)})
err = bazelContext.InvokeBazel(testConfig, &testInvokeBazelContext{})
if err != nil {
t.Fatalf("testCase #%d: did not expect error invoking Bazel, but got %s", i+1, err)
}
got := bazelContext.BuildStatementsToRegister()
if want := 1; len(got) != want {
t.Fatalf("expected %d registered build statements, but got %#v", want, got)
}
cmd := RuleBuilderCommand{}
ctx := builderContextForTests{PathContextForTesting(TestConfig("out", nil, "", nil))}
createCommand(&cmd, got[0], "test/exec_root", "test/bazel_out", ctx, map[string]bazel.AqueryDepset{}, "")
if actual, expected := cmd.buf.String(), testCase.command; expected != actual {
t.Errorf("expected: [%s], actual: [%s]", expected, actual)
}
}
}
func TestMixedBuildSandboxedAction(t *testing.T) {
input := `{
"artifacts": [
{ "id": 1, "path_fragment_id": 1 },
{ "id": 2, "path_fragment_id": 2 }],
"actions": [{
"target_Id": 1,
"action_Key": "x",
"mnemonic": "x",
"arguments": ["touch", "foo"],
"input_dep_set_ids": [1],
"output_Ids": [1],
"primary_output_id": 1
}],
"dep_set_of_files": [
{ "id": 1, "direct_artifact_ids": [1, 2] }],
"path_fragments": [
{ "id": 1, "label": "one" },
{ "id": 2, "label": "two" }]
}`
data, err := JsonToActionGraphContainer(input)
if err != nil {
t.Error(err)
}
bazelContext, _ := testBazelContext(t, map[bazelCommand]string{aqueryCmd: string(data)})
err = bazelContext.InvokeBazel(testConfig, &testInvokeBazelContext{})
if err != nil {
t.Fatalf("TestMixedBuildSandboxedAction did not expect error invoking Bazel, but got %s", err)
}
statement := bazelContext.BuildStatementsToRegister()[0]
statement.ShouldRunInSbox = true
cmd := RuleBuilderCommand{}
ctx := builderContextForTests{PathContextForTesting(TestConfig("out", nil, "", nil))}
createCommand(&cmd, statement, "test/exec_root", "test/bazel_out", ctx, map[string]bazel.AqueryDepset{}, "")
// Assert that the output is generated in an intermediate directory
// fe05bcdcdc4928012781a5f1a2a77cbb5398e106 is the sha1 checksum of "one"
if actual, expected := cmd.outputs[0].String(), "out/soong/mixed_build_sbox_intermediates/fe05bcdcdc4928012781a5f1a2a77cbb5398e106/test/exec_root/one"; expected != actual {
t.Errorf("expected: [%s], actual: [%s]", expected, actual)
}
// Assert the actual command remains unchanged inside the sandbox
if actual, expected := cmd.buf.String(), "mkdir -p 'test/exec_root' && cd 'test/exec_root' && rm -rf 'one' && touch foo"; expected != actual {
t.Errorf("expected: [%s], actual: [%s]", expected, actual)
}
}
func TestCoverageFlagsAfterInvokeBazel(t *testing.T) {
testConfig.productVariables.ClangCoverage = boolPtr(true)
testConfig.productVariables.NativeCoveragePaths = []string{"foo1", "foo2"}
testConfig.productVariables.NativeCoverageExcludePaths = []string{"bar1", "bar2"}
verifyAqueryContainsFlags(t, testConfig, "--collect_code_coverage", "--instrumentation_filter=+foo1,+foo2,-bar1,-bar2")
testConfig.productVariables.NativeCoveragePaths = []string{"foo1"}
testConfig.productVariables.NativeCoverageExcludePaths = []string{"bar1"}
verifyAqueryContainsFlags(t, testConfig, "--collect_code_coverage", "--instrumentation_filter=+foo1,-bar1")
testConfig.productVariables.NativeCoveragePaths = []string{"foo1"}
testConfig.productVariables.NativeCoverageExcludePaths = nil
verifyAqueryContainsFlags(t, testConfig, "--collect_code_coverage", "--instrumentation_filter=+foo1")
testConfig.productVariables.NativeCoveragePaths = nil
testConfig.productVariables.NativeCoverageExcludePaths = []string{"bar1"}
verifyAqueryContainsFlags(t, testConfig, "--collect_code_coverage", "--instrumentation_filter=-bar1")
testConfig.productVariables.NativeCoveragePaths = []string{"*"}
testConfig.productVariables.NativeCoverageExcludePaths = nil
verifyAqueryContainsFlags(t, testConfig, "--collect_code_coverage", "--instrumentation_filter=+.*")
testConfig.productVariables.ClangCoverage = boolPtr(false)
verifyAqueryDoesNotContainSubstrings(t, testConfig, "collect_code_coverage", "instrumentation_filter")
}
func TestBazelRequestsSorted(t *testing.T) {
bazelContext, _ := testBazelContext(t, map[bazelCommand]string{})
cfgKeyArm64Android := configKey{arch: "arm64_armv8-a", osType: Android}
cfgKeyArm64Linux := configKey{arch: "arm64_armv8-a", osType: Linux}
cfgKeyOtherAndroid := configKey{arch: "otherarch", osType: Android}
bazelContext.QueueBazelRequest("zzz", cquery.GetOutputFiles, cfgKeyArm64Android)
bazelContext.QueueBazelRequest("ccc", cquery.GetApexInfo, cfgKeyArm64Android)
bazelContext.QueueBazelRequest("duplicate", cquery.GetOutputFiles, cfgKeyArm64Android)
bazelContext.QueueBazelRequest("duplicate", cquery.GetOutputFiles, cfgKeyArm64Android)
bazelContext.QueueBazelRequest("xxx", cquery.GetOutputFiles, cfgKeyArm64Linux)
bazelContext.QueueBazelRequest("aaa", cquery.GetOutputFiles, cfgKeyArm64Android)
bazelContext.QueueBazelRequest("aaa", cquery.GetOutputFiles, cfgKeyOtherAndroid)
bazelContext.QueueBazelRequest("bbb", cquery.GetOutputFiles, cfgKeyOtherAndroid)
if len(bazelContext.requests) != 7 {
t.Error("Expected 7 request elements, but got", len(bazelContext.requests))
}
lastString := ""
for _, val := range bazelContext.requests {
thisString := val.String()
if thisString <= lastString {
t.Errorf("Requests are not ordered correctly. '%s' came before '%s'", lastString, thisString)
}
lastString = thisString
}
}
func TestIsModuleNameAllowed(t *testing.T) {
libDisabled := "lib_disabled"
libEnabled := "lib_enabled"
libDclaWithinApex := "lib_dcla_within_apex"
libDclaNonApex := "lib_dcla_non_apex"
libNotConverted := "lib_not_converted"
disabledModules := map[string]bool{
libDisabled: true,
}
enabledModules := map[string]bool{
libEnabled: true,
}
dclaEnabledModules := map[string]bool{
libDclaWithinApex: true,
libDclaNonApex: true,
}
bazelContext := &mixedBuildBazelContext{
bazelEnabledModules: enabledModules,
bazelDisabledModules: disabledModules,
bazelDclaEnabledModules: dclaEnabledModules,
}
if bazelContext.IsModuleNameAllowed(libDisabled, true) {
t.Fatalf("%s shouldn't be allowed for mixed build", libDisabled)
}
if !bazelContext.IsModuleNameAllowed(libEnabled, true) {
t.Fatalf("%s should be allowed for mixed build", libEnabled)
}
if !bazelContext.IsModuleNameAllowed(libDclaWithinApex, true) {
t.Fatalf("%s should be allowed for mixed build", libDclaWithinApex)
}
if bazelContext.IsModuleNameAllowed(libDclaNonApex, false) {
t.Fatalf("%s shouldn't be allowed for mixed build", libDclaNonApex)
}
if bazelContext.IsModuleNameAllowed(libNotConverted, true) {
t.Fatalf("%s shouldn't be allowed for mixed build", libNotConverted)
}
}
func verifyAqueryContainsFlags(t *testing.T, config Config, expected ...string) {
t.Helper()
bazelContext, _ := testBazelContext(t, map[bazelCommand]string{})
err := bazelContext.InvokeBazel(config, &testInvokeBazelContext{})
if err != nil {
t.Fatalf("Did not expect error invoking Bazel, but got %s", err)
}
sliceContains := func(slice []string, x string) bool {
for _, s := range slice {
if s == x {
return true
}
}
return false
}
aqueryArgv := bazelContext.bazelRunner.(*mockBazelRunner).bazelCommandRequests[aqueryCmd].Argv
for _, expectedFlag := range expected {
if !sliceContains(aqueryArgv, expectedFlag) {
t.Errorf("aquery does not contain expected flag %#v. Argv was: %#v", expectedFlag, aqueryArgv)
}
}
}
func verifyAqueryDoesNotContainSubstrings(t *testing.T, config Config, substrings ...string) {
t.Helper()
bazelContext, _ := testBazelContext(t, map[bazelCommand]string{})
err := bazelContext.InvokeBazel(config, &testInvokeBazelContext{})
if err != nil {
t.Fatalf("Did not expect error invoking Bazel, but got %s", err)
}
sliceContainsSubstring := func(slice []string, substring string) bool {
for _, s := range slice {
if strings.Contains(s, substring) {
return true
}
}
return false
}
aqueryArgv := bazelContext.bazelRunner.(*mockBazelRunner).bazelCommandRequests[aqueryCmd].Argv
for _, substring := range substrings {
if sliceContainsSubstring(aqueryArgv, substring) {
t.Errorf("aquery contains unexpected substring %#v. Argv was: %#v", substring, aqueryArgv)
}
}
}
func testBazelContext(t *testing.T, bazelCommandResults map[bazelCommand]string) (*mixedBuildBazelContext, string) {
t.Helper()
p := bazelPaths{
soongOutDir: t.TempDir(),
outputBase: "outputbase",
workspaceDir: "workspace_dir",
}
if _, exists := bazelCommandResults[aqueryCmd]; !exists {
bazelCommandResults[aqueryCmd] = ""
}
runner := &mockBazelRunner{
testHelper: t,
bazelCommandResults: bazelCommandResults,
bazelCommandRequests: map[bazelCommand]bazel.CmdRequest{},
}
return &mixedBuildBazelContext{
bazelRunner: runner,
paths: &p,
}, p.soongOutDir
}
// Transform the json format to ActionGraphContainer
func JsonToActionGraphContainer(inputString string) ([]byte, error) {
var aqueryProtoResult analysis_v2_proto.ActionGraphContainer
err := json.Unmarshal([]byte(inputString), &aqueryProtoResult)
if err != nil {
return []byte(""), err
}
data, _ := proto.Marshal(&aqueryProtoResult)
return data, err
}