Merge "Handle license conversions in androidmk"
diff --git a/android/androidmk.go b/android/androidmk.go
index 9025a74..0adc2a6 100644
--- a/android/androidmk.go
+++ b/android/androidmk.go
@@ -114,7 +114,7 @@
 	// If true, the module is skipped and does not appear on the final Android-<product name>.mk
 	// file. Useful when a module needs to be skipped conditionally.
 	Disabled bool
-	// The postprocessing mk file to include, e.g. $(BUILD_SYSTEM)/soong_cc_prebuilt.mk
+	// The postprocessing mk file to include, e.g. $(BUILD_SYSTEM)/soong_cc_rust_prebuilt.mk
 	// If not set, $(BUILD_SYSTEM)/prebuilt.mk is used.
 	Include string
 	// Required modules that need to be built and included in the final build output when building
diff --git a/android/bazel.go b/android/bazel.go
index 32963b3..4f613df 100644
--- a/android/bazel.go
+++ b/android/bazel.go
@@ -32,6 +32,10 @@
 	Bazel_module bazel.BazelModuleProperties
 }
 
+// namespacedVariableProperties is a map from a string representing a Soong
+// config variable namespace, like "android" or "vendor_name" to a struct
+// pointer representing the soong_config_variables property of a module created
+// by a soong_config_module_type or soong_config_module_type_import.
 type namespacedVariableProperties map[string]interface{}
 
 // BazelModuleBase contains the property structs with metadata for modules which can be converted to
@@ -328,12 +332,8 @@
 	// Per-module denylist to opt modules out of mixed builds. Such modules will
 	// still be generated via bp2build.
 	mixedBuildsDisabledList = []string{
-		"libbrotli",                            // http://b/198585397, ld.lld: error: bionic/libc/arch-arm64/generic/bionic/memmove.S:95:(.text+0x10): relocation R_AARCH64_CONDBR19 out of range: -1404176 is not in [-1048576, 1048575]; references __memcpy
-		"func_to_syscall_nrs",                  // http://b/200899432, bazel-built cc_genrule does not work in mixed build when it is a dependency of another soong module.
-		"libseccomp_policy_app_zygote_sources", // http://b/200899432, bazel-built cc_genrule does not work in mixed build when it is a dependency of another soong module.
-		"libseccomp_policy_app_sources",        // http://b/200899432, bazel-built cc_genrule does not work in mixed build when it is a dependency of another soong module.
-		"libseccomp_policy_system_sources",     // http://b/200899432, bazel-built cc_genrule does not work in mixed build when it is a dependency of another soong module.
-		"minijail_constants_json",              // http://b/200899432, bazel-built cc_genrule does not work in mixed build when it is a dependency of another soong module.
+		"libbrotli",               // http://b/198585397, ld.lld: error: bionic/libc/arch-arm64/generic/bionic/memmove.S:95:(.text+0x10): relocation R_AARCH64_CONDBR19 out of range: -1404176 is not in [-1048576, 1048575]; references __memcpy
+		"minijail_constants_json", // http://b/200899432, bazel-built cc_genrule does not work in mixed build when it is a dependency of another soong module.
 	}
 
 	// Used for quicker lookups
diff --git a/android/variable.go b/android/variable.go
index 6ad58c3..89cd59e 100644
--- a/android/variable.go
+++ b/android/variable.go
@@ -491,11 +491,11 @@
 type ProductConfigProperty struct {
 	// The name of the product variable, e.g. "safestack", "malloc_not_svelte",
 	// "board"
-	ProductConfigVariable string
+	Name string
 
 	// Namespace of the variable, if this is a soong_config_module_type variable
-	// e.g. "acme", "ANDROID", "vendor_nae"
-	Namespace string // for soong config variables
+	// e.g. "acme", "ANDROID", "vendor_name"
+	Namespace string
 
 	// Unique configuration to identify this product config property (i.e. a
 	// primary key), as just using the product variable name is not sufficient.
@@ -562,9 +562,6 @@
 	// "acme__board__soc_a", "acme__board__soc_b", and
 	// "acme__board__conditions_default"
 	FullConfig string
-
-	// The actual property value: list, bool, string..
-	Property interface{}
 }
 
 func (p *ProductConfigProperty) ConfigurationAxis() bazel.ConfigurationAxis {
@@ -573,14 +570,43 @@
 	} else {
 		// Soong config variables can be uniquely identified by the namespace
 		// (e.g. acme, android) and the product variable name (e.g. board, size)
-		return bazel.ProductVariableConfigurationAxis(p.Namespace + "__" + p.ProductConfigVariable)
+		return bazel.ProductVariableConfigurationAxis(p.Namespace + "__" + p.Name)
 	}
 }
 
-// ProductConfigProperties is a map of property name to a slice of
-// ProductConfigProperty such that all product variable-specific versions of a
-// property are easily accessed together
-type ProductConfigProperties map[string]map[string]ProductConfigProperty
+// SelectKey returns the literal string that represents this variable in a BUILD
+// select statement.
+func (p *ProductConfigProperty) SelectKey() string {
+	if p.Namespace == "" {
+		return strings.ToLower(p.FullConfig)
+	}
+
+	if p.FullConfig == bazel.ConditionsDefaultConfigKey {
+		return bazel.ConditionsDefaultConfigKey
+	}
+
+	value := p.FullConfig
+	if value == p.Name {
+		value = "enabled"
+	}
+	// e.g. acme__feature1__enabled, android__board__soc_a
+	return strings.ToLower(strings.Join([]string{p.Namespace, p.Name, value}, "__"))
+}
+
+// ProductConfigProperties is a map of maps to group property values according
+// their property name and the product config variable they're set under.
+//
+// The outer map key is the name of the property, like "cflags".
+//
+// The inner map key is a ProductConfigProperty, which is a struct of product
+// variable name, namespace, and the "full configuration" of the product
+// variable.
+//
+// e.g. product variable name: board, namespace: acme, full config: vendor_chip_foo
+//
+// The value of the map is the interface{} representing the value of the
+// property, like ["-DDEFINES"] for cflags.
+type ProductConfigProperties map[string]map[ProductConfigProperty]interface{}
 
 // ProductVariableProperties returns a ProductConfigProperties containing only the properties which
 // have been set for the module in the given context.
@@ -630,19 +656,16 @@
 func (p *ProductConfigProperties) AddProductConfigProperty(
 	propertyName, namespace, productVariableName, config string, property interface{}) {
 	if (*p)[propertyName] == nil {
-		(*p)[propertyName] = make(map[string]ProductConfigProperty)
+		(*p)[propertyName] = make(map[ProductConfigProperty]interface{})
 	}
 
-	// Normalize config to be all lowercase. It's the "primary key" of this
-	// unique property value. This can be the conditions_default value of the
-	// product variable as well.
-	config = strings.ToLower(config)
-	(*p)[propertyName][config] = ProductConfigProperty{
-		Namespace:             namespace,           // e.g. acme, android
-		ProductConfigVariable: productVariableName, // e.g. size, feature1, feature2, FEATURE3, board
-		FullConfig:            config,              // e.g. size, feature1-x86, size__conditions_default
-		Property:              property,            // e.g. ["-O3"]
+	productConfigProp := ProductConfigProperty{
+		Namespace:  namespace,           // e.g. acme, android
+		Name:       productVariableName, // e.g. size, feature1, feature2, FEATURE3, board
+		FullConfig: config,              // e.g. size, feature1-x86, size__conditions_default
 	}
+
+	(*p)[propertyName][productConfigProp] = property
 }
 
 var (
@@ -783,8 +806,8 @@
 					if field.Field(k).IsZero() {
 						continue
 					}
-					productVariableValue := proptools.PropertyNameForField(propertyName)
-					config := strings.Join([]string{namespace, productVariableName, productVariableValue}, "__")
+					// config can also be "conditions_default".
+					config := proptools.PropertyNameForField(propertyName)
 					actualPropertyName := field.Type().Field(k).Name
 
 					productConfigProperties.AddProductConfigProperty(
@@ -799,9 +822,6 @@
 				// Not a conditions_default or a struct prop, i.e. regular
 				// product variables, or not a string-typed config var.
 				config := productVariableName + suffix
-				if namespace != "" {
-					config = namespace + "__" + config
-				}
 				productConfigProperties.AddProductConfigProperty(
 					propertyName,
 					namespace,
diff --git a/apex/androidmk.go b/apex/androidmk.go
index 4f37fc3..2091274 100644
--- a/apex/androidmk.go
+++ b/apex/androidmk.go
@@ -272,7 +272,7 @@
 					fmt.Fprintln(w, "LOCAL_PREBUILT_COVERAGE_ARCHIVE :=", ccMod.CoverageOutputFile().String())
 				}
 			}
-			fmt.Fprintln(w, "include $(BUILD_SYSTEM)/soong_cc_prebuilt.mk")
+			fmt.Fprintln(w, "include $(BUILD_SYSTEM)/soong_cc_rust_prebuilt.mk")
 		default:
 			fmt.Fprintln(w, "LOCAL_MODULE_STEM :=", fi.stem())
 			if fi.builtFile == a.manifestPbOut && apexType == flattenedApex {
diff --git a/bazel/aquery.go b/bazel/aquery.go
index 6d96b1c..4475b5e 100644
--- a/bazel/aquery.go
+++ b/bazel/aquery.go
@@ -18,6 +18,7 @@
 	"encoding/json"
 	"fmt"
 	"path/filepath"
+	"regexp"
 	"strings"
 
 	"github.com/google/blueprint/proptools"
@@ -59,6 +60,8 @@
 	InputDepSetIds       []int
 	Mnemonic             string
 	OutputIds            []int
+	TemplateContent      string
+	Substitutions        []KeyValuePair
 }
 
 // actionGraphContainer contains relevant portions of Bazel's aquery proto, ActionGraphContainer.
@@ -100,6 +103,20 @@
 	artifactIdToPath map[int]string
 }
 
+// The tokens should be substituted with the value specified here, instead of the
+// one returned in 'substitutions' of TemplateExpand action.
+var TemplateActionOverriddenTokens = map[string]string{
+	// Uses "python3" for %python_binary% instead of the value returned by aquery
+	// which is "py3wrapper.sh". See removePy3wrapperScript.
+	"%python_binary%": "python3",
+}
+
+// This pattern matches the MANIFEST file created for a py_binary target.
+var manifestFilePattern = regexp.MustCompile(".*/.+\\.runfiles/MANIFEST$")
+
+// The file name of py3wrapper.sh, which is used by py_binary targets.
+var py3wrapperFileName = "/py3wrapper.sh"
+
 func newAqueryHandler(aqueryResult actionGraphContainer) (*aqueryArtifactHandler, error) {
 	pathFragments := map[int]pathFragment{}
 	for _, pathFragment := range aqueryResult.PathFragments {
@@ -163,7 +180,31 @@
 			}
 		}
 	}
-	return inputPaths, nil
+
+	// TODO(b/197135294): Clean up this custom runfiles handling logic when
+	// SourceSymlinkManifest and SymlinkTree actions are supported.
+	filteredInputPaths := filterOutPy3wrapperAndManifestFileFromInputPaths(inputPaths)
+
+	return filteredInputPaths, nil
+}
+
+// See go/python-binary-host-mixed-build for more details.
+// 1) For py3wrapper.sh, there is no action for creating py3wrapper.sh in the aquery output of
+// Bazel py_binary targets, so there is no Ninja build statements generated for creating it.
+// 2) For MANIFEST file, SourceSymlinkManifest action is in aquery output of Bazel py_binary targets,
+// but it doesn't contain sufficient information so no Ninja build statements are generated
+// for creating it.
+// So in mixed build mode, when these two are used as input of some Ninja build statement,
+// since there is no build statement to create them, they should be removed from input paths.
+func filterOutPy3wrapperAndManifestFileFromInputPaths(inputPaths []string) []string {
+	filteredInputPaths := []string{}
+	for _, path := range inputPaths {
+		if strings.HasSuffix(path, py3wrapperFileName) || manifestFilePattern.MatchString(path) {
+			continue
+		}
+		filteredInputPaths = append(filteredInputPaths, path)
+	}
+	return filteredInputPaths
 }
 
 func (a *aqueryArtifactHandler) artifactIdsFromDepsetId(depsetId int) ([]int, error) {
@@ -249,6 +290,45 @@
 			// Use absolute paths, because some soong actions don't play well with relative paths (for example, `cp -d`).
 			buildStatement.Command = fmt.Sprintf("mkdir -p %[1]s && rm -f %[2]s && ln -sf %[3]s %[2]s", outDir, out, in)
 			buildStatement.SymlinkPaths = outputPaths[:]
+		} else if isTemplateExpandAction(actionEntry) && len(actionEntry.Arguments) < 1 {
+			if len(outputPaths) != 1 {
+				return nil, fmt.Errorf("Expect 1 output to template expand action, got: output %q", outputPaths)
+			}
+			expandedTemplateContent := expandTemplateContent(actionEntry)
+			// The expandedTemplateContent is escaped for being used in double quotes and shell unescape,
+			// and the new line characters (\n) are also changed to \\n which avoids some Ninja escape on \n, which might
+			// change \n to space and mess up the format of Python programs.
+			// sed is used to convert \\n back to \n before saving to output file.
+			// See go/python-binary-host-mixed-build for more details.
+			command := fmt.Sprintf(`/bin/bash -c 'echo "%[1]s" | sed "s/\\\\n/\\n/g" > %[2]s && chmod a+x %[2]s'`,
+				escapeCommandlineArgument(expandedTemplateContent), outputPaths[0])
+			buildStatement.Command = command
+		} else if isPythonZipperAction(actionEntry) {
+			if len(inputPaths) < 1 || len(outputPaths) != 1 {
+				return nil, fmt.Errorf("Expect 1+ input and 1 output to python zipper action, got: input %q, output %q", inputPaths, outputPaths)
+			}
+			buildStatement.InputPaths, buildStatement.Command = removePy3wrapperScript(buildStatement)
+			buildStatement.Command = addCommandForPyBinaryRunfilesDir(buildStatement, inputPaths[0], outputPaths[0])
+			// Add the python zip file as input of the corresponding python binary stub script in Ninja build statements.
+			// In Ninja build statements, the outputs of dependents of a python binary have python binary stub script as input,
+			// which is not sufficient without the python zip file from which runfiles directory is created for py_binary.
+			//
+			// The following logic relies on that Bazel aquery output returns actions in the order that
+			// PythonZipper is after TemplateAction of creating Python binary stub script. If later Bazel doesn't return actions
+			// in that order, the following logic might not find the build statement generated for Python binary
+			// stub script and the build might fail. So the check of pyBinaryFound is added to help debug in case later Bazel might change aquery output.
+			// See go/python-binary-host-mixed-build for more details.
+			pythonZipFilePath := outputPaths[0]
+			pyBinaryFound := false
+			for i, _ := range buildStatements {
+				if len(buildStatements[i].OutputPaths) == 1 && buildStatements[i].OutputPaths[0]+".zip" == pythonZipFilePath {
+					buildStatements[i].InputPaths = append(buildStatements[i].InputPaths, pythonZipFilePath)
+					pyBinaryFound = true
+				}
+			}
+			if !pyBinaryFound {
+				return nil, fmt.Errorf("Could not find the correspondinging Python binary stub script of PythonZipper: %q", outputPaths)
+			}
 		} else if len(actionEntry.Arguments) < 1 {
 			return nil, fmt.Errorf("received action with no command: [%v]", buildStatement)
 		}
@@ -258,10 +338,85 @@
 	return buildStatements, nil
 }
 
+// expandTemplateContent substitutes the tokens in a template.
+func expandTemplateContent(actionEntry action) string {
+	replacerString := []string{}
+	for _, pair := range actionEntry.Substitutions {
+		value := pair.Value
+		if val, ok := TemplateActionOverriddenTokens[pair.Key]; ok {
+			value = val
+		}
+		replacerString = append(replacerString, pair.Key, value)
+	}
+	replacer := strings.NewReplacer(replacerString...)
+	return replacer.Replace(actionEntry.TemplateContent)
+}
+
+func escapeCommandlineArgument(str string) string {
+	// \->\\, $->\$, `->\`, "->\", \n->\\n, '->'"'"'
+	replacer := strings.NewReplacer(
+		`\`, `\\`,
+		`$`, `\$`,
+		"`", "\\`",
+		`"`, `\"`,
+		"\n", "\\n",
+		`'`, `'"'"'`,
+	)
+	return replacer.Replace(str)
+}
+
+// removePy3wrapperScript removes py3wrapper.sh from the input paths and command of the action of
+// creating python zip file in mixed build mode. py3wrapper.sh is returned as input by aquery but
+// there is no action returned by aquery for creating it. So in mixed build "python3" is used
+// as the PYTHON_BINARY in python binary stub script, and py3wrapper.sh is not needed and should be
+// removed from input paths and command of creating python zip file.
+// See go/python-binary-host-mixed-build for more details.
+// TODO(b/205879240) remove this after py3wrapper.sh could be created in the mixed build mode.
+func removePy3wrapperScript(bs BuildStatement) (newInputPaths []string, newCommand string) {
+	// Remove from inputs
+	filteredInputPaths := []string{}
+	for _, path := range bs.InputPaths {
+		if !strings.HasSuffix(path, py3wrapperFileName) {
+			filteredInputPaths = append(filteredInputPaths, path)
+		}
+	}
+	newInputPaths = filteredInputPaths
+
+	// Remove from command line
+	var re = regexp.MustCompile(`\S*` + py3wrapperFileName)
+	newCommand = re.ReplaceAllString(bs.Command, "")
+	return
+}
+
+// addCommandForPyBinaryRunfilesDir adds commands creating python binary runfiles directory.
+// runfiles directory is created by using MANIFEST file and MANIFEST file is the output of
+// SourceSymlinkManifest action is in aquery output of Bazel py_binary targets,
+// but since SourceSymlinkManifest doesn't contain sufficient information
+// so MANIFEST file could not be created, which also blocks the creation of runfiles directory.
+// See go/python-binary-host-mixed-build for more details.
+// TODO(b/197135294) create runfiles directory from MANIFEST file once it can be created from SourceSymlinkManifest action.
+func addCommandForPyBinaryRunfilesDir(bs BuildStatement, zipperCommandPath, zipFilePath string) string {
+	// Unzip the zip file, zipFilePath looks like <python_binary>.zip
+	runfilesDirName := zipFilePath[0:len(zipFilePath)-4] + ".runfiles"
+	command := fmt.Sprintf("%s x %s -d %s", zipperCommandPath, zipFilePath, runfilesDirName)
+	// Create a symbolic link in <python_binary>.runfiles/, which is the expected structure
+	// when running the python binary stub script.
+	command += fmt.Sprintf(" && ln -sf runfiles/__main__ %s", runfilesDirName)
+	return bs.Command + " && " + command
+}
+
 func isSymlinkAction(a action) bool {
 	return a.Mnemonic == "Symlink" || a.Mnemonic == "SolibSymlink"
 }
 
+func isTemplateExpandAction(a action) bool {
+	return a.Mnemonic == "TemplateExpand"
+}
+
+func isPythonZipperAction(a action) bool {
+	return a.Mnemonic == "PythonZipper"
+}
+
 func shouldSkipAction(a action) bool {
 	// TODO(b/180945121): Handle complex symlink actions.
 	if a.Mnemonic == "SymlinkTree" || a.Mnemonic == "SourceSymlinkManifest" {
diff --git a/bazel/aquery_test.go b/bazel/aquery_test.go
index 69f1115..ca58606 100644
--- a/bazel/aquery_test.go
+++ b/bazel/aquery_test.go
@@ -1015,6 +1015,355 @@
 	assertError(t, err, `Expect 1 input and 1 output to symlink action, got: input ["file"], output ["symlink" "other_symlink"]`)
 }
 
+func TestTemplateExpandActionSubstitutions(t *testing.T) {
+	const inputString = `
+{
+  "artifacts": [{
+    "id": 1,
+    "pathFragmentId": 1
+  }],
+  "actions": [{
+    "targetId": 1,
+    "actionKey": "x",
+    "mnemonic": "TemplateExpand",
+    "configurationId": 1,
+    "outputIds": [1],
+    "primaryOutputId": 1,
+    "executionPlatform": "//build/bazel/platforms:linux_x86_64",
+    "templateContent": "Test template substitutions: %token1%, %python_binary%",
+    "substitutions": [{
+      "key": "%token1%",
+      "value": "abcd"
+    },{
+      "key": "%python_binary%",
+      "value": "python3"
+    }]
+  }],
+  "pathFragments": [{
+    "id": 1,
+    "label": "template_file"
+  }]
+}`
+
+	actual, err := AqueryBuildStatements([]byte(inputString))
+
+	if err != nil {
+		t.Errorf("Unexpected error %q", err)
+	}
+
+	expectedBuildStatements := []BuildStatement{
+		BuildStatement{
+			Command: "/bin/bash -c 'echo \"Test template substitutions: abcd, python3\" | sed \"s/\\\\\\\\n/\\\\n/g\" > template_file && " +
+				"chmod a+x template_file'",
+			OutputPaths: []string{"template_file"},
+			Mnemonic:    "TemplateExpand",
+		},
+	}
+	assertBuildStatements(t, expectedBuildStatements, actual)
+}
+
+func TestTemplateExpandActionNoOutput(t *testing.T) {
+	const inputString = `
+{
+  "artifacts": [{
+    "id": 1,
+    "pathFragmentId": 1
+  }],
+  "actions": [{
+    "targetId": 1,
+    "actionKey": "x",
+    "mnemonic": "TemplateExpand",
+    "configurationId": 1,
+    "primaryOutputId": 1,
+    "executionPlatform": "//build/bazel/platforms:linux_x86_64",
+    "templateContent": "Test template substitutions: %token1%, %python_binary%",
+    "substitutions": [{
+      "key": "%token1%",
+      "value": "abcd"
+    },{
+      "key": "%python_binary%",
+      "value": "python3"
+    }]
+  }],
+  "pathFragments": [{
+    "id": 1,
+    "label": "template_file"
+  }]
+}`
+
+	_, err := AqueryBuildStatements([]byte(inputString))
+	assertError(t, err, `Expect 1 output to template expand action, got: output []`)
+}
+
+func TestPythonZipperActionSuccess(t *testing.T) {
+	const inputString = `
+{
+  "artifacts": [{
+    "id": 1,
+    "pathFragmentId": 1
+  },{
+    "id": 2,
+    "pathFragmentId": 2
+  },{
+    "id": 3,
+    "pathFragmentId": 3
+  },{
+    "id": 4,
+    "pathFragmentId": 4
+  },{
+    "id": 5,
+    "pathFragmentId": 10
+  },{
+    "id": 10,
+    "pathFragmentId": 20
+  }],
+  "actions": [{
+    "targetId": 1,
+    "actionKey": "x",
+    "mnemonic": "TemplateExpand",
+    "configurationId": 1,
+    "outputIds": [1],
+    "primaryOutputId": 1,
+    "executionPlatform": "//build/bazel/platforms:linux_x86_64",
+    "templateContent": "Test template substitutions: %token1%, %python_binary%",
+    "substitutions": [{
+      "key": "%token1%",
+      "value": "abcd"
+    },{
+      "key": "%python_binary%",
+      "value": "python3"
+    }]
+  },{
+    "targetId": 1,
+    "actionKey": "x",
+    "mnemonic": "PythonZipper",
+    "configurationId": 1,
+    "arguments": ["../bazel_tools/tools/zip/zipper/zipper", "cC", "python_binary.zip", "__main__.py\u003dbazel-out/k8-fastbuild/bin/python_binary.temp", "__init__.py\u003d", "runfiles/__main__/__init__.py\u003d", "runfiles/__main__/python_binary.py\u003dpython_binary.py", "runfiles/bazel_tools/tools/python/py3wrapper.sh\u003dbazel-out/bazel_tools/k8-fastbuild/bin/tools/python/py3wrapper.sh"],
+    "outputIds": [2],
+    "inputDepSetIds": [1],
+    "primaryOutputId": 2
+  }],
+  "depSetOfFiles": [{
+    "id": 1,
+    "directArtifactIds": [4, 3, 5]
+  }],
+  "pathFragments": [{
+    "id": 1,
+    "label": "python_binary"
+  },{
+    "id": 2,
+    "label": "python_binary.zip"
+  },{
+    "id": 3,
+    "label": "python_binary.py"
+  },{
+    "id": 9,
+    "label": ".."
+  }, {
+    "id": 8,
+    "label": "bazel_tools",
+    "parentId": 9
+  }, {
+    "id": 7,
+    "label": "tools",
+    "parentId": 8
+  }, {
+    "id": 6,
+    "label": "zip",
+    "parentId": 7
+  }, {
+    "id": 5,
+    "label": "zipper",
+    "parentId": 6
+  }, {
+    "id": 4,
+    "label": "zipper",
+    "parentId": 5
+  },{
+    "id": 16,
+    "label": "bazel-out"
+  },{
+    "id": 15,
+    "label": "bazel_tools",
+    "parentId": 16
+  }, {
+    "id": 14,
+    "label": "k8-fastbuild",
+    "parentId": 15
+  }, {
+    "id": 13,
+    "label": "bin",
+    "parentId": 14
+  }, {
+    "id": 12,
+    "label": "tools",
+    "parentId": 13
+  }, {
+    "id": 11,
+    "label": "python",
+    "parentId": 12
+  }, {
+    "id": 10,
+    "label": "py3wrapper.sh",
+    "parentId": 11
+  },{
+    "id": 20,
+    "label": "python_binary"
+  }]
+}`
+	actual, err := AqueryBuildStatements([]byte(inputString))
+
+	if err != nil {
+		t.Errorf("Unexpected error %q", err)
+	}
+
+	expectedBuildStatements := []BuildStatement{
+		BuildStatement{
+			Command: "/bin/bash -c 'echo \"Test template substitutions: abcd, python3\" | sed \"s/\\\\\\\\n/\\\\n/g\" > python_binary && " +
+				"chmod a+x python_binary'",
+			InputPaths:  []string{"python_binary.zip"},
+			OutputPaths: []string{"python_binary"},
+			Mnemonic:    "TemplateExpand",
+		},
+		BuildStatement{
+			Command: "../bazel_tools/tools/zip/zipper/zipper cC python_binary.zip __main__.py=bazel-out/k8-fastbuild/bin/python_binary.temp " +
+				"__init__.py= runfiles/__main__/__init__.py= runfiles/__main__/python_binary.py=python_binary.py  && " +
+				"../bazel_tools/tools/zip/zipper/zipper x python_binary.zip -d python_binary.runfiles && ln -sf runfiles/__main__ python_binary.runfiles",
+			InputPaths:  []string{"../bazel_tools/tools/zip/zipper/zipper", "python_binary.py"},
+			OutputPaths: []string{"python_binary.zip"},
+			Mnemonic:    "PythonZipper",
+		},
+	}
+	assertBuildStatements(t, expectedBuildStatements, actual)
+}
+
+func TestPythonZipperActionNoInput(t *testing.T) {
+	const inputString = `
+{
+  "artifacts": [{
+    "id": 1,
+    "pathFragmentId": 1
+  },{
+    "id": 2,
+    "pathFragmentId": 2
+  }],
+  "actions": [{
+    "targetId": 1,
+    "actionKey": "x",
+    "mnemonic": "PythonZipper",
+    "configurationId": 1,
+    "arguments": ["../bazel_tools/tools/zip/zipper/zipper", "cC", "python_binary.zip", "__main__.py\u003dbazel-out/k8-fastbuild/bin/python_binary.temp", "__init__.py\u003d", "runfiles/__main__/__init__.py\u003d", "runfiles/__main__/python_binary.py\u003dpython_binary.py", "runfiles/bazel_tools/tools/python/py3wrapper.sh\u003dbazel-out/bazel_tools/k8-fastbuild/bin/tools/python/py3wrapper.sh"],
+    "outputIds": [2],
+    "primaryOutputId": 2
+  }],
+  "pathFragments": [{
+    "id": 1,
+    "label": "python_binary"
+  },{
+    "id": 2,
+    "label": "python_binary.zip"
+  }]
+}`
+	_, err := AqueryBuildStatements([]byte(inputString))
+	assertError(t, err, `Expect 1+ input and 1 output to python zipper action, got: input [], output ["python_binary.zip"]`)
+}
+
+func TestPythonZipperActionNoOutput(t *testing.T) {
+	const inputString = `
+{
+  "artifacts": [{
+    "id": 1,
+    "pathFragmentId": 1
+  },{
+    "id": 2,
+    "pathFragmentId": 2
+  },{
+    "id": 3,
+    "pathFragmentId": 3
+  },{
+    "id": 4,
+    "pathFragmentId": 4
+  },{
+    "id": 5,
+    "pathFragmentId": 10
+  }],
+  "actions": [{
+    "targetId": 1,
+    "actionKey": "x",
+    "mnemonic": "PythonZipper",
+    "configurationId": 1,
+    "arguments": ["../bazel_tools/tools/zip/zipper/zipper", "cC", "python_binary.zip", "__main__.py\u003dbazel-out/k8-fastbuild/bin/python_binary.temp", "__init__.py\u003d", "runfiles/__main__/__init__.py\u003d", "runfiles/__main__/python_binary.py\u003dpython_binary.py", "runfiles/bazel_tools/tools/python/py3wrapper.sh\u003dbazel-out/bazel_tools/k8-fastbuild/bin/tools/python/py3wrapper.sh"],
+    "inputDepSetIds": [1]
+  }],
+  "depSetOfFiles": [{
+    "id": 1,
+    "directArtifactIds": [4, 3, 5]
+  }],
+  "pathFragments": [{
+    "id": 1,
+    "label": "python_binary"
+  },{
+    "id": 2,
+    "label": "python_binary.zip"
+  },{
+    "id": 3,
+    "label": "python_binary.py"
+  },{
+    "id": 9,
+    "label": ".."
+  }, {
+    "id": 8,
+    "label": "bazel_tools",
+    "parentId": 9
+  }, {
+    "id": 7,
+    "label": "tools",
+    "parentId": 8
+  }, {
+    "id": 6,
+    "label": "zip",
+    "parentId": 7
+  }, {
+    "id": 5,
+    "label": "zipper",
+    "parentId": 6
+  }, {
+    "id": 4,
+    "label": "zipper",
+    "parentId": 5
+  },{
+    "id": 16,
+    "label": "bazel-out"
+  },{
+    "id": 15,
+    "label": "bazel_tools",
+    "parentId": 16
+  }, {
+    "id": 14,
+    "label": "k8-fastbuild",
+    "parentId": 15
+  }, {
+    "id": 13,
+    "label": "bin",
+    "parentId": 14
+  }, {
+    "id": 12,
+    "label": "tools",
+    "parentId": 13
+  }, {
+    "id": 11,
+    "label": "python",
+    "parentId": 12
+  }, {
+    "id": 10,
+    "label": "py3wrapper.sh",
+    "parentId": 11
+  }]
+}`
+	_, err := AqueryBuildStatements([]byte(inputString))
+	assertError(t, err, `Expect 1+ input and 1 output to python zipper action, got: input ["../bazel_tools/tools/zip/zipper/zipper" "python_binary.py"], output []`)
+}
+
 func assertError(t *testing.T, err error, expected string) {
 	t.Helper()
 	if err == nil {
diff --git a/bp2build/android_app_certificate_conversion_test.go b/bp2build/android_app_certificate_conversion_test.go
index 022c687..6a53b00 100644
--- a/bp2build/android_app_certificate_conversion_test.go
+++ b/bp2build/android_app_certificate_conversion_test.go
@@ -42,8 +42,9 @@
         certificate: "chamber_of_secrets_dir",
 }
 `,
-		expectedBazelTargets: []string{`android_app_certificate(
-    name = "com.android.apogee.cert",
-    certificate = "chamber_of_secrets_dir",
-)`}})
+		expectedBazelTargets: []string{
+			makeBazelTarget("android_app_certificate", "com.android.apogee.cert", attrNameToString{
+				"certificate": `"chamber_of_secrets_dir"`,
+			}),
+		}})
 }
diff --git a/bp2build/apex_conversion_test.go b/bp2build/apex_conversion_test.go
index 456f18a..1a23db7 100644
--- a/bp2build/apex_conversion_test.go
+++ b/bp2build/apex_conversion_test.go
@@ -113,29 +113,30 @@
 	],
 }
 `,
-		expectedBazelTargets: []string{`apex(
-    name = "com.android.apogee",
-    android_manifest = "ApogeeAndroidManifest.xml",
-    binaries = [
+		expectedBazelTargets: []string{
+			makeBazelTarget("apex", "com.android.apogee", attrNameToString{
+				"android_manifest": `"ApogeeAndroidManifest.xml"`,
+				"binaries": `[
         "binary_1",
         "binary_2",
-    ],
-    certificate = ":com.android.apogee.certificate",
-    file_contexts = ":com.android.apogee-file_contexts",
-    installable = False,
-    key = ":com.android.apogee.key",
-    manifest = "apogee_manifest.json",
-    min_sdk_version = "29",
-    native_shared_libs = [
+    ]`,
+				"certificate":     `":com.android.apogee.certificate"`,
+				"file_contexts":   `":com.android.apogee-file_contexts"`,
+				"installable":     "False",
+				"key":             `":com.android.apogee.key"`,
+				"manifest":        `"apogee_manifest.json"`,
+				"min_sdk_version": `"29"`,
+				"native_shared_libs": `[
         ":native_shared_lib_1",
         ":native_shared_lib_2",
-    ],
-    prebuilts = [
+    ]`,
+				"prebuilts": `[
         ":pretend_prebuilt_1",
         ":pretend_prebuilt_2",
-    ],
-    updatable = False,
-)`}})
+    ]`,
+				"updatable": "False",
+			}),
+		}})
 }
 
 func TestApexBundleDefaultPropertyValues(t *testing.T) {
@@ -151,10 +152,10 @@
 	manifest: "apogee_manifest.json",
 }
 `,
-		expectedBazelTargets: []string{`apex(
-    name = "com.android.apogee",
-    manifest = "apogee_manifest.json",
-)`}})
+		expectedBazelTargets: []string{makeBazelTarget("apex", "com.android.apogee", attrNameToString{
+			"manifest": `"apogee_manifest.json"`,
+		}),
+		}})
 }
 
 func TestApexBundleHasBazelModuleProps(t *testing.T) {
@@ -171,8 +172,8 @@
 	bazel_module: { bp2build_available: true },
 }
 `,
-		expectedBazelTargets: []string{`apex(
-    name = "apogee",
-    manifest = "manifest.json",
-)`}})
+		expectedBazelTargets: []string{makeBazelTarget("apex", "apogee", attrNameToString{
+			"manifest": `"manifest.json"`,
+		}),
+		}})
 }
diff --git a/bp2build/apex_key_conversion_test.go b/bp2build/apex_key_conversion_test.go
index 8e1aa09..17f79a6 100644
--- a/bp2build/apex_key_conversion_test.go
+++ b/bp2build/apex_key_conversion_test.go
@@ -43,9 +43,9 @@
         private_key: "com.android.apogee.pem",
 }
 `,
-		expectedBazelTargets: []string{`apex_key(
-    name = "com.android.apogee.key",
-    private_key = "com.android.apogee.pem",
-    public_key = "com.android.apogee.avbpubkey",
-)`}})
+		expectedBazelTargets: []string{makeBazelTarget("apex_key", "com.android.apogee.key", attrNameToString{
+			"private_key": `"com.android.apogee.pem"`,
+			"public_key":  `"com.android.apogee.avbpubkey"`,
+		}),
+		}})
 }
diff --git a/bp2build/build_conversion_test.go b/bp2build/build_conversion_test.go
index ee1d862..983604b 100644
--- a/bp2build/build_conversion_test.go
+++ b/bp2build/build_conversion_test.go
@@ -230,32 +230,32 @@
     string_prop: "a",
     bazel_module: { bp2build_available: true },
 }`,
-			expectedBazelTargets: []string{`custom(
-    name = "foo",
-    string_list_prop = [
+			expectedBazelTargets: []string{
+				makeBazelTarget("custom", "foo", attrNameToString{
+					"string_list_prop": `[
         "a",
         "b",
-    ],
-    string_prop = "a",
-)`,
+    ]`,
+					"string_prop": `"a"`,
+				}),
 			},
 		},
 		{
 			description: "control characters",
 			blueprint: `custom {
-	name: "control_characters",
+    name: "foo",
     string_list_prop: ["\t", "\n"],
     string_prop: "a\t\n\r",
     bazel_module: { bp2build_available: true },
 }`,
-			expectedBazelTargets: []string{`custom(
-    name = "control_characters",
-    string_list_prop = [
+			expectedBazelTargets: []string{
+				makeBazelTarget("custom", "foo", attrNameToString{
+					"string_list_prop": `[
         "\t",
         "\n",
-    ],
-    string_prop = "a\t\n\r",
-)`,
+    ]`,
+					"string_prop": `"a\t\n\r"`,
+				}),
 			},
 		},
 		{
@@ -271,14 +271,13 @@
   arch_paths: ["abc"],
   bazel_module: { bp2build_available: true },
 }`,
-			expectedBazelTargets: []string{`custom(
-    name = "dep",
-    arch_paths = ["abc"],
-)`,
-				`custom(
-    name = "has_dep",
-    arch_paths = [":dep"],
-)`,
+			expectedBazelTargets: []string{
+				makeBazelTarget("custom", "dep", attrNameToString{
+					"arch_paths": `["abc"]`,
+				}),
+				makeBazelTarget("custom", "has_dep", attrNameToString{
+					"arch_paths": `[":dep"]`,
+				}),
 			},
 		},
 		{
@@ -311,9 +310,9 @@
     },
     bazel_module: { bp2build_available: true },
 }`,
-			expectedBazelTargets: []string{`custom(
-    name = "arch_paths",
-    arch_paths = select({
+			expectedBazelTargets: []string{
+				makeBazelTarget("custom", "arch_paths", attrNameToString{
+					"arch_paths": `select({
         "//build/bazel/platforms/arch:arm": [
             "arm.txt",
             "lib32.txt",
@@ -368,8 +367,8 @@
             "windows.txt",
         ],
         "//conditions:default": [],
-    }),
-)`,
+    })`,
+				}),
 			},
 		},
 		{
@@ -389,17 +388,16 @@
     arch_paths: ["abc"],
     bazel_module: { bp2build_available: true },
 }`,
-			expectedBazelTargets: []string{`custom(
-    name = "dep",
-    arch_paths = ["abc"],
-)`,
-				`custom(
-    name = "has_dep",
-    arch_paths = select({
+			expectedBazelTargets: []string{
+				makeBazelTarget("custom", "dep", attrNameToString{
+					"arch_paths": `["abc"]`,
+				}),
+				makeBazelTarget("custom", "has_dep", attrNameToString{
+					"arch_paths": `select({
         "//build/bazel/platforms/arch:x86": [":dep"],
         "//conditions:default": [],
-    }),
-)`,
+    })`,
+				}),
 			},
 		},
 		{
@@ -409,10 +407,10 @@
     embedded_prop: "abc",
     bazel_module: { bp2build_available: true },
 }`,
-			expectedBazelTargets: []string{`custom(
-    name = "embedded_props",
-    embedded_attr = "abc",
-)`,
+			expectedBazelTargets: []string{
+				makeBazelTarget("custom", "embedded_props", attrNameToString{
+					"embedded_attr": `"abc"`,
+				}),
 			},
 		},
 		{
@@ -422,10 +420,10 @@
     other_embedded_prop: "abc",
     bazel_module: { bp2build_available: true },
 }`,
-			expectedBazelTargets: []string{`custom(
-    name = "ptr_to_embedded_props",
-    other_embedded_attr = "abc",
-)`,
+			expectedBazelTargets: []string{
+				makeBazelTarget("custom", "ptr_to_embedded_props", attrNameToString{
+					"other_embedded_attr": `"abc"`,
+				}),
 			},
 		},
 	}
@@ -649,9 +647,7 @@
     bazel_module: { bp2build_available: true },
 }`,
 			expectedBazelTargets: []string{
-				`filegroup(
-    name = "fg_foo",
-)`,
+				makeBazelTarget("filegroup", "fg_foo", map[string]string{}),
 			},
 		},
 		{
@@ -665,9 +661,7 @@
     bazel_module: { bp2build_available: true },
 }`,
 			expectedBazelTargets: []string{
-				`filegroup(
-    name = "fg_foo",
-)`,
+				makeBazelTarget("filegroup", "fg_foo", map[string]string{}),
 			},
 		},
 		{
@@ -680,13 +674,13 @@
     srcs: ["a", "b"],
     bazel_module: { bp2build_available: true },
 }`,
-			expectedBazelTargets: []string{`filegroup(
-    name = "fg_foo",
-    srcs = [
+			expectedBazelTargets: []string{
+				makeBazelTarget("filegroup", "fg_foo", map[string]string{
+					"srcs": `[
         "a",
         "b",
-    ],
-)`,
+    ]`,
+				}),
 			},
 		},
 		{
@@ -700,10 +694,10 @@
     exclude_srcs: ["a"],
     bazel_module: { bp2build_available: true },
 }`,
-			expectedBazelTargets: []string{`filegroup(
-    name = "fg_foo",
-    srcs = ["b"],
-)`,
+			expectedBazelTargets: []string{
+				makeBazelTarget("filegroup", "fg_foo", map[string]string{
+					"srcs": `["b"]`,
+				}),
 			},
 		},
 		{
@@ -712,18 +706,18 @@
 			moduleTypeUnderTestFactory:         android.FileGroupFactory,
 			moduleTypeUnderTestBp2BuildMutator: android.FilegroupBp2Build,
 			blueprint: `filegroup {
-    name: "foo",
+    name: "fg_foo",
     srcs: ["**/*.txt"],
     bazel_module: { bp2build_available: true },
 }`,
-			expectedBazelTargets: []string{`filegroup(
-    name = "foo",
-    srcs = [
+			expectedBazelTargets: []string{
+				makeBazelTarget("filegroup", "fg_foo", map[string]string{
+					"srcs": `[
         "other/a.txt",
         "other/b.txt",
         "other/subdir/a.txt",
-    ],
-)`,
+    ]`,
+				}),
 			},
 			filesystem: map[string]string{
 				"other/a.txt":        "",
@@ -737,21 +731,8 @@
 			moduleTypeUnderTest:                "filegroup",
 			moduleTypeUnderTestFactory:         android.FileGroupFactory,
 			moduleTypeUnderTestBp2BuildMutator: android.FilegroupBp2Build,
-			blueprint: `filegroup {
-    name: "foo",
-    srcs: ["a.txt"],
-    bazel_module: { bp2build_available: true },
-}`,
-			dir: "other",
-			expectedBazelTargets: []string{`filegroup(
-    name = "fg_foo",
-    srcs = [
-        "a.txt",
-        "b.txt",
-        "subdir/a.txt",
-    ],
-)`,
-			},
+			blueprint:                          ``,
+			dir:                                "other",
 			filesystem: map[string]string{
 				"other/Android.bp": `filegroup {
     name: "fg_foo",
@@ -763,6 +744,15 @@
 				"other/subdir/a.txt": "",
 				"other/file":         "",
 			},
+			expectedBazelTargets: []string{
+				makeBazelTarget("filegroup", "fg_foo", map[string]string{
+					"srcs": `[
+        "a.txt",
+        "b.txt",
+        "subdir/a.txt",
+    ]`,
+				}),
+			},
 		},
 		{
 			description:                        "depends_on_other_dir_module",
@@ -770,21 +760,13 @@
 			moduleTypeUnderTestFactory:         android.FileGroupFactory,
 			moduleTypeUnderTestBp2BuildMutator: android.FilegroupBp2Build,
 			blueprint: `filegroup {
-    name: "foobar",
+    name: "fg_foo",
     srcs: [
         ":foo",
         "c",
     ],
     bazel_module: { bp2build_available: true },
 }`,
-			expectedBazelTargets: []string{`filegroup(
-    name = "foobar",
-    srcs = [
-        "//other:foo",
-        "c",
-    ],
-)`,
-			},
 			filesystem: map[string]string{
 				"other/Android.bp": `filegroup {
     name: "foo",
@@ -792,6 +774,14 @@
     bazel_module: { bp2build_available: true },
 }`,
 			},
+			expectedBazelTargets: []string{
+				makeBazelTarget("filegroup", "fg_foo", map[string]string{
+					"srcs": `[
+        "//other:foo",
+        "c",
+    ]`,
+				}),
+			},
 		},
 		{
 			description:                        "depends_on_other_unconverted_module_error",
@@ -799,21 +789,21 @@
 			moduleTypeUnderTestFactory:         android.FileGroupFactory,
 			moduleTypeUnderTestBp2BuildMutator: android.FilegroupBp2Build,
 			unconvertedDepsMode:                errorModulesUnconvertedDeps,
-			blueprint: `filegroup {
-    name: "foobar",
-    srcs: [
-        ":foo",
-        "c",
-    ],
-    bazel_module: { bp2build_available: true },
-}`,
-			expectedErr: fmt.Errorf(`"foobar" depends on unconverted modules: foo`),
 			filesystem: map[string]string{
 				"other/Android.bp": `filegroup {
     name: "foo",
     srcs: ["a", "b"],
 }`,
 			},
+			blueprint: `filegroup {
+    name: "fg_foo",
+    srcs: [
+        ":foo",
+        "c",
+    ],
+    bazel_module: { bp2build_available: true },
+}`,
+			expectedErr: fmt.Errorf(`"fg_foo" depends on unconverted modules: foo`),
 		},
 	}
 
@@ -1088,9 +1078,8 @@
 				"other/BUILD.bazel": `// definition for fg_bar`,
 			},
 			expectedBazelTargets: []string{
-				`filegroup(
-    name = "fg_foo",
-)`, `// definition for fg_bar`,
+				makeBazelTarget("filegroup", "fg_foo", map[string]string{}),
+				`// definition for fg_bar`,
 			},
 		},
 		{
@@ -1098,6 +1087,9 @@
 			moduleTypeUnderTest:                "filegroup",
 			moduleTypeUnderTestFactory:         android.FileGroupFactory,
 			moduleTypeUnderTestBp2BuildMutator: android.FilegroupBp2Build,
+			filesystem: map[string]string{
+				"other/BUILD.bazel": `// BUILD file`,
+			},
 			blueprint: `filegroup {
 		    name: "fg_foo",
 		    bazel_module: {
@@ -1112,14 +1104,9 @@
 		    },
 		}`,
 			expectedBazelTargets: []string{
-				`filegroup(
-    name = "fg_bar",
-)`,
+				makeBazelTarget("filegroup", "fg_bar", map[string]string{}),
 				`// BUILD file`,
 			},
-			filesystem: map[string]string{
-				"other/BUILD.bazel": `// BUILD file`,
-			},
 		},
 	}
 
@@ -1195,16 +1182,6 @@
     exclude_srcs: ["c.txt"],
     bazel_module: { bp2build_available: true },
 }`,
-			expectedBazelTargets: []string{`filegroup(
-    name = "fg_foo",
-    srcs = [
-        "a.txt",
-        "b.txt",
-        "//dir:e.txt",
-        "//dir:f.txt",
-    ],
-)`,
-			},
 			filesystem: map[string]string{
 				"a.txt":          "",
 				"b.txt":          "",
@@ -1213,6 +1190,16 @@
 				"dir/e.txt":      "",
 				"dir/f.txt":      "",
 			},
+			expectedBazelTargets: []string{
+				makeBazelTarget("filegroup", "fg_foo", map[string]string{
+					"srcs": `[
+        "a.txt",
+        "b.txt",
+        "//dir:e.txt",
+        "//dir:f.txt",
+    ]`,
+				}),
+			},
 		},
 		{
 			description:                        "filegroup in subdir exclude_srcs",
@@ -1235,66 +1222,22 @@
 				"dir/subdir/e.txt":      "",
 				"dir/subdir/f.txt":      "",
 			},
-			expectedBazelTargets: []string{`filegroup(
-    name = "fg_foo",
-    srcs = [
+			expectedBazelTargets: []string{
+				makeBazelTarget("filegroup", "fg_foo", map[string]string{
+					"srcs": `[
         "a.txt",
         "//dir/subdir:e.txt",
         "//dir/subdir:f.txt",
-    ],
-)`,
+    ]`,
+				}),
 			},
 		},
 	}
 
-	dir := "."
 	for _, testCase := range testCases {
-		fs := make(map[string][]byte)
-		toParse := []string{
-			"Android.bp",
-		}
-		for f, content := range testCase.filesystem {
-			if strings.HasSuffix(f, "Android.bp") {
-				toParse = append(toParse, f)
-			}
-			fs[f] = []byte(content)
-		}
-		config := android.TestConfig(buildDir, nil, testCase.blueprint, fs)
-		ctx := android.NewTestContext(config)
-		ctx.RegisterModuleType(testCase.moduleTypeUnderTest, testCase.moduleTypeUnderTestFactory)
-		ctx.RegisterBp2BuildMutator(testCase.moduleTypeUnderTest, testCase.moduleTypeUnderTestBp2BuildMutator)
-		ctx.RegisterForBazelConversion()
-
-		_, errs := ctx.ParseFileList(dir, toParse)
-		if errored(t, testCase, errs) {
-			continue
-		}
-		_, errs = ctx.ResolveDependencies(config)
-		if errored(t, testCase, errs) {
-			continue
-		}
-
-		checkDir := dir
-		if testCase.dir != "" {
-			checkDir = testCase.dir
-		}
-		codegenCtx := NewCodegenContext(config, *ctx.Context, Bp2Build)
-		bazelTargets, err := generateBazelTargetsForDir(codegenCtx, checkDir)
-		android.FailIfErrored(t, err)
-		if actualCount, expectedCount := len(bazelTargets), len(testCase.expectedBazelTargets); actualCount != expectedCount {
-			t.Errorf("%s: Expected %d bazel target, got %d\n%s", testCase.description, expectedCount, actualCount, bazelTargets)
-		} else {
-			for i, target := range bazelTargets {
-				if w, g := testCase.expectedBazelTargets[i], target.content; w != g {
-					t.Errorf(
-						"%s: Expected generated Bazel target to be '%s', got '%s'",
-						testCase.description,
-						w,
-						g,
-					)
-				}
-			}
-		}
+		t.Run(testCase.description, func(t *testing.T) {
+			runBp2BuildTestCaseSimple(t, testCase)
+		})
 	}
 }
 
@@ -1305,22 +1248,16 @@
 			moduleTypeUnderTest:                "filegroup",
 			moduleTypeUnderTestFactory:         android.FileGroupFactory,
 			moduleTypeUnderTestBp2BuildMutator: android.FilegroupBp2Build,
-			blueprint: `filegroup {
-    name: "reqd",
-}
-
+			blueprint: simpleModuleDoNotConvertBp2build("filegroup", "reqd") + `
 filegroup {
     name: "fg_foo",
     required: ["reqd"],
     bazel_module: { bp2build_available: true },
 }`,
-			expectedBazelTargets: []string{`filegroup(
-    name = "fg_foo",
-    data = [":reqd"],
-)`,
-				`filegroup(
-    name = "reqd",
-)`,
+			expectedBazelTargets: []string{
+				makeBazelTarget("filegroup", "fg_foo", map[string]string{
+					"data": `[":reqd"]`,
+				}),
 			},
 		},
 		{
@@ -1328,16 +1265,8 @@
 			moduleTypeUnderTest:                "python_library",
 			moduleTypeUnderTestFactory:         python.PythonLibraryFactory,
 			moduleTypeUnderTestBp2BuildMutator: python.PythonLibraryBp2Build,
-			blueprint: `python_library {
-    name: "reqdx86",
-    bazel_module: { bp2build_available: false, },
-}
-
-python_library {
-    name: "reqdarm",
-    bazel_module: { bp2build_available: false, },
-}
-
+			blueprint: simpleModuleDoNotConvertBp2build("python_library", "reqdx86") +
+				simpleModuleDoNotConvertBp2build("python_library", "reqdarm") + `
 python_library {
     name: "fg_foo",
     arch: {
@@ -1350,15 +1279,15 @@
     },
     bazel_module: { bp2build_available: true },
 }`,
-			expectedBazelTargets: []string{`py_library(
-    name = "fg_foo",
-    data = select({
+			expectedBazelTargets: []string{
+				makeBazelTarget("py_library", "fg_foo", map[string]string{
+					"data": `select({
         "//build/bazel/platforms/arch:arm": [":reqdarm"],
         "//build/bazel/platforms/arch:x86": [":reqdx86"],
         "//conditions:default": [],
-    }),
-    srcs_version = "PY3",
-)`,
+    })`,
+					"srcs_version": `"PY3"`,
+				}),
 			},
 		},
 		{
@@ -1366,11 +1295,11 @@
 			moduleTypeUnderTest:                "python_library",
 			moduleTypeUnderTestFactory:         python.PythonLibraryFactory,
 			moduleTypeUnderTestBp2BuildMutator: python.PythonLibraryBp2Build,
-			blueprint: `python_library {
-    name: "reqd",
-    srcs: ["src.py"],
-}
-
+			filesystem: map[string]string{
+				"data.bin": "",
+				"src.py":   "",
+			},
+			blueprint: simpleModuleDoNotConvertBp2build("python_library", "reqd") + `
 python_library {
     name: "fg_foo",
     data: ["data.bin"],
@@ -1378,23 +1307,13 @@
     bazel_module: { bp2build_available: true },
 }`,
 			expectedBazelTargets: []string{
-				`py_library(
-    name = "fg_foo",
-    data = [
+				makeBazelTarget("py_library", "fg_foo", map[string]string{
+					"data": `[
         "data.bin",
         ":reqd",
-    ],
-    srcs_version = "PY3",
-)`,
-				`py_library(
-    name = "reqd",
-    srcs = ["src.py"],
-    srcs_version = "PY3",
-)`,
-			},
-			filesystem: map[string]string{
-				"data.bin": "",
-				"src.py":   "",
+    ]`,
+					"srcs_version": `"PY3"`,
+				}),
 			},
 		},
 		{
@@ -1402,28 +1321,23 @@
 			moduleTypeUnderTest:                "filegroup",
 			moduleTypeUnderTestFactory:         android.FileGroupFactory,
 			moduleTypeUnderTestBp2BuildMutator: android.FilegroupBp2Build,
-			blueprint: `filegroup {
-    name: "reqd"
-}
+			blueprint: simpleModuleDoNotConvertBp2build("filegroup", "reqd") + `
 filegroup {
     name: "fg_foo",
     required: ["reqd"],
     bazel_module: { bp2build_available: true },
 }`,
 			expectedBazelTargets: []string{
-				`filegroup(
-    name = "fg_foo",
-    data = [":reqd"],
-)`,
-				`filegroup(
-    name = "reqd",
-)`,
+				makeBazelTarget("filegroup", "fg_foo", map[string]string{
+					"data": `[":reqd"]`,
+				}),
 			},
-			filesystem: map[string]string{},
 		},
 	}
 
-	for _, test := range testCases {
-		runBp2BuildTestCaseSimple(t, test)
+	for _, tc := range testCases {
+		t.Run(tc.description, func(t *testing.T) {
+			runBp2BuildTestCaseSimple(t, tc)
+		})
 	}
 }
diff --git a/bp2build/cc_binary_conversion_test.go b/bp2build/cc_binary_conversion_test.go
index 0b71d89..f9abcba 100644
--- a/bp2build/cc_binary_conversion_test.go
+++ b/bp2build/cc_binary_conversion_test.go
@@ -28,6 +28,26 @@
 	compatibleWithPlaceHolder = "{target_compatible_with}"
 )
 
+type testBazelTarget struct {
+	typ   string
+	name  string
+	attrs attrNameToString
+}
+
+func generateBazelTargetsForTest(targets []testBazelTarget) []string {
+	ret := make([]string, 0, len(targets))
+	for _, t := range targets {
+		ret = append(ret, makeBazelTarget(t.typ, t.name, t.attrs))
+	}
+	return ret
+}
+
+type ccBinaryBp2buildTestCase struct {
+	description string
+	blueprint   string
+	targets     []testBazelTarget
+}
+
 func registerCcBinaryModuleTypes(ctx android.RegistrationContext) {
 	cc.RegisterCCBuildComponents(ctx)
 	ctx.RegisterModuleType("filegroup", android.FileGroupFactory)
@@ -36,55 +56,56 @@
 	ctx.RegisterModuleType("genrule", genrule.GenRuleFactory)
 }
 
-var binaryReplacer = strings.NewReplacer(ccBinaryTypePlaceHolder, "cc_binary", compatibleWithPlaceHolder, "")
-var hostBinaryReplacer = strings.NewReplacer(ccBinaryTypePlaceHolder, "cc_binary_host", compatibleWithPlaceHolder, `
-    target_compatible_with = select({
-        "//build/bazel/platforms/os:android": ["@platforms//:incompatible"],
-        "//conditions:default": [],
-    }),`)
+var binaryReplacer = strings.NewReplacer(ccBinaryTypePlaceHolder, "cc_binary")
+var hostBinaryReplacer = strings.NewReplacer(ccBinaryTypePlaceHolder, "cc_binary_host")
 
-func runCcBinaryTests(t *testing.T, tc bp2buildTestCase) {
+func runCcBinaryTests(t *testing.T, tc ccBinaryBp2buildTestCase) {
 	t.Helper()
 	runCcBinaryTestCase(t, tc)
 	runCcHostBinaryTestCase(t, tc)
 }
 
-func runCcBinaryTestCase(t *testing.T, tc bp2buildTestCase) {
+func runCcBinaryTestCase(t *testing.T, tc ccBinaryBp2buildTestCase) {
 	t.Helper()
-	testCase := tc
-	testCase.expectedBazelTargets = append([]string{}, tc.expectedBazelTargets...)
-	testCase.moduleTypeUnderTest = "cc_binary"
-	testCase.moduleTypeUnderTestFactory = cc.BinaryFactory
-	testCase.moduleTypeUnderTestBp2BuildMutator = cc.BinaryBp2build
-	testCase.description = fmt.Sprintf("%s %s", testCase.moduleTypeUnderTest, testCase.description)
-	testCase.blueprint = binaryReplacer.Replace(testCase.blueprint)
-	for i, et := range testCase.expectedBazelTargets {
-		testCase.expectedBazelTargets[i] = binaryReplacer.Replace(et)
+	moduleTypeUnderTest := "cc_binary"
+	testCase := bp2buildTestCase{
+		expectedBazelTargets:               generateBazelTargetsForTest(tc.targets),
+		moduleTypeUnderTest:                moduleTypeUnderTest,
+		moduleTypeUnderTestFactory:         cc.BinaryFactory,
+		moduleTypeUnderTestBp2BuildMutator: cc.BinaryBp2build,
+		description:                        fmt.Sprintf("%s %s", moduleTypeUnderTest, tc.description),
+		blueprint:                          binaryReplacer.Replace(tc.blueprint),
 	}
 	t.Run(testCase.description, func(t *testing.T) {
 		runBp2BuildTestCase(t, registerCcBinaryModuleTypes, testCase)
 	})
 }
 
-func runCcHostBinaryTestCase(t *testing.T, tc bp2buildTestCase) {
+func runCcHostBinaryTestCase(t *testing.T, tc ccBinaryBp2buildTestCase) {
 	t.Helper()
 	testCase := tc
-	testCase.expectedBazelTargets = append([]string{}, tc.expectedBazelTargets...)
-	testCase.moduleTypeUnderTest = "cc_binary_host"
-	testCase.moduleTypeUnderTestFactory = cc.BinaryHostFactory
-	testCase.moduleTypeUnderTestBp2BuildMutator = cc.BinaryHostBp2build
-	testCase.description = fmt.Sprintf("%s %s", testCase.moduleTypeUnderTest, testCase.description)
-	testCase.blueprint = hostBinaryReplacer.Replace(testCase.blueprint)
-	for i, et := range testCase.expectedBazelTargets {
-		testCase.expectedBazelTargets[i] = hostBinaryReplacer.Replace(et)
+	for i, t := range testCase.targets {
+		t.attrs["target_compatible_with"] = `select({
+        "//build/bazel/platforms/os:android": ["@platforms//:incompatible"],
+        "//conditions:default": [],
+    })`
+		testCase.targets[i] = t
 	}
+	moduleTypeUnderTest := "cc_binary_host"
 	t.Run(testCase.description, func(t *testing.T) {
-		runBp2BuildTestCase(t, registerCcBinaryModuleTypes, testCase)
+		runBp2BuildTestCase(t, registerCcBinaryModuleTypes, bp2buildTestCase{
+			expectedBazelTargets:               generateBazelTargetsForTest(testCase.targets),
+			moduleTypeUnderTest:                moduleTypeUnderTest,
+			moduleTypeUnderTestFactory:         cc.BinaryHostFactory,
+			moduleTypeUnderTestBp2BuildMutator: cc.BinaryHostBp2build,
+			description:                        fmt.Sprintf("%s %s", moduleTypeUnderTest, tc.description),
+			blueprint:                          hostBinaryReplacer.Replace(testCase.blueprint),
+		})
 	})
 }
 
 func TestBasicCcBinary(t *testing.T) {
-	runCcBinaryTests(t, bp2buildTestCase{
+	runCcBinaryTests(t, ccBinaryBp2buildTestCase{
 		description: "basic -- properties -> attrs with little/no transformation",
 		blueprint: `
 {rule_name} {
@@ -107,33 +128,35 @@
     },
 }
 `,
-		expectedBazelTargets: []string{`cc_binary(
-    name = "foo",
-    absolute_includes = ["absolute_dir"],
-    asflags = ["-Dasflag"],
-    conlyflags = ["-Dconlyflag"],
-    copts = ["-Dcopt"],
-    cppflags = ["-Dcppflag"],
-    linkopts = ["ld-flag"],
-    local_includes = [
+		targets: []testBazelTarget{
+			{"cc_binary", "foo", attrNameToString{
+				"absolute_includes": `["absolute_dir"]`,
+				"asflags":           `["-Dasflag"]`,
+				"conlyflags":        `["-Dconlyflag"]`,
+				"copts":             `["-Dcopt"]`,
+				"cppflags":          `["-Dcppflag"]`,
+				"linkopts":          `["ld-flag"]`,
+				"local_includes": `[
         "dir",
         ".",
-    ],
-    rtti = True,
-    srcs = ["a.cc"],
-    strip = {
+    ]`,
+				"rtti": `True`,
+				"srcs": `["a.cc"]`,
+				"strip": `{
         "all": True,
         "keep_symbols": True,
         "keep_symbols_and_debug_frame": True,
         "keep_symbols_list": ["symbol"],
         "none": True,
-    },{target_compatible_with}
-)`},
+    }`,
+			},
+			},
+		},
 	})
 }
 
 func TestCcBinaryWithSharedLdflagDisableFeature(t *testing.T) {
-	runCcBinaryTests(t, bp2buildTestCase{
+	runCcBinaryTests(t, ccBinaryBp2buildTestCase{
 		description: `ldflag "-shared" disables static_flag feature`,
 		blueprint: `
 {rule_name} {
@@ -142,16 +165,18 @@
     include_build_directory: false,
 }
 `,
-		expectedBazelTargets: []string{`cc_binary(
-    name = "foo",
-    features = ["-static_flag"],
-    linkopts = ["-shared"],{target_compatible_with}
-)`},
+		targets: []testBazelTarget{
+			{"cc_binary", "foo", attrNameToString{
+				"features": `["-static_flag"]`,
+				"linkopts": `["-shared"]`,
+			},
+			},
+		},
 	})
 }
 
 func TestCcBinaryWithLinkStatic(t *testing.T) {
-	runCcBinaryTests(t, bp2buildTestCase{
+	runCcBinaryTests(t, ccBinaryBp2buildTestCase{
 		description: "link static",
 		blueprint: `
 {rule_name} {
@@ -160,15 +185,17 @@
     include_build_directory: false,
 }
 `,
-		expectedBazelTargets: []string{`cc_binary(
-    name = "foo",
-    linkshared = False,{target_compatible_with}
-)`},
+		targets: []testBazelTarget{
+			{"cc_binary", "foo", attrNameToString{
+				"linkshared": `False`,
+			},
+			},
+		},
 	})
 }
 
 func TestCcBinaryVersionScript(t *testing.T) {
-	runCcBinaryTests(t, bp2buildTestCase{
+	runCcBinaryTests(t, ccBinaryBp2buildTestCase{
 		description: `version script`,
 		blueprint: `
 {rule_name} {
@@ -177,16 +204,18 @@
     version_script: "vs",
 }
 `,
-		expectedBazelTargets: []string{`cc_binary(
-    name = "foo",
-    additional_linker_inputs = ["vs"],
-    linkopts = ["-Wl,--version-script,$(location vs)"],{target_compatible_with}
-)`},
+		targets: []testBazelTarget{
+			{"cc_binary", "foo", attrNameToString{
+				"additional_linker_inputs": `["vs"]`,
+				"linkopts":                 `["-Wl,--version-script,$(location vs)"]`,
+			},
+			},
+		},
 	})
 }
 
 func TestCcBinarySplitSrcsByLang(t *testing.T) {
-	runCcHostBinaryTestCase(t, bp2buildTestCase{
+	runCcHostBinaryTestCase(t, ccBinaryBp2buildTestCase{
 		description: "split srcs by lang",
 		blueprint: `
 {rule_name} {
@@ -200,26 +229,28 @@
     include_build_directory: false,
 }
 ` + simpleModuleDoNotConvertBp2build("filegroup", "fg_foo"),
-		expectedBazelTargets: []string{`cc_binary(
-    name = "foo",
-    srcs = [
+		targets: []testBazelTarget{
+			{"cc_binary", "foo", attrNameToString{
+				"srcs": `[
         "cpponly.cpp",
         ":fg_foo_cpp_srcs",
-    ],
-    srcs_as = [
+    ]`,
+				"srcs_as": `[
         "asonly.S",
         ":fg_foo_as_srcs",
-    ],
-    srcs_c = [
+    ]`,
+				"srcs_c": `[
         "conly.c",
         ":fg_foo_c_srcs",
-    ],{target_compatible_with}
-)`},
+    ]`,
+			},
+			},
+		},
 	})
 }
 
 func TestCcBinaryDoNotDistinguishBetweenDepsAndImplementationDeps(t *testing.T) {
-	runCcBinaryTestCase(t, bp2buildTestCase{
+	runCcBinaryTestCase(t, ccBinaryBp2buildTestCase{
 		description: "no implementation deps",
 		blueprint: `
 genrule {
@@ -251,26 +282,28 @@
 			simpleModuleDoNotConvertBp2build("cc_library_static", "not_explicitly_exported_whole_static_dep") +
 			simpleModuleDoNotConvertBp2build("cc_library", "shared_dep") +
 			simpleModuleDoNotConvertBp2build("cc_library", "implementation_shared_dep"),
-		expectedBazelTargets: []string{`cc_binary(
-    name = "foo",
-    deps = [
+		targets: []testBazelTarget{
+			{"cc_binary", "foo", attrNameToString{
+				"deps": `[
         ":implementation_static_dep",
         ":static_dep",
-    ],
-    dynamic_deps = [
+    ]`,
+				"dynamic_deps": `[
         ":implementation_shared_dep",
         ":shared_dep",
-    ],
-    srcs = [
+    ]`,
+				"srcs": `[
         "foo.cpp",
         ":generated_hdr",
         ":export_generated_hdr",
-    ],{target_compatible_with}
-    whole_archive_deps = [
+    ]`,
+				"whole_archive_deps": `[
         ":not_explicitly_exported_whole_static_dep",
         ":whole_static_dep",
-    ],
-)`},
+    ]`,
+			},
+			},
+		},
 	})
 }
 
@@ -278,19 +311,21 @@
 	baseTestCases := []struct {
 		description   string
 		soongProperty string
-		bazelAttr     string
+		bazelAttr     attrNameToString
 	}{
 		{
 			description:   "nocrt: true",
 			soongProperty: `nocrt: true,`,
-			bazelAttr:     `    link_crt = False,`,
+			bazelAttr:     attrNameToString{"link_crt": `False`},
 		},
 		{
 			description:   "nocrt: false",
 			soongProperty: `nocrt: false,`,
+			bazelAttr:     attrNameToString{},
 		},
 		{
 			description: "nocrt: not set",
+			bazelAttr:   attrNameToString{},
 		},
 	}
 
@@ -300,24 +335,16 @@
 }
 `
 
-	baseBazelTarget := `cc_binary(
-    name = "foo",%s{target_compatible_with}
-)`
-
 	for _, btc := range baseTestCases {
 		prop := btc.soongProperty
 		if len(prop) > 0 {
 			prop = "\n" + prop
 		}
-		attr := btc.bazelAttr
-		if len(attr) > 0 {
-			attr = "\n" + attr
-		}
-		runCcBinaryTests(t, bp2buildTestCase{
+		runCcBinaryTests(t, ccBinaryBp2buildTestCase{
 			description: btc.description,
 			blueprint:   fmt.Sprintf(baseBlueprint, prop),
-			expectedBazelTargets: []string{
-				fmt.Sprintf(baseBazelTarget, attr),
+			targets: []testBazelTarget{
+				{"cc_binary", "foo", btc.bazelAttr},
 			},
 		})
 	}
@@ -327,20 +354,21 @@
 	baseTestCases := []struct {
 		description   string
 		soongProperty string
-		bazelAttr     string
+		bazelAttr     attrNameToString
 	}{
 		{
 			description:   "no_libcrt: true",
 			soongProperty: `no_libcrt: true,`,
-			bazelAttr:     `    use_libcrt = False,`,
+			bazelAttr:     attrNameToString{"use_libcrt": `False`},
 		},
 		{
 			description:   "no_libcrt: false",
 			soongProperty: `no_libcrt: false,`,
-			bazelAttr:     `    use_libcrt = True,`,
+			bazelAttr:     attrNameToString{"use_libcrt": `True`},
 		},
 		{
 			description: "no_libcrt: not set",
+			bazelAttr:   attrNameToString{},
 		},
 	}
 
@@ -350,24 +378,16 @@
 }
 `
 
-	baseBazelTarget := `cc_binary(
-    name = "foo",{target_compatible_with}%s
-)`
-
 	for _, btc := range baseTestCases {
 		prop := btc.soongProperty
 		if len(prop) > 0 {
 			prop = "\n" + prop
 		}
-		attr := btc.bazelAttr
-		if len(attr) > 0 {
-			attr = "\n" + attr
-		}
-		runCcBinaryTests(t, bp2buildTestCase{
+		runCcBinaryTests(t, ccBinaryBp2buildTestCase{
 			description: btc.description,
 			blueprint:   fmt.Sprintf(baseBlueprint, prop),
-			expectedBazelTargets: []string{
-				fmt.Sprintf(baseBazelTarget, attr),
+			targets: []testBazelTarget{
+				{"cc_binary", "foo", btc.bazelAttr},
 			},
 		})
 	}
@@ -377,31 +397,35 @@
 	baseTestCases := []struct {
 		description   string
 		soongProperty string
-		bazelAttr     string
+		bazelAttr     attrNameToString
 	}{
 		{
 			description:   "pack_relocation: true",
 			soongProperty: `pack_relocations: true,`,
+			bazelAttr:     attrNameToString{},
 		},
 		{
 			description:   "pack_relocations: false",
 			soongProperty: `pack_relocations: false,`,
-			bazelAttr:     `    features = ["disable_pack_relocations"],`,
+			bazelAttr:     attrNameToString{"features": `["disable_pack_relocations"]`},
 		},
 		{
 			description: "pack_relocations: not set",
+			bazelAttr:   attrNameToString{},
 		},
 		{
 			description:   "pack_relocation: true",
 			soongProperty: `allow_undefined_symbols: true,`,
-			bazelAttr:     `    features = ["-no_undefined_symbols"],`,
+			bazelAttr:     attrNameToString{"features": `["-no_undefined_symbols"]`},
 		},
 		{
 			description:   "allow_undefined_symbols: false",
 			soongProperty: `allow_undefined_symbols: false,`,
+			bazelAttr:     attrNameToString{},
 		},
 		{
 			description: "allow_undefined_symbols: not set",
+			bazelAttr:   attrNameToString{},
 		},
 	}
 
@@ -410,25 +434,16 @@
     include_build_directory: false,
 }
 `
-
-	baseBazelTarget := `cc_binary(
-    name = "foo",%s{target_compatible_with}
-)`
-
 	for _, btc := range baseTestCases {
 		prop := btc.soongProperty
 		if len(prop) > 0 {
 			prop = "\n" + prop
 		}
-		attr := btc.bazelAttr
-		if len(attr) > 0 {
-			attr = "\n" + attr
-		}
-		runCcBinaryTests(t, bp2buildTestCase{
+		runCcBinaryTests(t, ccBinaryBp2buildTestCase{
 			description: btc.description,
 			blueprint:   fmt.Sprintf(baseBlueprint, prop),
-			expectedBazelTargets: []string{
-				fmt.Sprintf(baseBazelTarget, attr),
+			targets: []testBazelTarget{
+				{"cc_binary", "foo", btc.bazelAttr},
 			},
 		})
 	}
diff --git a/bp2build/cc_genrule_conversion_test.go b/bp2build/cc_genrule_conversion_test.go
index a7e9cb2..b3624dd 100644
--- a/bp2build/cc_genrule_conversion_test.go
+++ b/bp2build/cc_genrule_conversion_test.go
@@ -39,15 +39,15 @@
 
 func runCcGenruleTestCase(t *testing.T, tc bp2buildTestCase) {
 	t.Helper()
+	(&tc).moduleTypeUnderTest = "cc_genrule"
+	(&tc).moduleTypeUnderTestFactory = cc.GenRuleFactory
+	(&tc).moduleTypeUnderTestBp2BuildMutator = genrule.CcGenruleBp2Build
 	runBp2BuildTestCase(t, func(ctx android.RegistrationContext) {}, tc)
 }
 
 func TestCliVariableReplacement(t *testing.T) {
 	runCcGenruleTestCase(t, bp2buildTestCase{
-		description:                        "cc_genrule with command line variable replacements",
-		moduleTypeUnderTest:                "cc_genrule",
-		moduleTypeUnderTestFactory:         cc.GenRuleFactory,
-		moduleTypeUnderTestBp2BuildMutator: genrule.CcGenruleBp2Build,
+		description: "cc_genrule with command line variable replacements",
 		blueprint: `cc_genrule {
     name: "foo.tool",
     out: ["foo_tool.out"],
@@ -65,29 +65,24 @@
     bazel_module: { bp2build_available: true },
 }`,
 		expectedBazelTargets: []string{
-			`genrule(
-    name = "foo",
-    cmd = "$(location :foo.tool) --genDir=$(RULEDIR) arg $(SRCS) $(OUTS)",
-    outs = ["foo.out"],
-    srcs = ["foo.in"],
-    tools = [":foo.tool"],
-)`,
-			`genrule(
-    name = "foo.tool",
-    cmd = "cp $(SRCS) $(OUTS)",
-    outs = ["foo_tool.out"],
-    srcs = ["foo_tool.in"],
-)`,
+			makeBazelTarget("genrule", "foo", attrNameToString{
+				"cmd":   `"$(location :foo.tool) --genDir=$(RULEDIR) arg $(SRCS) $(OUTS)"`,
+				"outs":  `["foo.out"]`,
+				"srcs":  `["foo.in"]`,
+				"tools": `[":foo.tool"]`,
+			}),
+			makeBazelTarget("genrule", "foo.tool", attrNameToString{
+				"cmd":  `"cp $(SRCS) $(OUTS)"`,
+				"outs": `["foo_tool.out"]`,
+				"srcs": `["foo_tool.in"]`,
+			}),
 		},
 	})
 }
 
 func TestUsingLocationsLabel(t *testing.T) {
 	runCcGenruleTestCase(t, bp2buildTestCase{
-		description:                        "cc_genrule using $(locations :label)",
-		moduleTypeUnderTest:                "cc_genrule",
-		moduleTypeUnderTestFactory:         cc.GenRuleFactory,
-		moduleTypeUnderTestBp2BuildMutator: genrule.CcGenruleBp2Build,
+		description: "cc_genrule using $(locations :label)",
 		blueprint: `cc_genrule {
     name: "foo.tools",
     out: ["foo_tool.out", "foo_tool2.out"],
@@ -104,32 +99,28 @@
     cmd: "$(locations :foo.tools) -s $(out) $(in)",
     bazel_module: { bp2build_available: true },
 }`,
-		expectedBazelTargets: []string{`genrule(
-    name = "foo",
-    cmd = "$(locations :foo.tools) -s $(OUTS) $(SRCS)",
-    outs = ["foo.out"],
-    srcs = ["foo.in"],
-    tools = [":foo.tools"],
-)`,
-			`genrule(
-    name = "foo.tools",
-    cmd = "cp $(SRCS) $(OUTS)",
-    outs = [
+		expectedBazelTargets: []string{
+			makeBazelTarget("genrule", "foo", attrNameToString{
+				"cmd":   `"$(locations :foo.tools) -s $(OUTS) $(SRCS)"`,
+				"outs":  `["foo.out"]`,
+				"srcs":  `["foo.in"]`,
+				"tools": `[":foo.tools"]`,
+			}),
+			makeBazelTarget("genrule", "foo.tools", attrNameToString{
+				"cmd": `"cp $(SRCS) $(OUTS)"`,
+				"outs": `[
         "foo_tool.out",
         "foo_tool2.out",
-    ],
-    srcs = ["foo_tool.in"],
-)`,
+    ]`,
+				"srcs": `["foo_tool.in"]`,
+			}),
 		},
 	})
 }
 
 func TestUsingLocationsAbsoluteLabel(t *testing.T) {
 	runCcGenruleTestCase(t, bp2buildTestCase{
-		description:                        "cc_genrule using $(locations //absolute:label)",
-		moduleTypeUnderTest:                "cc_genrule",
-		moduleTypeUnderTestFactory:         cc.GenRuleFactory,
-		moduleTypeUnderTestBp2BuildMutator: genrule.CcGenruleBp2Build,
+		description: "cc_genrule using $(locations //absolute:label)",
 		blueprint: `cc_genrule {
     name: "foo",
     out: ["foo.out"],
@@ -138,24 +129,21 @@
     cmd: "$(locations :foo.tool) -s $(out) $(in)",
     bazel_module: { bp2build_available: true },
 }`,
-		expectedBazelTargets: []string{`genrule(
-    name = "foo",
-    cmd = "$(locations //other:foo.tool) -s $(OUTS) $(SRCS)",
-    outs = ["foo.out"],
-    srcs = ["foo.in"],
-    tools = ["//other:foo.tool"],
-)`,
-		},
 		filesystem: otherCcGenruleBp,
+		expectedBazelTargets: []string{
+			makeBazelTarget("genrule", "foo", attrNameToString{
+				"cmd":   `"$(locations //other:foo.tool) -s $(OUTS) $(SRCS)"`,
+				"outs":  `["foo.out"]`,
+				"srcs":  `["foo.in"]`,
+				"tools": `["//other:foo.tool"]`,
+			}),
+		},
 	})
 }
 
 func TestSrcsUsingAbsoluteLabel(t *testing.T) {
 	runCcGenruleTestCase(t, bp2buildTestCase{
-		description:                        "cc_genrule srcs using $(locations //absolute:label)",
-		moduleTypeUnderTest:                "cc_genrule",
-		moduleTypeUnderTestFactory:         cc.GenRuleFactory,
-		moduleTypeUnderTestBp2BuildMutator: genrule.CcGenruleBp2Build,
+		description: "cc_genrule srcs using $(locations //absolute:label)",
 		blueprint: `cc_genrule {
     name: "foo",
     out: ["foo.out"],
@@ -164,24 +152,21 @@
     cmd: "$(locations :foo.tool) -s $(out) $(location :other.tool)",
     bazel_module: { bp2build_available: true },
 }`,
-		expectedBazelTargets: []string{`genrule(
-    name = "foo",
-    cmd = "$(locations //other:foo.tool) -s $(OUTS) $(location //other:other.tool)",
-    outs = ["foo.out"],
-    srcs = ["//other:other.tool"],
-    tools = ["//other:foo.tool"],
-)`,
-		},
 		filesystem: otherCcGenruleBp,
+		expectedBazelTargets: []string{
+			makeBazelTarget("genrule", "foo", attrNameToString{
+				"cmd":   `"$(locations //other:foo.tool) -s $(OUTS) $(location //other:other.tool)"`,
+				"outs":  `["foo.out"]`,
+				"srcs":  `["//other:other.tool"]`,
+				"tools": `["//other:foo.tool"]`,
+			}),
+		},
 	})
 }
 
 func TestLocationsLabelUsesFirstToolFile(t *testing.T) {
 	runCcGenruleTestCase(t, bp2buildTestCase{
-		description:                        "cc_genrule using $(location) label should substitute first tool label automatically",
-		moduleTypeUnderTest:                "cc_genrule",
-		moduleTypeUnderTestFactory:         cc.GenRuleFactory,
-		moduleTypeUnderTestBp2BuildMutator: genrule.CcGenruleBp2Build,
+		description: "cc_genrule using $(location) label should substitute first tool label automatically",
 		blueprint: `cc_genrule {
     name: "foo",
     out: ["foo.out"],
@@ -190,27 +175,24 @@
     cmd: "$(location) -s $(out) $(in)",
     bazel_module: { bp2build_available: true },
 }`,
-		expectedBazelTargets: []string{`genrule(
-    name = "foo",
-    cmd = "$(location //other:foo.tool) -s $(OUTS) $(SRCS)",
-    outs = ["foo.out"],
-    srcs = ["foo.in"],
-    tools = [
+		filesystem: otherCcGenruleBp,
+		expectedBazelTargets: []string{
+			makeBazelTarget("genrule", "foo", attrNameToString{
+				"cmd":  `"$(location //other:foo.tool) -s $(OUTS) $(SRCS)"`,
+				"outs": `["foo.out"]`,
+				"srcs": `["foo.in"]`,
+				"tools": `[
         "//other:foo.tool",
         "//other:other.tool",
-    ],
-)`,
+    ]`,
+			}),
 		},
-		filesystem: otherCcGenruleBp,
 	})
 }
 
 func TestLocationsLabelUsesFirstTool(t *testing.T) {
 	runCcGenruleTestCase(t, bp2buildTestCase{
-		description:                        "cc_genrule using $(locations) label should substitute first tool label automatically",
-		moduleTypeUnderTest:                "cc_genrule",
-		moduleTypeUnderTestFactory:         cc.GenRuleFactory,
-		moduleTypeUnderTestBp2BuildMutator: genrule.CcGenruleBp2Build,
+		description: "cc_genrule using $(locations) label should substitute first tool label automatically",
 		blueprint: `cc_genrule {
     name: "foo",
     out: ["foo.out"],
@@ -219,27 +201,24 @@
     cmd: "$(locations) -s $(out) $(in)",
     bazel_module: { bp2build_available: true },
 }`,
-		expectedBazelTargets: []string{`genrule(
-    name = "foo",
-    cmd = "$(locations //other:foo.tool) -s $(OUTS) $(SRCS)",
-    outs = ["foo.out"],
-    srcs = ["foo.in"],
-    tools = [
+		filesystem: otherCcGenruleBp,
+		expectedBazelTargets: []string{
+			makeBazelTarget("genrule", "foo", attrNameToString{
+				"cmd":  `"$(locations //other:foo.tool) -s $(OUTS) $(SRCS)"`,
+				"outs": `["foo.out"]`,
+				"srcs": `["foo.in"]`,
+				"tools": `[
         "//other:foo.tool",
         "//other:other.tool",
-    ],
-)`,
+    ]`,
+			}),
 		},
-		filesystem: otherCcGenruleBp,
 	})
 }
 
 func TestWithoutToolsOrToolFiles(t *testing.T) {
 	runCcGenruleTestCase(t, bp2buildTestCase{
-		description:                        "cc_genrule without tools or tool_files can convert successfully",
-		moduleTypeUnderTest:                "cc_genrule",
-		moduleTypeUnderTestFactory:         cc.GenRuleFactory,
-		moduleTypeUnderTestBp2BuildMutator: genrule.CcGenruleBp2Build,
+		description: "cc_genrule without tools or tool_files can convert successfully",
 		blueprint: `cc_genrule {
     name: "foo",
     out: ["foo.out"],
@@ -247,12 +226,12 @@
     cmd: "cp $(in) $(out)",
     bazel_module: { bp2build_available: true },
 }`,
-		expectedBazelTargets: []string{`genrule(
-    name = "foo",
-    cmd = "cp $(SRCS) $(OUTS)",
-    outs = ["foo.out"],
-    srcs = ["foo.in"],
-)`,
+		expectedBazelTargets: []string{
+			makeBazelTarget("genrule", "foo", attrNameToString{
+				"cmd":  `"cp $(SRCS) $(OUTS)"`,
+				"outs": `["foo.out"]`,
+				"srcs": `["foo.in"]`,
+			}),
 		},
 	})
 }
diff --git a/bp2build/cc_library_conversion_test.go b/bp2build/cc_library_conversion_test.go
index cbdc167..a3d902a 100644
--- a/bp2build/cc_library_conversion_test.go
+++ b/bp2build/cc_library_conversion_test.go
@@ -81,8 +81,8 @@
 			"x86_64.cpp":       "",
 			"foo-dir/a.h":      "",
 		},
-		blueprint: soongCcLibraryPreamble + `
-cc_library_headers { name: "some-headers" }
+		blueprint: soongCcLibraryPreamble +
+			simpleModuleDoNotConvertBp2build("cc_library_headers", "some-headers") + `
 cc_library {
     name: "foo-lib",
     srcs: ["impl.cpp"],
@@ -117,17 +117,17 @@
     include_build_directory: false,
 }
 `,
-		expectedBazelTargets: []string{`cc_library(
-    name = "foo-lib",
-    copts = ["-Wall"],
-    export_includes = ["foo-dir"],
-    implementation_deps = [":some-headers"],
-    linkopts = ["-Wl,--exclude-libs=bar.a"] + select({
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library", "foo-lib", attrNameToString{
+				"copts":               `["-Wall"]`,
+				"export_includes":     `["foo-dir"]`,
+				"implementation_deps": `[":some-headers"]`,
+				"linkopts": `["-Wl,--exclude-libs=bar.a"] + select({
         "//build/bazel/platforms/arch:x86": ["-Wl,--exclude-libs=baz.a"],
         "//build/bazel/platforms/arch:x86_64": ["-Wl,--exclude-libs=qux.a"],
         "//conditions:default": [],
-    }),
-    srcs = ["impl.cpp"] + select({
+    })`,
+				"srcs": `["impl.cpp"] + select({
         "//build/bazel/platforms/arch:x86": ["x86.cpp"],
         "//build/bazel/platforms/arch:x86_64": ["x86_64.cpp"],
         "//conditions:default": [],
@@ -140,8 +140,10 @@
         "//build/bazel/platforms/os:linux": ["linux.cpp"],
         "//build/bazel/platforms/os:linux_bionic": ["bionic.cpp"],
         "//conditions:default": [],
-    }),
-)`}})
+    })`,
+			}),
+		},
+	})
 }
 
 func TestCcLibraryTrimmedLdAndroid(t *testing.T) {
@@ -188,16 +190,17 @@
     include_build_directory: false,
 }
 `,
-		expectedBazelTargets: []string{`cc_library(
-    name = "fake-ld-android",
-    copts = [
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library", "fake-ld-android", attrNameToString{
+				"srcs": `["ld_android.cpp"]`,
+				"copts": `[
         "-Wall",
         "-Wextra",
         "-Wunused",
         "-Werror",
-    ],
-    implementation_deps = [":libc_headers"],
-    linkopts = [
+    ]`,
+				"implementation_deps": `[":libc_headers"]`,
+				"linkopts": `[
         "-Wl,--exclude-libs=libgcc.a",
         "-Wl,--exclude-libs=libgcc_stripped.a",
         "-Wl,--exclude-libs=libclang_rt.builtins-arm-android.a",
@@ -208,9 +211,9 @@
         "//build/bazel/platforms/arch:x86": ["-Wl,--exclude-libs=libgcc_eh.a"],
         "//build/bazel/platforms/arch:x86_64": ["-Wl,--exclude-libs=libgcc_eh.a"],
         "//conditions:default": [],
-    }),
-    srcs = ["ld_android.cpp"],
-)`},
+    })`,
+			}),
+		},
 	})
 }
 
@@ -255,15 +258,16 @@
 `,
 		},
 		blueprint: soongCcLibraryPreamble,
-		expectedBazelTargets: []string{`cc_library(
-    name = "fake-libarm-optimized-routines-math",
-    copts = select({
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library", "fake-libarm-optimized-routines-math", attrNameToString{
+				"copts": `select({
         "//build/bazel/platforms/arch:arm64": ["-DHAVE_FAST_FMA=1"],
         "//conditions:default": [],
-    }),
-    local_includes = ["."],
-    srcs_c = ["math/cosf.c"],
-)`},
+    })`,
+				"local_includes": `["."]`,
+				"srcs_c":         `["math/cosf.c"]`,
+			}),
+		},
 	})
 }
 
@@ -348,28 +352,29 @@
     bazel_module: { bp2build_available: false },
 }
 `,
-		expectedBazelTargets: []string{`cc_library(
-    name = "a",
-    copts = ["bothflag"],
-    implementation_deps = [":static_dep_for_both"],
-    implementation_dynamic_deps = [":shared_dep_for_both"],
-    shared = {
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library", "a", attrNameToString{
+				"copts":                       `["bothflag"]`,
+				"implementation_deps":         `[":static_dep_for_both"]`,
+				"implementation_dynamic_deps": `[":shared_dep_for_both"]`,
+				"shared": `{
         "copts": ["sharedflag"],
         "implementation_deps": [":static_dep_for_shared"],
         "implementation_dynamic_deps": [":shared_dep_for_shared"],
         "srcs": ["sharedonly.cpp"],
         "whole_archive_deps": [":whole_static_lib_for_shared"],
-    },
-    srcs = ["both.cpp"],
-    static = {
+    }`,
+				"srcs": `["both.cpp"]`,
+				"static": `{
         "copts": ["staticflag"],
         "implementation_deps": [":static_dep_for_static"],
         "implementation_dynamic_deps": [":shared_dep_for_static"],
         "srcs": ["staticonly.cpp"],
         "whole_archive_deps": [":whole_static_lib_for_static"],
-    },
-    whole_archive_deps = [":whole_static_lib_for_both"],
-)`},
+    }`,
+				"whole_archive_deps": `[":whole_static_lib_for_both"]`,
+			}),
+		},
 	})
 }
 
@@ -432,14 +437,14 @@
 			simpleModuleDoNotConvertBp2build("cc_library", "implementation_shared_dep_for_static") +
 			simpleModuleDoNotConvertBp2build("cc_library", "shared_dep_for_both") +
 			simpleModuleDoNotConvertBp2build("cc_library", "implementation_shared_dep_for_both"),
-		expectedBazelTargets: []string{`cc_library(
-    name = "a",
-    copts = ["bothflag"],
-    deps = [":static_dep_for_both"],
-    dynamic_deps = [":shared_dep_for_both"],
-    implementation_deps = [":implementation_static_dep_for_both"],
-    implementation_dynamic_deps = [":implementation_shared_dep_for_both"],
-    shared = {
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library", "a", attrNameToString{
+				"copts":                       `["bothflag"]`,
+				"deps":                        `[":static_dep_for_both"]`,
+				"dynamic_deps":                `[":shared_dep_for_both"]`,
+				"implementation_deps":         `[":implementation_static_dep_for_both"]`,
+				"implementation_dynamic_deps": `[":implementation_shared_dep_for_both"]`,
+				"shared": `{
         "copts": ["sharedflag"],
         "deps": [":static_dep_for_shared"],
         "dynamic_deps": [":shared_dep_for_shared"],
@@ -450,9 +455,9 @@
             ":not_explicitly_exported_whole_static_dep_for_shared",
             ":whole_static_dep_for_shared",
         ],
-    },
-    srcs = ["both.cpp"],
-    static = {
+    }`,
+				"srcs": `["both.cpp"]`,
+				"static": `{
         "copts": ["staticflag"],
         "deps": [":static_dep_for_static"],
         "dynamic_deps": [":shared_dep_for_static"],
@@ -463,12 +468,13 @@
             ":not_explicitly_exported_whole_static_dep_for_static",
             ":whole_static_dep_for_static",
         ],
-    },
-    whole_archive_deps = [
+    }`,
+				"whole_archive_deps": `[
         ":not_explicitly_exported_whole_static_dep_for_both",
         ":whole_static_dep_for_both",
-    ],
-)`},
+    ]`,
+			}),
+		},
 	})
 }
 
@@ -501,16 +507,17 @@
 `,
 		},
 		blueprint: soongCcLibraryPreamble,
-		expectedBazelTargets: []string{`cc_library(
-    name = "a",
-    shared = {
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library", "a", attrNameToString{
+				"shared": `{
         "whole_archive_deps": [":whole_static_lib_for_shared_alwayslink"],
-    },
-    static = {
+    }`,
+				"static": `{
         "whole_archive_deps": [":whole_static_lib_for_static_alwayslink"],
-    },
-    whole_archive_deps = [":whole_static_lib_for_both_alwayslink"],
-)`},
+    }`,
+				"whole_archive_deps": `[":whole_static_lib_for_both_alwayslink"]`,
+			}),
+		},
 	})
 }
 
@@ -591,12 +598,12 @@
 `,
 		},
 		blueprint: soongCcLibraryPreamble,
-		expectedBazelTargets: []string{`cc_library(
-    name = "a",
-    copts = ["bothflag"],
-    implementation_deps = [":static_dep_for_both"],
-    local_includes = ["."],
-    shared = {
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library", "a", attrNameToString{
+				"copts":               `["bothflag"]`,
+				"implementation_deps": `[":static_dep_for_both"]`,
+				"local_includes":      `["."]`,
+				"shared": `{
         "copts": ["sharedflag"] + select({
             "//build/bazel/platforms/arch:arm": ["-DARM_SHARED"],
             "//conditions:default": [],
@@ -629,9 +636,9 @@
             "//build/bazel/platforms/arch:arm": [":arm_whole_static_dep_for_shared"],
             "//conditions:default": [],
         }),
-    },
-    srcs = ["both.cpp"],
-    static = {
+    }`,
+				"srcs": `["both.cpp"]`,
+				"static": `{
         "copts": ["staticflag"] + select({
             "//build/bazel/platforms/arch:x86": ["-DX86_STATIC"],
             "//conditions:default": [],
@@ -644,8 +651,9 @@
             "//build/bazel/platforms/arch:x86": ["x86_static.cpp"],
             "//conditions:default": [],
         }),
-    },
-)`},
+    }`,
+			}),
+		},
 	})
 }
 
@@ -729,10 +737,10 @@
 `,
 		},
 		blueprint: soongCcLibraryPreamble,
-		expectedBazelTargets: []string{`cc_library(
-    name = "a",
-    local_includes = ["."],
-    shared = {
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library", "a", attrNameToString{
+				"local_includes": `["."]`,
+				"shared": `{
         "srcs": [
             "shared_source.cpp",
             "shared_source.cc",
@@ -747,22 +755,22 @@
             "shared_source.c",
             ":shared_filegroup_c_srcs",
         ],
-    },
-    srcs = [
+    }`,
+				"srcs": `[
         "both_source.cpp",
         "both_source.cc",
         ":both_filegroup_cpp_srcs",
-    ],
-    srcs_as = [
+    ]`,
+				"srcs_as": `[
         "both_source.s",
         "both_source.S",
         ":both_filegroup_as_srcs",
-    ],
-    srcs_c = [
+    ]`,
+				"srcs_c": `[
         "both_source.c",
         ":both_filegroup_c_srcs",
-    ],
-    static = {
+    ]`,
+				"static": `{
         "srcs": [
             "static_source.cpp",
             "static_source.cc",
@@ -777,8 +785,9 @@
             "static_source.c",
             ":static_filegroup_c_srcs",
         ],
-    },
-)`},
+    }`,
+			}),
+		},
 	})
 }
 
@@ -801,12 +810,13 @@
 `,
 		},
 		blueprint: soongCcLibraryPreamble,
-		expectedBazelTargets: []string{`cc_library(
-    name = "a",
-    additional_linker_inputs = ["v.map"],
-    linkopts = ["-Wl,--version-script,$(location v.map)"],
-    srcs = ["a.cpp"],
-)`},
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library", "a", attrNameToString{
+				"additional_linker_inputs": `["v.map"]`,
+				"linkopts":                 `["-Wl,--version-script,$(location v.map)"]`,
+				"srcs":                     `["a.cpp"]`,
+			}),
+		},
 	})
 }
 
@@ -837,20 +847,21 @@
     `,
 		},
 		blueprint: soongCcLibraryPreamble,
-		expectedBazelTargets: []string{`cc_library(
-    name = "a",
-    additional_linker_inputs = select({
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library", "a", attrNameToString{
+				"additional_linker_inputs": `select({
         "//build/bazel/platforms/arch:arm": ["arm.map"],
         "//build/bazel/platforms/arch:arm64": ["arm64.map"],
         "//conditions:default": [],
-    }),
-    linkopts = select({
+    })`,
+				"linkopts": `select({
         "//build/bazel/platforms/arch:arm": ["-Wl,--version-script,$(location arm.map)"],
         "//build/bazel/platforms/arch:arm64": ["-Wl,--version-script,$(location arm64.map)"],
         "//conditions:default": [],
-    }),
-    srcs = ["a.cpp"],
-)`},
+    })`,
+				"srcs": `["a.cpp"]`,
+			}),
+		},
 	})
 }
 
@@ -872,10 +883,11 @@
     include_build_directory: false,
 }
 `,
-		expectedBazelTargets: []string{`cc_library(
-    name = "a",
-    implementation_dynamic_deps = [":mylib"],
-)`},
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library", "a", attrNameToString{
+				"implementation_dynamic_deps": `[":mylib"]`,
+			}),
+		},
 	})
 }
 
@@ -917,34 +929,33 @@
     },
     include_build_directory: false,
 }`,
-		expectedBazelTargets: []string{`cc_library(
-    name = "a",
-    features = [
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library", "a", attrNameToString{
+				"features": `[
         "disable_pack_relocations",
         "-no_undefined_symbols",
-    ],
-    srcs = ["a.cpp"],
-)`, `cc_library(
-    name = "b",
-    features = select({
+    ]`,
+				"srcs": `["a.cpp"]`,
+			}), makeBazelTarget("cc_library", "b", attrNameToString{
+				"features": `select({
         "//build/bazel/platforms/arch:x86_64": [
             "disable_pack_relocations",
             "-no_undefined_symbols",
         ],
         "//conditions:default": [],
-    }),
-    srcs = ["b.cpp"],
-)`, `cc_library(
-    name = "c",
-    features = select({
+    })`,
+				"srcs": `["b.cpp"]`,
+			}), makeBazelTarget("cc_library", "c", attrNameToString{
+				"features": `select({
         "//build/bazel/platforms/os:darwin": [
             "disable_pack_relocations",
             "-no_undefined_symbols",
         ],
         "//conditions:default": [],
-    }),
-    srcs = ["c.cpp"],
-)`},
+    })`,
+				"srcs": `["c.cpp"]`,
+			}),
+		},
 	})
 }
 
@@ -961,13 +972,14 @@
     include_build_directory: false,
 }
 `,
-		expectedBazelTargets: []string{`cc_library(
-    name = "a",
-    copts = [
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library", "a", attrNameToString{
+				"copts": `[
         "-include",
         "header.h",
-    ],
-)`},
+    ]`,
+			}),
+		},
 	})
 }
 
@@ -998,10 +1010,10 @@
     include_build_directory: false,
 }
 `,
-		expectedBazelTargets: []string{`cc_library(
-    name = "a",
-    copts = ["-Wall"],
-    cppflags = [
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library", "a", attrNameToString{
+				"copts": `["-Wall"]`,
+				"cppflags": `[
         "-fsigned-char",
         "-pedantic",
     ] + select({
@@ -1010,9 +1022,10 @@
     }) + select({
         "//build/bazel/platforms/os:android": ["-DANDROID=1"],
         "//conditions:default": [],
-    }),
-    srcs = ["a.cpp"],
-)`},
+    })`,
+				"srcs": `["a.cpp"]`,
+			}),
+		},
 	})
 }
 
@@ -1097,31 +1110,30 @@
 }
 `,
 		expectedBazelTargets: []string{
-			`cc_library(
-    name = "foo_static",
-    implementation_deps = select({
+			makeBazelTarget("cc_library", "foo_static", attrNameToString{
+				"implementation_deps": `select({
         "//build/bazel/platforms/arch:arm": [],
         "//conditions:default": [":arm_static_lib_excludes_bp2build_cc_library_static"],
     }) + select({
         "//build/bazel/product_variables:malloc_not_svelte": [],
         "//conditions:default": [":malloc_not_svelte_static_lib_excludes_bp2build_cc_library_static"],
-    }),
-    implementation_dynamic_deps = select({
+    })`,
+				"implementation_dynamic_deps": `select({
         "//build/bazel/platforms/arch:arm": [],
         "//conditions:default": [":arm_shared_lib_excludes"],
     }) + select({
         "//build/bazel/product_variables:malloc_not_svelte": [":malloc_not_svelte_shared_lib"],
         "//conditions:default": [],
-    }),
-    srcs_c = ["common.c"],
-    whole_archive_deps = select({
+    })`,
+				"srcs_c": `["common.c"]`,
+				"whole_archive_deps": `select({
         "//build/bazel/platforms/arch:arm": [],
         "//conditions:default": [":arm_whole_static_lib_excludes_bp2build_cc_library_static"],
     }) + select({
         "//build/bazel/product_variables:malloc_not_svelte": [":malloc_not_svelte_whole_static_lib_bp2build_cc_library_static"],
         "//conditions:default": [":malloc_not_svelte_whole_static_lib_excludes_bp2build_cc_library_static"],
-    }),
-)`,
+    })`,
+			}),
 		},
 	})
 }
@@ -1143,11 +1155,13 @@
     include_build_directory: false,
 }
 `,
-		expectedBazelTargets: []string{`cc_library(
-    name = "foo-lib",
-    link_crt = False,
-    srcs = ["impl.cpp"],
-)`}})
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library", "foo-lib", attrNameToString{
+				"link_crt": `False`,
+				"srcs":     `["impl.cpp"]`,
+			}),
+		},
+	})
 }
 
 func TestCCLibraryNoCrtFalse(t *testing.T) {
@@ -1167,10 +1181,12 @@
     include_build_directory: false,
 }
 `,
-		expectedBazelTargets: []string{`cc_library(
-    name = "foo-lib",
-    srcs = ["impl.cpp"],
-)`}})
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library", "foo-lib", attrNameToString{
+				"srcs": `["impl.cpp"]`,
+			}),
+		},
+	})
 }
 
 func TestCCLibraryNoCrtArchVariant(t *testing.T) {
@@ -1219,11 +1235,12 @@
     include_build_directory: false,
 }
 `,
-		expectedBazelTargets: []string{`cc_library(
-    name = "foo-lib",
-    srcs = ["impl.cpp"],
-    use_libcrt = False,
-)`}})
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library", "foo-lib", attrNameToString{
+				"srcs":       `["impl.cpp"]`,
+				"use_libcrt": `False`,
+			}),
+		}})
 }
 
 func TestCCLibraryNoLibCrtFalse(t *testing.T) {
@@ -1243,11 +1260,12 @@
     include_build_directory: false,
 }
 `,
-		expectedBazelTargets: []string{`cc_library(
-    name = "foo-lib",
-    srcs = ["impl.cpp"],
-    use_libcrt = True,
-)`}})
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library", "foo-lib", attrNameToString{
+				"srcs":       `["impl.cpp"]`,
+				"use_libcrt": `True`,
+			}),
+		}})
 }
 
 func TestCCLibraryNoLibCrtArchVariant(t *testing.T) {
@@ -1273,15 +1291,16 @@
     include_build_directory: false,
 }
 `,
-		expectedBazelTargets: []string{`cc_library(
-    name = "foo-lib",
-    srcs = ["impl.cpp"],
-    use_libcrt = select({
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library", "foo-lib", attrNameToString{
+				"srcs": `["impl.cpp"]`,
+				"use_libcrt": `select({
         "//build/bazel/platforms/arch:arm": False,
         "//build/bazel/platforms/arch:x86": False,
         "//conditions:default": None,
-    }),
-)`}})
+    })`,
+			}),
+		}})
 }
 
 func TestCcLibraryStrip(t *testing.T) {
@@ -1331,34 +1350,29 @@
     include_build_directory: false,
 }
 `,
-		expectedBazelTargets: []string{`cc_library(
-    name = "all",
-    strip = {
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library", "all", attrNameToString{
+				"strip": `{
         "all": True,
-    },
-)`, `cc_library(
-    name = "keep_symbols",
-    strip = {
+    }`,
+			}), makeBazelTarget("cc_library", "keep_symbols", attrNameToString{
+				"strip": `{
         "keep_symbols": True,
-    },
-)`, `cc_library(
-    name = "keep_symbols_and_debug_frame",
-    strip = {
+    }`,
+			}), makeBazelTarget("cc_library", "keep_symbols_and_debug_frame", attrNameToString{
+				"strip": `{
         "keep_symbols_and_debug_frame": True,
-    },
-)`, `cc_library(
-    name = "keep_symbols_list",
-    strip = {
+    }`,
+			}), makeBazelTarget("cc_library", "keep_symbols_list", attrNameToString{
+				"strip": `{
         "keep_symbols_list": ["symbol"],
-    },
-)`, `cc_library(
-    name = "none",
-    strip = {
+    }`,
+			}), makeBazelTarget("cc_library", "none", attrNameToString{
+				"strip": `{
         "none": True,
-    },
-)`, `cc_library(
-    name = "nothing",
-)`},
+    }`,
+			}), makeBazelTarget("cc_library", "nothing", attrNameToString{}),
+		},
 	})
 }
 
@@ -1393,9 +1407,9 @@
     include_build_directory: false,
 }
 `,
-		expectedBazelTargets: []string{`cc_library(
-    name = "multi-arch",
-    strip = {
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library", "multi-arch", attrNameToString{
+				"strip": `{
         "keep_symbols": select({
             "//build/bazel/platforms/arch:arm64": True,
             "//conditions:default": None,
@@ -1411,8 +1425,9 @@
             ],
             "//conditions:default": [],
         }),
-    },
-)`},
+    }`,
+			}),
+		},
 	})
 }
 
@@ -1429,10 +1444,11 @@
     include_build_directory: false,
 }
 `,
-		expectedBazelTargets: []string{`cc_library(
-    name = "root_empty",
-    system_dynamic_deps = [],
-)`},
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library", "root_empty", attrNameToString{
+				"system_dynamic_deps": `[]`,
+			}),
+		},
 	})
 }
 
@@ -1451,12 +1467,13 @@
     include_build_directory: false,
 }
 `,
-		expectedBazelTargets: []string{`cc_library(
-    name = "static_empty",
-    static = {
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library", "static_empty", attrNameToString{
+				"static": `{
         "system_dynamic_deps": [],
-    },
-)`},
+    }`,
+			}),
+		},
 	})
 }
 
@@ -1475,12 +1492,13 @@
     include_build_directory: false,
 }
 `,
-		expectedBazelTargets: []string{`cc_library(
-    name = "shared_empty",
-    shared = {
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library", "shared_empty", attrNameToString{
+				"shared": `{
         "system_dynamic_deps": [],
-    },
-)`},
+    }`,
+			}),
+		},
 	})
 }
 
@@ -1503,12 +1521,13 @@
     include_build_directory: false,
 }
 `,
-		expectedBazelTargets: []string{`cc_library(
-    name = "shared_empty",
-    shared = {
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library", "shared_empty", attrNameToString{
+				"shared": `{
         "system_dynamic_deps": [],
-    },
-)`},
+    }`,
+			}),
+		},
 	})
 }
 
@@ -1533,10 +1552,11 @@
     include_build_directory: false,
 }
 `,
-		expectedBazelTargets: []string{`cc_library(
-    name = "target_linux_bionic_empty",
-    system_dynamic_deps = [],
-)`},
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library", "target_linux_bionic_empty", attrNameToString{
+				"system_dynamic_deps": `[]`,
+			}),
+		},
 	})
 }
 
@@ -1557,10 +1577,11 @@
     include_build_directory: false,
 }
 `,
-		expectedBazelTargets: []string{`cc_library(
-    name = "target_bionic_empty",
-    system_dynamic_deps = [],
-)`},
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library", "target_bionic_empty", attrNameToString{
+				"system_dynamic_deps": `[]`,
+			}),
+		},
 	})
 }
 
@@ -1589,13 +1610,14 @@
     include_build_directory: false,
 }
 `,
-		expectedBazelTargets: []string{`cc_library(
-    name = "foo",
-    shared = {
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library", "foo", attrNameToString{
+				"shared": `{
         "system_dynamic_deps": [":libm"],
-    },
-    system_dynamic_deps = [":libc"],
-)`},
+    }`,
+				"system_dynamic_deps": `[":libc"]`,
+			}),
+		},
 	})
 }
 
@@ -1637,9 +1659,9 @@
     include_build_directory: false,
 }
 `,
-		expectedBazelTargets: []string{`cc_library(
-    name = "foo-lib",
-    srcs = ["base.cpp"] + select({
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library", "foo-lib", attrNameToString{
+				"srcs": `["base.cpp"] + select({
         "//build/bazel/platforms/os:android": [
             "linux.cpp",
             "bionic.cpp",
@@ -1660,9 +1682,10 @@
         ],
         "//build/bazel/platforms/os:windows": ["windows.cpp"],
         "//conditions:default": [],
-    }),
-)`}})
-
+    })`,
+			}),
+		},
+	})
 }
 
 func TestCcLibraryCppStdWithGnuExtensions_ConvertsToFeatureAttr(t *testing.T) {
@@ -1714,17 +1737,17 @@
 		{cpp_std: "gnu++17", gnu_extensions: "true", bazel_cpp_std: "gnu++17"},
 	}
 	for _, tc := range testCases {
-		cppStdAttr := ""
+		cppStdProp := ""
 		if tc.cpp_std != "" {
-			cppStdAttr = fmt.Sprintf("    cpp_std: \"%s\",", tc.cpp_std)
+			cppStdProp = fmt.Sprintf("    cpp_std: \"%s\",", tc.cpp_std)
 		}
-		gnuExtensionsAttr := ""
+		gnuExtensionsProp := ""
 		if tc.gnu_extensions != "" {
-			gnuExtensionsAttr = fmt.Sprintf("    gnu_extensions: %s,", tc.gnu_extensions)
+			gnuExtensionsProp = fmt.Sprintf("    gnu_extensions: %s,", tc.gnu_extensions)
 		}
-		bazelCppStdAttr := ""
+		attrs := attrNameToString{}
 		if tc.bazel_cpp_std != "" {
-			bazelCppStdAttr = fmt.Sprintf("\n    cpp_std = \"%s\",", tc.bazel_cpp_std)
+			attrs["cpp_std"] = fmt.Sprintf(`"%s"`, tc.bazel_cpp_std)
 		}
 
 		runCcLibraryTestCase(t, bp2buildTestCase{
@@ -1740,10 +1763,10 @@
 %s // gnu_extensions: *bool
 	include_build_directory: false,
 }
-`, cppStdAttr, gnuExtensionsAttr),
-			expectedBazelTargets: []string{fmt.Sprintf(`cc_library(
-    name = "a",%s
-)`, bazelCppStdAttr)},
+`, cppStdProp, gnuExtensionsProp),
+			expectedBazelTargets: []string{
+				makeBazelTarget("cc_library", "a", attrs),
+			},
 		})
 
 		runCcLibraryStaticTestCase(t, bp2buildTestCase{
@@ -1759,10 +1782,10 @@
 %s // gnu_extensions: *bool
 	include_build_directory: false,
 }
-`, cppStdAttr, gnuExtensionsAttr),
-			expectedBazelTargets: []string{fmt.Sprintf(`cc_library_static(
-    name = "a",%s
-)`, bazelCppStdAttr)},
+`, cppStdProp, gnuExtensionsProp),
+			expectedBazelTargets: []string{
+				makeBazelTarget("cc_library_static", "a", attrs),
+			},
 		})
 
 		runCcLibrarySharedTestCase(t, bp2buildTestCase{
@@ -1778,10 +1801,10 @@
 %s // gnu_extensions: *bool
 	include_build_directory: false,
 }
-`, cppStdAttr, gnuExtensionsAttr),
-			expectedBazelTargets: []string{fmt.Sprintf(`cc_library_shared(
-    name = "a",%s
-)`, bazelCppStdAttr)},
+`, cppStdProp, gnuExtensionsProp),
+			expectedBazelTargets: []string{
+				makeBazelTarget("cc_library_shared", "a", attrs),
+			},
 		})
 	}
 }
diff --git a/bp2build/cc_library_headers_conversion_test.go b/bp2build/cc_library_headers_conversion_test.go
index e43672b..76fdab2 100644
--- a/bp2build/cc_library_headers_conversion_test.go
+++ b/bp2build/cc_library_headers_conversion_test.go
@@ -128,9 +128,9 @@
 
     // TODO: Also support export_header_lib_headers
 }`,
-		expectedBazelTargets: []string{`cc_library_headers(
-    name = "foo_headers",
-    export_includes = [
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library_headers", "foo_headers", attrNameToString{
+				"export_includes": `[
         "dir-1",
         "dir-2",
     ] + select({
@@ -138,12 +138,13 @@
         "//build/bazel/platforms/arch:x86": ["arch_x86_exported_include_dir"],
         "//build/bazel/platforms/arch:x86_64": ["arch_x86_64_exported_include_dir"],
         "//conditions:default": [],
-    }),
-    implementation_deps = [
+    })`,
+				"implementation_deps": `[
         ":lib-1",
         ":lib-2",
-    ],
-)`},
+    ]`,
+			}),
+		},
 	})
 }
 
@@ -191,17 +192,18 @@
     },
     include_build_directory: false,
 }`,
-		expectedBazelTargets: []string{`cc_library_headers(
-    name = "foo_headers",
-    implementation_deps = [":base-lib"] + select({
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library_headers", "foo_headers", attrNameToString{
+				"implementation_deps": `[":base-lib"] + select({
         "//build/bazel/platforms/os:android": [":android-lib"],
         "//build/bazel/platforms/os:darwin": [":darwin-lib"],
         "//build/bazel/platforms/os:linux": [":linux-lib"],
         "//build/bazel/platforms/os:linux_bionic": [":linux_bionic-lib"],
         "//build/bazel/platforms/os:windows": [":windows-lib"],
         "//conditions:default": [],
-    }),
-)`},
+    })`,
+			}),
+		},
 	})
 }
 
@@ -231,17 +233,18 @@
     },
     include_build_directory: false,
 }`,
-		expectedBazelTargets: []string{`cc_library_headers(
-    name = "foo_headers",
-    deps = select({
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library_headers", "foo_headers", attrNameToString{
+				"deps": `select({
         "//build/bazel/platforms/os:android": [":exported-lib"],
         "//conditions:default": [],
-    }),
-    implementation_deps = select({
+    })`,
+				"implementation_deps": `select({
         "//build/bazel/platforms/os:android": [":android-lib"],
         "//conditions:default": [],
-    }),
-)`},
+    })`,
+			}),
+		},
 	})
 }
 
@@ -288,9 +291,9 @@
     },
     include_build_directory: false,
 }`,
-		expectedBazelTargets: []string{`cc_library_headers(
-    name = "foo_headers",
-    export_system_includes = ["shared_include_dir"] + select({
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library_headers", "foo_headers", attrNameToString{
+				"export_system_includes": `["shared_include_dir"] + select({
         "//build/bazel/platforms/arch:arm": ["arm_include_dir"],
         "//build/bazel/platforms/arch:x86_64": ["x86_64_include_dir"],
         "//conditions:default": [],
@@ -299,8 +302,9 @@
         "//build/bazel/platforms/os:darwin": ["darwin_include_dir"],
         "//build/bazel/platforms/os:linux": ["linux_include_dir"],
         "//conditions:default": [],
-    }),
-)`},
+    })`,
+			}),
+		},
 	})
 }
 
@@ -330,9 +334,10 @@
     no_libcrt: true,
     include_build_directory: false,
 }`,
-		expectedBazelTargets: []string{`cc_library_headers(
-    name = "lib-1",
-    export_includes = ["lib-1"],
-)`},
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library_headers", "lib-1", attrNameToString{
+				"export_includes": `["lib-1"]`,
+			}),
+		},
 	})
 }
diff --git a/bp2build/cc_library_shared_conversion_test.go b/bp2build/cc_library_shared_conversion_test.go
index bb15776..4ec95c3 100644
--- a/bp2build/cc_library_shared_conversion_test.go
+++ b/bp2build/cc_library_shared_conversion_test.go
@@ -37,15 +37,15 @@
 
 func runCcLibrarySharedTestCase(t *testing.T, tc bp2buildTestCase) {
 	t.Helper()
+	(&tc).moduleTypeUnderTest = "cc_library_shared"
+	(&tc).moduleTypeUnderTestFactory = cc.LibrarySharedFactory
+	(&tc).moduleTypeUnderTestBp2BuildMutator = cc.CcLibrarySharedBp2Build
 	runBp2BuildTestCase(t, registerCcLibrarySharedModuleTypes, tc)
 }
 
 func TestCcLibrarySharedSimple(t *testing.T) {
 	runCcLibrarySharedTestCase(t, bp2buildTestCase{
-		description:                        "cc_library_shared simple overall test",
-		moduleTypeUnderTest:                "cc_library_shared",
-		moduleTypeUnderTestFactory:         cc.LibrarySharedFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.CcLibrarySharedBp2Build,
+		description: "cc_library_shared simple overall test",
 		filesystem: map[string]string{
 			// NOTE: include_dir headers *should not* appear in Bazel hdrs later (?)
 			"include_dir_1/include_dir_1_a.h": "",
@@ -140,52 +140,50 @@
 
     // TODO: Also support export_header_lib_headers
 }`,
-		expectedBazelTargets: []string{`cc_library_shared(
-    name = "foo_shared",
-    absolute_includes = [
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library_shared", "foo_shared", attrNameToString{
+				"absolute_includes": `[
         "include_dir_1",
         "include_dir_2",
-    ],
-    copts = [
+    ]`,
+				"copts": `[
         "-Dflag1",
         "-Dflag2",
-    ],
-    export_includes = [
+    ]`,
+				"export_includes": `[
         "export_include_dir_1",
         "export_include_dir_2",
-    ],
-    implementation_deps = [
+    ]`,
+				"implementation_deps": `[
         ":header_lib_1",
         ":header_lib_2",
-    ],
-    implementation_dynamic_deps = [
+    ]`,
+				"implementation_dynamic_deps": `[
         ":shared_lib_1",
         ":shared_lib_2",
-    ],
-    local_includes = [
+    ]`,
+				"local_includes": `[
         "local_include_dir_1",
         "local_include_dir_2",
         ".",
-    ],
-    srcs = [
+    ]`,
+				"srcs": `[
         "foo_shared1.cc",
         "foo_shared2.cc",
-    ],
-    whole_archive_deps = [
+    ]`,
+				"whole_archive_deps": `[
         ":whole_static_lib_1",
         ":whole_static_lib_2",
-    ],
-)`},
+    ]`,
+			}),
+		},
 	})
 }
 
 func TestCcLibrarySharedArchSpecificSharedLib(t *testing.T) {
 	runCcLibrarySharedTestCase(t, bp2buildTestCase{
-		description:                        "cc_library_shared arch-specific shared_libs with whole_static_libs",
-		moduleTypeUnderTest:                "cc_library_shared",
-		moduleTypeUnderTestFactory:         cc.LibrarySharedFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.CcLibrarySharedBp2Build,
-		filesystem:                         map[string]string{},
+		description: "cc_library_shared arch-specific shared_libs with whole_static_libs",
+		filesystem:  map[string]string{},
 		blueprint: soongCcLibrarySharedPreamble + `
 cc_library_static {
     name: "static_dep",
@@ -200,27 +198,25 @@
     arch: { arm64: { shared_libs: ["shared_dep"], whole_static_libs: ["static_dep"] } },
     include_build_directory: false,
 }`,
-		expectedBazelTargets: []string{`cc_library_shared(
-    name = "foo_shared",
-    implementation_dynamic_deps = select({
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library_shared", "foo_shared", attrNameToString{
+				"implementation_dynamic_deps": `select({
         "//build/bazel/platforms/arch:arm64": [":shared_dep"],
         "//conditions:default": [],
-    }),
-    whole_archive_deps = select({
+    })`,
+				"whole_archive_deps": `select({
         "//build/bazel/platforms/arch:arm64": [":static_dep"],
         "//conditions:default": [],
-    }),
-)`},
+    })`,
+			}),
+		},
 	})
 }
 
 func TestCcLibrarySharedOsSpecificSharedLib(t *testing.T) {
-	runCcLibraryStaticTestCase(t, bp2buildTestCase{
-		description:                        "cc_library_shared os-specific shared_libs",
-		moduleTypeUnderTest:                "cc_library_shared",
-		moduleTypeUnderTestFactory:         cc.LibrarySharedFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.CcLibrarySharedBp2Build,
-		filesystem:                         map[string]string{},
+	runCcLibrarySharedTestCase(t, bp2buildTestCase{
+		description: "cc_library_shared os-specific shared_libs",
+		filesystem:  map[string]string{},
 		blueprint: soongCcLibrarySharedPreamble + `
 cc_library_shared {
     name: "shared_dep",
@@ -231,23 +227,21 @@
     target: { android: { shared_libs: ["shared_dep"], } },
     include_build_directory: false,
 }`,
-		expectedBazelTargets: []string{`cc_library_shared(
-    name = "foo_shared",
-    implementation_dynamic_deps = select({
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library_shared", "foo_shared", attrNameToString{
+				"implementation_dynamic_deps": `select({
         "//build/bazel/platforms/os:android": [":shared_dep"],
         "//conditions:default": [],
-    }),
-)`},
+    })`,
+			}),
+		},
 	})
 }
 
 func TestCcLibrarySharedBaseArchOsSpecificSharedLib(t *testing.T) {
 	runCcLibrarySharedTestCase(t, bp2buildTestCase{
-		description:                        "cc_library_shared base, arch, and os-specific shared_libs",
-		moduleTypeUnderTest:                "cc_library_shared",
-		moduleTypeUnderTestFactory:         cc.LibrarySharedFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.CcLibrarySharedBp2Build,
-		filesystem:                         map[string]string{},
+		description: "cc_library_shared base, arch, and os-specific shared_libs",
+		filesystem:  map[string]string{},
 		blueprint: soongCcLibrarySharedPreamble + `
 cc_library_shared {
     name: "shared_dep",
@@ -268,25 +262,23 @@
     arch: { arm64: { shared_libs: ["shared_dep3"] } },
     include_build_directory: false,
 }`,
-		expectedBazelTargets: []string{`cc_library_shared(
-    name = "foo_shared",
-    implementation_dynamic_deps = [":shared_dep"] + select({
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library_shared", "foo_shared", attrNameToString{
+				"implementation_dynamic_deps": `[":shared_dep"] + select({
         "//build/bazel/platforms/arch:arm64": [":shared_dep3"],
         "//conditions:default": [],
     }) + select({
         "//build/bazel/platforms/os:android": [":shared_dep2"],
         "//conditions:default": [],
-    }),
-)`},
+    })`,
+			}),
+		},
 	})
 }
 
 func TestCcLibrarySharedSimpleExcludeSrcs(t *testing.T) {
 	runCcLibrarySharedTestCase(t, bp2buildTestCase{
-		description:                        "cc_library_shared simple exclude_srcs",
-		moduleTypeUnderTest:                "cc_library_shared",
-		moduleTypeUnderTestFactory:         cc.LibrarySharedFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.CcLibrarySharedBp2Build,
+		description: "cc_library_shared simple exclude_srcs",
 		filesystem: map[string]string{
 			"common.c":       "",
 			"foo-a.c":        "",
@@ -299,23 +291,21 @@
     exclude_srcs: ["foo-excluded.c"],
     include_build_directory: false,
 }`,
-		expectedBazelTargets: []string{`cc_library_shared(
-    name = "foo_shared",
-    srcs_c = [
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library_shared", "foo_shared", attrNameToString{
+				"srcs_c": `[
         "common.c",
         "foo-a.c",
-    ],
-)`},
+    ]`,
+			}),
+		},
 	})
 }
 
 func TestCcLibrarySharedStrip(t *testing.T) {
 	runCcLibrarySharedTestCase(t, bp2buildTestCase{
-		description:                        "cc_library_shared stripping",
-		moduleTypeUnderTest:                "cc_library_shared",
-		moduleTypeUnderTestFactory:         cc.LibrarySharedFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.CcLibrarySharedBp2Build,
-		filesystem:                         map[string]string{},
+		description: "cc_library_shared stripping",
+		filesystem:  map[string]string{},
 		blueprint: soongCcLibrarySharedPreamble + `
 cc_library_shared {
     name: "foo_shared",
@@ -328,9 +318,9 @@
     },
     include_build_directory: false,
 }`,
-		expectedBazelTargets: []string{`cc_library_shared(
-    name = "foo_shared",
-    strip = {
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library_shared", "foo_shared", attrNameToString{
+				"strip": `{
         "all": True,
         "keep_symbols": False,
         "keep_symbols_and_debug_frame": True,
@@ -339,17 +329,15 @@
             "sym2",
         ],
         "none": False,
-    },
-)`},
+    }`,
+			}),
+		},
 	})
 }
 
 func TestCcLibrarySharedVersionScript(t *testing.T) {
 	runCcLibrarySharedTestCase(t, bp2buildTestCase{
-		description:                        "cc_library_shared version script",
-		moduleTypeUnderTest:                "cc_library_shared",
-		moduleTypeUnderTestFactory:         cc.LibrarySharedFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.CcLibrarySharedBp2Build,
+		description: "cc_library_shared version script",
 		filesystem: map[string]string{
 			"version_script": "",
 		},
@@ -359,20 +347,18 @@
     version_script: "version_script",
     include_build_directory: false,
 }`,
-		expectedBazelTargets: []string{`cc_library_shared(
-    name = "foo_shared",
-    additional_linker_inputs = ["version_script"],
-    linkopts = ["-Wl,--version-script,$(location version_script)"],
-)`},
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library_shared", "foo_shared", attrNameToString{
+				"additional_linker_inputs": `["version_script"]`,
+				"linkopts":                 `["-Wl,--version-script,$(location version_script)"]`,
+			}),
+		},
 	})
 }
 
 func TestCcLibrarySharedNoCrtTrue(t *testing.T) {
 	runCcLibrarySharedTestCase(t, bp2buildTestCase{
-		description:                        "cc_library_shared - nocrt: true emits attribute",
-		moduleTypeUnderTest:                "cc_library_shared",
-		moduleTypeUnderTestFactory:         cc.LibrarySharedFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.CcLibrarySharedBp2Build,
+		description: "cc_library_shared - nocrt: true emits attribute",
 		filesystem: map[string]string{
 			"impl.cpp": "",
 		},
@@ -384,19 +370,18 @@
     include_build_directory: false,
 }
 `,
-		expectedBazelTargets: []string{`cc_library_shared(
-    name = "foo_shared",
-    link_crt = False,
-    srcs = ["impl.cpp"],
-)`}})
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library_shared", "foo_shared", attrNameToString{
+				"link_crt": `False`,
+				"srcs":     `["impl.cpp"]`,
+			}),
+		},
+	})
 }
 
 func TestCcLibrarySharedNoCrtFalse(t *testing.T) {
 	runCcLibrarySharedTestCase(t, bp2buildTestCase{
-		description:                        "cc_library_shared - nocrt: false doesn't emit attribute",
-		moduleTypeUnderTest:                "cc_library_shared",
-		moduleTypeUnderTestFactory:         cc.LibrarySharedFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.CcLibrarySharedBp2Build,
+		description: "cc_library_shared - nocrt: false doesn't emit attribute",
 		filesystem: map[string]string{
 			"impl.cpp": "",
 		},
@@ -408,18 +393,17 @@
     include_build_directory: false,
 }
 `,
-		expectedBazelTargets: []string{`cc_library_shared(
-    name = "foo_shared",
-    srcs = ["impl.cpp"],
-)`}})
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library_shared", "foo_shared", attrNameToString{
+				"srcs": `["impl.cpp"]`,
+			}),
+		},
+	})
 }
 
 func TestCcLibrarySharedNoCrtArchVariant(t *testing.T) {
 	runCcLibrarySharedTestCase(t, bp2buildTestCase{
-		description:                        "cc_library_shared - nocrt in select",
-		moduleTypeUnderTest:                "cc_library_shared",
-		moduleTypeUnderTestFactory:         cc.LibrarySharedFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.CcLibrarySharedBp2Build,
+		description: "cc_library_shared - nocrt in select",
 		filesystem: map[string]string{
 			"impl.cpp": "",
 		},
diff --git a/bp2build/cc_library_static_conversion_test.go b/bp2build/cc_library_static_conversion_test.go
index 9887811..2f760d2 100644
--- a/bp2build/cc_library_static_conversion_test.go
+++ b/bp2build/cc_library_static_conversion_test.go
@@ -79,15 +79,16 @@
 
 func runCcLibraryStaticTestCase(t *testing.T, tc bp2buildTestCase) {
 	t.Helper()
+
+	(&tc).moduleTypeUnderTest = "cc_library_static"
+	(&tc).moduleTypeUnderTestFactory = cc.LibraryStaticFactory
+	(&tc).moduleTypeUnderTestBp2BuildMutator = cc.CcLibraryStaticBp2Build
 	runBp2BuildTestCase(t, registerCcLibraryStaticModuleTypes, tc)
 }
 
 func TestCcLibraryStaticSimple(t *testing.T) {
 	runCcLibraryStaticTestCase(t, bp2buildTestCase{
-		description:                        "cc_library_static test",
-		moduleTypeUnderTest:                "cc_library_static",
-		moduleTypeUnderTestFactory:         cc.LibraryStaticFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.CcLibraryStaticBp2Build,
+		description: "cc_library_static test",
 		filesystem: map[string]string{
 			// NOTE: include_dir headers *should not* appear in Bazel hdrs later (?)
 			"include_dir_1/include_dir_1_a.h": "",
@@ -182,49 +183,47 @@
 
     // TODO: Also support export_header_lib_headers
 }`,
-		expectedBazelTargets: []string{`cc_library_static(
-    name = "foo_static",
-    absolute_includes = [
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library_static", "foo_static", attrNameToString{
+				"absolute_includes": `[
         "include_dir_1",
         "include_dir_2",
-    ],
-    copts = [
+    ]`,
+				"copts": `[
         "-Dflag1",
         "-Dflag2",
-    ],
-    export_includes = [
+    ]`,
+				"export_includes": `[
         "export_include_dir_1",
         "export_include_dir_2",
-    ],
-    implementation_deps = [
+    ]`,
+				"implementation_deps": `[
         ":header_lib_1",
         ":header_lib_2",
         ":static_lib_1",
         ":static_lib_2",
-    ],
-    local_includes = [
+    ]`,
+				"local_includes": `[
         "local_include_dir_1",
         "local_include_dir_2",
         ".",
-    ],
-    srcs = [
+    ]`,
+				"srcs": `[
         "foo_static1.cc",
         "foo_static2.cc",
-    ],
-    whole_archive_deps = [
+    ]`,
+				"whole_archive_deps": `[
         ":whole_static_lib_1",
         ":whole_static_lib_2",
-    ],
-)`},
+    ]`,
+			}),
+		},
 	})
 }
 
 func TestCcLibraryStaticSubpackage(t *testing.T) {
 	runCcLibraryStaticTestCase(t, bp2buildTestCase{
-		description:                        "cc_library_static subpackage test",
-		moduleTypeUnderTest:                "cc_library_static",
-		moduleTypeUnderTestFactory:         cc.LibraryStaticFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.CcLibraryStaticBp2Build,
+		description: "cc_library_static subpackage test",
 		filesystem: map[string]string{
 			// subpackage with subdirectory
 			"subpackage/Android.bp":                         "",
@@ -247,20 +246,18 @@
         "subpackage",
     ],
 }`,
-		expectedBazelTargets: []string{`cc_library_static(
-    name = "foo_static",
-    absolute_includes = ["subpackage"],
-    local_includes = ["."],
-)`},
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library_static", "foo_static", attrNameToString{
+				"absolute_includes": `["subpackage"]`,
+				"local_includes":    `["."]`,
+			}),
+		},
 	})
 }
 
 func TestCcLibraryStaticExportIncludeDir(t *testing.T) {
 	runCcLibraryStaticTestCase(t, bp2buildTestCase{
-		description:                        "cc_library_static export include dir",
-		moduleTypeUnderTest:                "cc_library_static",
-		moduleTypeUnderTestFactory:         cc.LibraryStaticFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.CcLibraryStaticBp2Build,
+		description: "cc_library_static export include dir",
 		filesystem: map[string]string{
 			// subpackage with subdirectory
 			"subpackage/Android.bp":                         "",
@@ -273,19 +270,17 @@
     export_include_dirs: ["subpackage"],
     include_build_directory: false,
 }`,
-		expectedBazelTargets: []string{`cc_library_static(
-    name = "foo_static",
-    export_includes = ["subpackage"],
-)`},
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library_static", "foo_static", attrNameToString{
+				"export_includes": `["subpackage"]`,
+			}),
+		},
 	})
 }
 
 func TestCcLibraryStaticExportSystemIncludeDir(t *testing.T) {
 	runCcLibraryStaticTestCase(t, bp2buildTestCase{
-		description:                        "cc_library_static export system include dir",
-		moduleTypeUnderTest:                "cc_library_static",
-		moduleTypeUnderTestFactory:         cc.LibraryStaticFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.CcLibraryStaticBp2Build,
+		description: "cc_library_static export system include dir",
 		filesystem: map[string]string{
 			// subpackage with subdirectory
 			"subpackage/Android.bp":                         "",
@@ -298,20 +293,18 @@
     export_system_include_dirs: ["subpackage"],
     include_build_directory: false,
 }`,
-		expectedBazelTargets: []string{`cc_library_static(
-    name = "foo_static",
-    export_system_includes = ["subpackage"],
-)`},
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library_static", "foo_static", attrNameToString{
+				"export_system_includes": `["subpackage"]`,
+			}),
+		},
 	})
 }
 
 func TestCcLibraryStaticManyIncludeDirs(t *testing.T) {
 	runCcLibraryStaticTestCase(t, bp2buildTestCase{
-		description:                        "cc_library_static include_dirs, local_include_dirs, export_include_dirs (b/183742505)",
-		moduleTypeUnderTest:                "cc_library_static",
-		moduleTypeUnderTestFactory:         cc.LibraryStaticFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.CcLibraryStaticBp2Build,
-		dir:                                "subpackage",
+		description: "cc_library_static include_dirs, local_include_dirs, export_include_dirs (b/183742505)",
+		dir:         "subpackage",
 		filesystem: map[string]string{
 			// subpackage with subdirectory
 			"subpackage/Android.bp": `
@@ -335,28 +328,25 @@
 			"subpackage3/subsubpackage/header.h":         "",
 		},
 		blueprint: soongCcLibraryStaticPreamble,
-		expectedBazelTargets: []string{`cc_library_static(
-    name = "foo_static",
-    absolute_includes = [
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library_static", "foo_static", attrNameToString{
+				"absolute_includes": `[
         "subpackage/subsubpackage",
         "subpackage2",
         "subpackage3/subsubpackage",
-    ],
-    export_includes = ["./exported_subsubpackage"],
-    local_includes = [
+    ]`,
+				"export_includes": `["./exported_subsubpackage"]`,
+				"local_includes": `[
         "subsubpackage2",
         ".",
-    ],
-)`},
+    ]`,
+			})},
 	})
 }
 
 func TestCcLibraryStaticIncludeBuildDirectoryDisabled(t *testing.T) {
 	runCcLibraryStaticTestCase(t, bp2buildTestCase{
-		description:                        "cc_library_static include_build_directory disabled",
-		moduleTypeUnderTest:                "cc_library_static",
-		moduleTypeUnderTestFactory:         cc.LibraryStaticFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.CcLibraryStaticBp2Build,
+		description: "cc_library_static include_build_directory disabled",
 		filesystem: map[string]string{
 			// subpackage with subdirectory
 			"subpackage/Android.bp":                         "",
@@ -370,20 +360,18 @@
     local_include_dirs: ["subpackage2"],
     include_build_directory: false,
 }`,
-		expectedBazelTargets: []string{`cc_library_static(
-    name = "foo_static",
-    absolute_includes = ["subpackage"],
-    local_includes = ["subpackage2"],
-)`},
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library_static", "foo_static", attrNameToString{
+				"absolute_includes": `["subpackage"]`,
+				"local_includes":    `["subpackage2"]`,
+			}),
+		},
 	})
 }
 
 func TestCcLibraryStaticIncludeBuildDirectoryEnabled(t *testing.T) {
 	runCcLibraryStaticTestCase(t, bp2buildTestCase{
-		description:                        "cc_library_static include_build_directory enabled",
-		moduleTypeUnderTest:                "cc_library_static",
-		moduleTypeUnderTestFactory:         cc.LibraryStaticFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.CcLibraryStaticBp2Build,
+		description: "cc_library_static include_build_directory enabled",
 		filesystem: map[string]string{
 			// subpackage with subdirectory
 			"subpackage/Android.bp":                         "",
@@ -399,24 +387,22 @@
     local_include_dirs: ["subpackage2"],
     include_build_directory: true,
 }`,
-		expectedBazelTargets: []string{`cc_library_static(
-    name = "foo_static",
-    absolute_includes = ["subpackage"],
-    local_includes = [
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library_static", "foo_static", attrNameToString{
+				"absolute_includes": `["subpackage"]`,
+				"local_includes": `[
         "subpackage2",
         ".",
-    ],
-)`},
+    ]`,
+			}),
+		},
 	})
 }
 
 func TestCcLibraryStaticArchSpecificStaticLib(t *testing.T) {
 	runCcLibraryStaticTestCase(t, bp2buildTestCase{
-		description:                        "cc_library_static arch-specific static_libs",
-		moduleTypeUnderTest:                "cc_library_static",
-		moduleTypeUnderTestFactory:         cc.LibraryStaticFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.CcLibraryStaticBp2Build,
-		filesystem:                         map[string]string{},
+		description: "cc_library_static arch-specific static_libs",
+		filesystem:  map[string]string{},
 		blueprint: soongCcLibraryStaticPreamble + `
 cc_library_static {
     name: "static_dep",
@@ -431,27 +417,25 @@
     arch: { arm64: { static_libs: ["static_dep"], whole_static_libs: ["static_dep2"] } },
     include_build_directory: false,
 }`,
-		expectedBazelTargets: []string{`cc_library_static(
-    name = "foo_static",
-    implementation_deps = select({
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library_static", "foo_static", attrNameToString{
+				"implementation_deps": `select({
         "//build/bazel/platforms/arch:arm64": [":static_dep"],
         "//conditions:default": [],
-    }),
-    whole_archive_deps = select({
+    })`,
+				"whole_archive_deps": `select({
         "//build/bazel/platforms/arch:arm64": [":static_dep2"],
         "//conditions:default": [],
-    }),
-)`},
+    })`,
+			}),
+		},
 	})
 }
 
 func TestCcLibraryStaticOsSpecificStaticLib(t *testing.T) {
 	runCcLibraryStaticTestCase(t, bp2buildTestCase{
-		description:                        "cc_library_static os-specific static_libs",
-		moduleTypeUnderTest:                "cc_library_static",
-		moduleTypeUnderTestFactory:         cc.LibraryStaticFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.CcLibraryStaticBp2Build,
-		filesystem:                         map[string]string{},
+		description: "cc_library_static os-specific static_libs",
+		filesystem:  map[string]string{},
 		blueprint: soongCcLibraryStaticPreamble + `
 cc_library_static {
     name: "static_dep",
@@ -466,27 +450,25 @@
     target: { android: { static_libs: ["static_dep"], whole_static_libs: ["static_dep2"] } },
     include_build_directory: false,
 }`,
-		expectedBazelTargets: []string{`cc_library_static(
-    name = "foo_static",
-    implementation_deps = select({
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library_static", "foo_static", attrNameToString{
+				"implementation_deps": `select({
         "//build/bazel/platforms/os:android": [":static_dep"],
         "//conditions:default": [],
-    }),
-    whole_archive_deps = select({
+    })`,
+				"whole_archive_deps": `select({
         "//build/bazel/platforms/os:android": [":static_dep2"],
         "//conditions:default": [],
-    }),
-)`},
+    })`,
+			}),
+		},
 	})
 }
 
 func TestCcLibraryStaticBaseArchOsSpecificStaticLib(t *testing.T) {
 	runCcLibraryStaticTestCase(t, bp2buildTestCase{
-		description:                        "cc_library_static base, arch and os-specific static_libs",
-		moduleTypeUnderTest:                "cc_library_static",
-		moduleTypeUnderTestFactory:         cc.LibraryStaticFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.CcLibraryStaticBp2Build,
-		filesystem:                         map[string]string{},
+		description: "cc_library_static base, arch and os-specific static_libs",
+		filesystem:  map[string]string{},
 		blueprint: soongCcLibraryStaticPreamble + `
 cc_library_static {
     name: "static_dep",
@@ -512,26 +494,24 @@
     arch: { arm64: { static_libs: ["static_dep4"] } },
     include_build_directory: false,
 }`,
-		expectedBazelTargets: []string{`cc_library_static(
-    name = "foo_static",
-    implementation_deps = [":static_dep"] + select({
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library_static", "foo_static", attrNameToString{
+				"implementation_deps": `[":static_dep"] + select({
         "//build/bazel/platforms/arch:arm64": [":static_dep4"],
         "//conditions:default": [],
     }) + select({
         "//build/bazel/platforms/os:android": [":static_dep3"],
         "//conditions:default": [],
-    }),
-    whole_archive_deps = [":static_dep2"],
-)`},
+    })`,
+				"whole_archive_deps": `[":static_dep2"]`,
+			}),
+		},
 	})
 }
 
 func TestCcLibraryStaticSimpleExcludeSrcs(t *testing.T) {
 	runCcLibraryStaticTestCase(t, bp2buildTestCase{
-		description:                        "cc_library_static simple exclude_srcs",
-		moduleTypeUnderTest:                "cc_library_static",
-		moduleTypeUnderTestFactory:         cc.LibraryStaticFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.CcLibraryStaticBp2Build,
+		description: "cc_library_static simple exclude_srcs",
 		filesystem: map[string]string{
 			"common.c":       "",
 			"foo-a.c":        "",
@@ -544,22 +524,20 @@
     exclude_srcs: ["foo-excluded.c"],
     include_build_directory: false,
 }`,
-		expectedBazelTargets: []string{`cc_library_static(
-    name = "foo_static",
-    srcs_c = [
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library_static", "foo_static", attrNameToString{
+				"srcs_c": `[
         "common.c",
         "foo-a.c",
-    ],
-)`},
+    ]`,
+			}),
+		},
 	})
 }
 
 func TestCcLibraryStaticOneArchSrcs(t *testing.T) {
 	runCcLibraryStaticTestCase(t, bp2buildTestCase{
-		description:                        "cc_library_static one arch specific srcs",
-		moduleTypeUnderTest:                "cc_library_static",
-		moduleTypeUnderTestFactory:         cc.LibraryStaticFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.CcLibraryStaticBp2Build,
+		description: "cc_library_static one arch specific srcs",
 		filesystem: map[string]string{
 			"common.c":  "",
 			"foo-arm.c": "",
@@ -571,22 +549,20 @@
     arch: { arm: { srcs: ["foo-arm.c"] } },
     include_build_directory: false,
 }`,
-		expectedBazelTargets: []string{`cc_library_static(
-    name = "foo_static",
-    srcs_c = ["common.c"] + select({
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library_static", "foo_static", attrNameToString{
+				"srcs_c": `["common.c"] + select({
         "//build/bazel/platforms/arch:arm": ["foo-arm.c"],
         "//conditions:default": [],
-    }),
-)`},
+    })`,
+			}),
+		},
 	})
 }
 
 func TestCcLibraryStaticOneArchSrcsExcludeSrcs(t *testing.T) {
 	runCcLibraryStaticTestCase(t, bp2buildTestCase{
-		description:                        "cc_library_static one arch specific srcs and exclude_srcs",
-		moduleTypeUnderTest:                "cc_library_static",
-		moduleTypeUnderTestFactory:         cc.LibraryStaticFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.CcLibraryStaticBp2Build,
+		description: "cc_library_static one arch specific srcs and exclude_srcs",
 		filesystem: map[string]string{
 			"common.c":           "",
 			"for-arm.c":          "",
@@ -603,22 +579,20 @@
     },
     include_build_directory: false,
 }`,
-		expectedBazelTargets: []string{`cc_library_static(
-    name = "foo_static",
-    srcs_c = ["common.c"] + select({
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library_static", "foo_static", attrNameToString{
+				"srcs_c": `["common.c"] + select({
         "//build/bazel/platforms/arch:arm": ["for-arm.c"],
         "//conditions:default": ["not-for-arm.c"],
-    }),
-)`},
+    })`,
+			}),
+		},
 	})
 }
 
 func TestCcLibraryStaticTwoArchExcludeSrcs(t *testing.T) {
 	runCcLibraryStaticTestCase(t, bp2buildTestCase{
-		description:                        "cc_library_static arch specific exclude_srcs for 2 architectures",
-		moduleTypeUnderTest:                "cc_library_static",
-		moduleTypeUnderTestFactory:         cc.LibraryStaticFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.CcLibraryStaticBp2Build,
+		description: "cc_library_static arch specific exclude_srcs for 2 architectures",
 		filesystem: map[string]string{
 			"common.c":      "",
 			"for-arm.c":     "",
@@ -637,9 +611,9 @@
     },
     include_build_directory: false,
 } `,
-		expectedBazelTargets: []string{`cc_library_static(
-    name = "foo_static",
-    srcs_c = ["common.c"] + select({
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library_static", "foo_static", attrNameToString{
+				"srcs_c": `["common.c"] + select({
         "//build/bazel/platforms/arch:arm": [
             "not-for-x86.c",
             "for-arm.c",
@@ -652,16 +626,15 @@
             "not-for-arm.c",
             "not-for-x86.c",
         ],
-    }),
-)`},
+    })`,
+			}),
+		},
 	})
 }
+
 func TestCcLibraryStaticFourArchExcludeSrcs(t *testing.T) {
 	runCcLibraryStaticTestCase(t, bp2buildTestCase{
-		description:                        "cc_library_static arch specific exclude_srcs for 4 architectures",
-		moduleTypeUnderTest:                "cc_library_static",
-		moduleTypeUnderTestFactory:         cc.LibraryStaticFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.CcLibraryStaticBp2Build,
+		description: "cc_library_static arch specific exclude_srcs for 4 architectures",
 		filesystem: map[string]string{
 			"common.c":             "",
 			"for-arm.c":            "",
@@ -687,9 +660,9 @@
   },
     include_build_directory: false,
 } `,
-		expectedBazelTargets: []string{`cc_library_static(
-    name = "foo_static",
-    srcs_c = ["common.c"] + select({
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library_static", "foo_static", attrNameToString{
+				"srcs_c": `["common.c"] + select({
         "//build/bazel/platforms/arch:arm": [
             "not-for-arm64.c",
             "not-for-x86.c",
@@ -720,17 +693,15 @@
             "not-for-x86.c",
             "not-for-x86_64.c",
         ],
-    }),
-)`},
+    })`,
+			}),
+		},
 	})
 }
 
 func TestCcLibraryStaticOneArchEmpty(t *testing.T) {
 	runCcLibraryStaticTestCase(t, bp2buildTestCase{
-		description:                        "cc_library_static one arch empty",
-		moduleTypeUnderTest:                "cc_library_static",
-		moduleTypeUnderTestFactory:         cc.LibraryStaticFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.CcLibraryStaticBp2Build,
+		description: "cc_library_static one arch empty",
 		filesystem: map[string]string{
 			"common.cc":       "",
 			"foo-no-arm.cc":   "",
@@ -746,22 +717,20 @@
     },
     include_build_directory: false,
 }`,
-		expectedBazelTargets: []string{`cc_library_static(
-    name = "foo_static",
-    srcs = ["common.cc"] + select({
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library_static", "foo_static", attrNameToString{
+				"srcs": `["common.cc"] + select({
         "//build/bazel/platforms/arch:arm": [],
         "//conditions:default": ["foo-no-arm.cc"],
-    }),
-)`},
+    })`,
+			}),
+		},
 	})
 }
 
 func TestCcLibraryStaticOneArchEmptyOtherSet(t *testing.T) {
 	runCcLibraryStaticTestCase(t, bp2buildTestCase{
-		description:                        "cc_library_static one arch empty other set",
-		moduleTypeUnderTest:                "cc_library_static",
-		moduleTypeUnderTestFactory:         cc.LibraryStaticFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.CcLibraryStaticBp2Build,
+		description: "cc_library_static one arch empty other set",
 		filesystem: map[string]string{
 			"common.cc":       "",
 			"foo-no-arm.cc":   "",
@@ -779,27 +748,25 @@
     },
     include_build_directory: false,
 }`,
-		expectedBazelTargets: []string{`cc_library_static(
-    name = "foo_static",
-    srcs = ["common.cc"] + select({
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library_static", "foo_static", attrNameToString{
+				"srcs": `["common.cc"] + select({
         "//build/bazel/platforms/arch:arm": [],
         "//build/bazel/platforms/arch:x86": [
             "foo-no-arm.cc",
             "x86-only.cc",
         ],
         "//conditions:default": ["foo-no-arm.cc"],
-    }),
-)`},
+    })`,
+			}),
+		},
 	})
 }
 
 func TestCcLibraryStaticMultipleDepSameName(t *testing.T) {
 	runCcLibraryStaticTestCase(t, bp2buildTestCase{
-		description:                        "cc_library_static multiple dep same name panic",
-		moduleTypeUnderTest:                "cc_library_static",
-		moduleTypeUnderTestFactory:         cc.LibraryStaticFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.CcLibraryStaticBp2Build,
-		filesystem:                         map[string]string{},
+		description: "cc_library_static multiple dep same name panic",
+		filesystem:  map[string]string{},
 		blueprint: soongCcLibraryStaticPreamble + `
 cc_library_static {
     name: "static_dep",
@@ -810,19 +777,17 @@
     static_libs: ["static_dep", "static_dep"],
     include_build_directory: false,
 }`,
-		expectedBazelTargets: []string{`cc_library_static(
-    name = "foo_static",
-    implementation_deps = [":static_dep"],
-)`},
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library_static", "foo_static", attrNameToString{
+				"implementation_deps": `[":static_dep"]`,
+			}),
+		},
 	})
 }
 
 func TestCcLibraryStaticOneMultilibSrcsExcludeSrcs(t *testing.T) {
 	runCcLibraryStaticTestCase(t, bp2buildTestCase{
-		description:                        "cc_library_static 1 multilib srcs and exclude_srcs",
-		moduleTypeUnderTest:                "cc_library_static",
-		moduleTypeUnderTestFactory:         cc.LibraryStaticFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.CcLibraryStaticBp2Build,
+		description: "cc_library_static 1 multilib srcs and exclude_srcs",
 		filesystem: map[string]string{
 			"common.c":        "",
 			"for-lib32.c":     "",
@@ -837,23 +802,21 @@
     },
     include_build_directory: false,
 } `,
-		expectedBazelTargets: []string{`cc_library_static(
-    name = "foo_static",
-    srcs_c = ["common.c"] + select({
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library_static", "foo_static", attrNameToString{
+				"srcs_c": `["common.c"] + select({
         "//build/bazel/platforms/arch:arm": ["for-lib32.c"],
         "//build/bazel/platforms/arch:x86": ["for-lib32.c"],
         "//conditions:default": ["not-for-lib32.c"],
-    }),
-)`},
+    })`,
+			}),
+		},
 	})
 }
 
 func TestCcLibraryStaticTwoMultilibSrcsExcludeSrcs(t *testing.T) {
 	runCcLibraryStaticTestCase(t, bp2buildTestCase{
-		description:                        "cc_library_static 2 multilib srcs and exclude_srcs",
-		moduleTypeUnderTest:                "cc_library_static",
-		moduleTypeUnderTestFactory:         cc.LibraryStaticFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.CcLibraryStaticBp2Build,
+		description: "cc_library_static 2 multilib srcs and exclude_srcs",
 		filesystem: map[string]string{
 			"common.c":        "",
 			"for-lib32.c":     "",
@@ -863,7 +826,7 @@
 		},
 		blueprint: soongCcLibraryStaticPreamble + `
 cc_library_static {
-    name: "foo_static2",
+    name: "foo_static",
     srcs: ["common.c", "not-for-*.c"],
     multilib: {
         lib32: { srcs: ["for-lib32.c"], exclude_srcs: ["not-for-lib32.c"] },
@@ -871,9 +834,9 @@
     },
     include_build_directory: false,
 } `,
-		expectedBazelTargets: []string{`cc_library_static(
-    name = "foo_static2",
-    srcs_c = ["common.c"] + select({
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library_static", "foo_static", attrNameToString{
+				"srcs_c": `["common.c"] + select({
         "//build/bazel/platforms/arch:arm": [
             "not-for-lib64.c",
             "for-lib32.c",
@@ -894,17 +857,15 @@
             "not-for-lib32.c",
             "not-for-lib64.c",
         ],
-    }),
-)`},
+    })`,
+			}),
+		},
 	})
 }
 
 func TestCcLibrarySTaticArchMultilibSrcsExcludeSrcs(t *testing.T) {
 	runCcLibraryStaticTestCase(t, bp2buildTestCase{
-		description:                        "cc_library_static arch and multilib srcs and exclude_srcs",
-		moduleTypeUnderTest:                "cc_library_static",
-		moduleTypeUnderTestFactory:         cc.LibraryStaticFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.CcLibraryStaticBp2Build,
+		description: "cc_library_static arch and multilib srcs and exclude_srcs",
 		filesystem: map[string]string{
 			"common.c":             "",
 			"for-arm.c":            "",
@@ -923,7 +884,7 @@
 		},
 		blueprint: soongCcLibraryStaticPreamble + `
 cc_library_static {
-   name: "foo_static3",
+   name: "foo_static",
    srcs: ["common.c", "not-for-*.c"],
    exclude_srcs: ["not-for-everything.c"],
    arch: {
@@ -938,9 +899,9 @@
    },
     include_build_directory: false,
 }`,
-		expectedBazelTargets: []string{`cc_library_static(
-    name = "foo_static3",
-    srcs_c = ["common.c"] + select({
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library_static", "foo_static", attrNameToString{
+				"srcs_c": `["common.c"] + select({
         "//build/bazel/platforms/arch:arm": [
             "not-for-arm64.c",
             "not-for-lib64.c",
@@ -981,16 +942,14 @@
             "not-for-x86.c",
             "not-for-x86_64.c",
         ],
-    }),
-)`},
+    })`,
+			}),
+		},
 	})
 }
 
 func TestCcLibraryStaticGeneratedHeadersAllPartitions(t *testing.T) {
 	runCcLibraryStaticTestCase(t, bp2buildTestCase{
-		moduleTypeUnderTest:                "cc_library_static",
-		moduleTypeUnderTestFactory:         cc.LibraryStaticFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.CcLibraryStaticBp2Build,
 		blueprint: soongCcLibraryStaticPreamble + `
 genrule {
     name: "generated_hdr",
@@ -1009,31 +968,29 @@
     export_generated_headers: ["export_generated_hdr"],
     include_build_directory: false,
 }`,
-		expectedBazelTargets: []string{`cc_library_static(
-    name = "foo_static",
-    hdrs = [":export_generated_hdr"],
-    srcs = [
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library_static", "foo_static", attrNameToString{
+				"hdrs": `[":export_generated_hdr"]`,
+				"srcs": `[
         "cpp_src.cpp",
         ":generated_hdr",
-    ],
-    srcs_as = [
+    ]`,
+				"srcs_as": `[
         "as_src.S",
         ":generated_hdr",
-    ],
-    srcs_c = [
+    ]`,
+				"srcs_c": `[
         "c_src.c",
         ":generated_hdr",
-    ],
-)`},
+    ]`,
+			}),
+		},
 	})
 }
 
 func TestCcLibraryStaticArchSrcsExcludeSrcsGeneratedFiles(t *testing.T) {
 	runCcLibraryStaticTestCase(t, bp2buildTestCase{
-		description:                        "cc_library_static arch srcs/exclude_srcs with generated files",
-		moduleTypeUnderTest:                "cc_library_static",
-		moduleTypeUnderTestFactory:         cc.LibraryStaticFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.CcLibraryStaticBp2Build,
+		description: "cc_library_static arch srcs/exclude_srcs with generated files",
 		filesystem: map[string]string{
 			"common.cpp":             "",
 			"for-x86.cpp":            "",
@@ -1082,7 +1039,7 @@
 }
 
 cc_library_static {
-    name: "foo_static3",
+    name: "foo_static",
     srcs: ["common.cpp", "not-for-*.cpp"],
     exclude_srcs: ["not-for-everything.cpp"],
     generated_sources: ["generated_src", "generated_src_other_pkg", "generated_src_not_x86"],
@@ -1105,9 +1062,9 @@
     include_build_directory: false,
 }
 `,
-		expectedBazelTargets: []string{`cc_library_static(
-    name = "foo_static3",
-    srcs = [
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library_static", "foo_static", attrNameToString{
+				"srcs": `[
         "common.cpp",
         ":generated_src",
         "//dep:generated_src_other_pkg",
@@ -1128,18 +1085,16 @@
             "//dep:generated_hdr_other_pkg_android",
         ],
         "//conditions:default": [],
-    }),
-)`},
+    })`,
+			}),
+		},
 	})
 }
 
 func TestCcLibraryStaticGetTargetProperties(t *testing.T) {
 	runCcLibraryStaticTestCase(t, bp2buildTestCase{
 
-		description:                        "cc_library_static complex GetTargetProperties",
-		moduleTypeUnderTest:                "cc_library_static",
-		moduleTypeUnderTestFactory:         cc.LibraryStaticFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.CcLibraryStaticBp2Build,
+		description: "cc_library_static complex GetTargetProperties",
 		blueprint: soongCcLibraryStaticPreamble + `
 cc_library_static {
     name: "foo_static",
@@ -1168,9 +1123,9 @@
     },
     include_build_directory: false,
 }`,
-		expectedBazelTargets: []string{`cc_library_static(
-    name = "foo_static",
-    srcs_c = select({
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library_static", "foo_static", attrNameToString{
+				"srcs_c": `select({
         "//build/bazel/platforms/os:android": ["android_src.c"],
         "//conditions:default": [],
     }) + select({
@@ -1181,17 +1136,15 @@
         "//build/bazel/platforms/os_arch:linux_bionic_arm64": ["linux_bionic_arm64_src.c"],
         "//build/bazel/platforms/os_arch:linux_bionic_x86_64": ["linux_bionic_x86_64_src.c"],
         "//conditions:default": [],
-    }),
-)`},
+    })`,
+			}),
+		},
 	})
 }
 
 func TestCcLibraryStaticProductVariableSelects(t *testing.T) {
 	runCcLibraryStaticTestCase(t, bp2buildTestCase{
-		description:                        "cc_library_static product variable selects",
-		moduleTypeUnderTest:                "cc_library_static",
-		moduleTypeUnderTestFactory:         cc.LibraryStaticFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.CcLibraryStaticBp2Build,
+		description: "cc_library_static product variable selects",
 		blueprint: soongCcLibraryStaticPreamble + `
 cc_library_static {
     name: "foo_static",
@@ -1209,9 +1162,9 @@
     },
     include_build_directory: false,
 } `,
-		expectedBazelTargets: []string{`cc_library_static(
-    name = "foo_static",
-    copts = select({
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library_static", "foo_static", attrNameToString{
+				"copts": `select({
         "//build/bazel/product_variables:binder32bit": ["-Wbinder32bit"],
         "//conditions:default": [],
     }) + select({
@@ -1220,19 +1173,17 @@
     }) + select({
         "//build/bazel/product_variables:malloc_zero_contents": ["-Wmalloc_zero_contents"],
         "//conditions:default": [],
-    }),
-    srcs_c = ["common.c"],
-)`},
+    })`,
+				"srcs_c": `["common.c"]`,
+			}),
+		},
 	})
 }
 
 func TestCcLibraryStaticProductVariableArchSpecificSelects(t *testing.T) {
 	runCcLibraryStaticTestCase(t, bp2buildTestCase{
-		description:                        "cc_library_static arch-specific product variable selects",
-		moduleTypeUnderTest:                "cc_library_static",
-		moduleTypeUnderTestFactory:         cc.LibraryStaticFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.CcLibraryStaticBp2Build,
-		filesystem:                         map[string]string{},
+		description: "cc_library_static arch-specific product variable selects",
+		filesystem:  map[string]string{},
 		blueprint: soongCcLibraryStaticPreamble + `
 cc_library_static {
     name: "foo_static",
@@ -1271,9 +1222,9 @@
     },
     include_build_directory: false,
 } `,
-		expectedBazelTargets: []string{`cc_library_static(
-    name = "foo_static",
-    copts = select({
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library_static", "foo_static", attrNameToString{
+				"copts": `select({
         "//build/bazel/product_variables:malloc_not_svelte": ["-Wmalloc_not_svelte"],
         "//conditions:default": [],
     }) + select({
@@ -1288,19 +1239,17 @@
     }) + select({
         "//build/bazel/product_variables:malloc_not_svelte-x86": ["-Wlib32_malloc_not_svelte"],
         "//conditions:default": [],
-    }),
-    srcs_c = ["common.c"],
-)`},
+    })`,
+				"srcs_c": `["common.c"]`,
+			}),
+		},
 	})
 }
 
 func TestCcLibraryStaticProductVariableStringReplacement(t *testing.T) {
 	runCcLibraryStaticTestCase(t, bp2buildTestCase{
-		description:                        "cc_library_static product variable string replacement",
-		moduleTypeUnderTest:                "cc_library_static",
-		moduleTypeUnderTestFactory:         cc.LibraryStaticFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.CcLibraryStaticBp2Build,
-		filesystem:                         map[string]string{},
+		description: "cc_library_static product variable string replacement",
+		filesystem:  map[string]string{},
 		blueprint: soongCcLibraryStaticPreamble + `
 cc_library_static {
     name: "foo_static",
@@ -1312,23 +1261,21 @@
     },
     include_build_directory: false,
 } `,
-		expectedBazelTargets: []string{`cc_library_static(
-    name = "foo_static",
-    asflags = select({
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library_static", "foo_static", attrNameToString{
+				"asflags": `select({
         "//build/bazel/product_variables:platform_sdk_version": ["-DPLATFORM_SDK_VERSION=$(Platform_sdk_version)"],
         "//conditions:default": [],
-    }),
-    srcs_as = ["common.S"],
-)`},
+    })`,
+				"srcs_as": `["common.S"]`,
+			}),
+		},
 	})
 }
 
 func TestStaticLibrary_SystemSharedLibsRootEmpty(t *testing.T) {
 	runCcLibraryStaticTestCase(t, bp2buildTestCase{
-		description:                        "cc_library_static system_shared_lib empty root",
-		moduleTypeUnderTest:                "cc_library_static",
-		moduleTypeUnderTestFactory:         cc.LibraryStaticFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.CcLibraryStaticBp2Build,
+		description: "cc_library_static system_shared_lib empty root",
 		blueprint: soongCcLibraryStaticPreamble + `
 cc_library_static {
     name: "root_empty",
@@ -1336,19 +1283,17 @@
     include_build_directory: false,
 }
 `,
-		expectedBazelTargets: []string{`cc_library_static(
-    name = "root_empty",
-    system_dynamic_deps = [],
-)`},
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library_static", "root_empty", attrNameToString{
+				"system_dynamic_deps": `[]`,
+			}),
+		},
 	})
 }
 
 func TestStaticLibrary_SystemSharedLibsStaticEmpty(t *testing.T) {
 	runCcLibraryStaticTestCase(t, bp2buildTestCase{
-		description:                        "cc_library_static system_shared_lib empty static default",
-		moduleTypeUnderTest:                "cc_library_static",
-		moduleTypeUnderTestFactory:         cc.LibraryStaticFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.CcLibraryStaticBp2Build,
+		description: "cc_library_static system_shared_lib empty static default",
 		blueprint: soongCcLibraryStaticPreamble + `
 cc_defaults {
     name: "static_empty_defaults",
@@ -1362,19 +1307,17 @@
     defaults: ["static_empty_defaults"],
 }
 `,
-		expectedBazelTargets: []string{`cc_library_static(
-    name = "static_empty",
-    system_dynamic_deps = [],
-)`},
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library_static", "static_empty", attrNameToString{
+				"system_dynamic_deps": `[]`,
+			}),
+		},
 	})
 }
 
 func TestStaticLibrary_SystemSharedLibsBionicEmpty(t *testing.T) {
 	runCcLibraryStaticTestCase(t, bp2buildTestCase{
-		description:                        "cc_library_static system_shared_lib empty for bionic variant",
-		moduleTypeUnderTest:                "cc_library_static",
-		moduleTypeUnderTestFactory:         cc.LibraryStaticFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.CcLibraryStaticBp2Build,
+		description: "cc_library_static system_shared_lib empty for bionic variant",
 		blueprint: soongCcLibraryStaticPreamble + `
 cc_library_static {
     name: "target_bionic_empty",
@@ -1386,10 +1329,11 @@
     include_build_directory: false,
 }
 `,
-		expectedBazelTargets: []string{`cc_library_static(
-    name = "target_bionic_empty",
-    system_dynamic_deps = [],
-)`},
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library_static", "target_bionic_empty", attrNameToString{
+				"system_dynamic_deps": `[]`,
+			}),
+		},
 	})
 }
 
@@ -1399,10 +1343,7 @@
 	// only for linux_bionic, but `android` had `["libc", "libdl", "libm"].
 	// b/195791252 tracks the fix.
 	runCcLibraryStaticTestCase(t, bp2buildTestCase{
-		description:                        "cc_library_static system_shared_lib empty for linux_bionic variant",
-		moduleTypeUnderTest:                "cc_library_static",
-		moduleTypeUnderTestFactory:         cc.LibraryStaticFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.CcLibraryStaticBp2Build,
+		description: "cc_library_static system_shared_lib empty for linux_bionic variant",
 		blueprint: soongCcLibraryStaticPreamble + `
 cc_library_static {
     name: "target_linux_bionic_empty",
@@ -1414,19 +1355,17 @@
     include_build_directory: false,
 }
 `,
-		expectedBazelTargets: []string{`cc_library_static(
-    name = "target_linux_bionic_empty",
-    system_dynamic_deps = [],
-)`},
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library_static", "target_linux_bionic_empty", attrNameToString{
+				"system_dynamic_deps": `[]`,
+			}),
+		},
 	})
 }
 
 func TestStaticLibrary_SystemSharedLibsBionic(t *testing.T) {
 	runCcLibraryStaticTestCase(t, bp2buildTestCase{
-		description:                        "cc_library_static system_shared_libs set for bionic variant",
-		moduleTypeUnderTest:                "cc_library_static",
-		moduleTypeUnderTestFactory:         cc.LibraryStaticFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.CcLibraryStaticBp2Build,
+		description: "cc_library_static system_shared_libs set for bionic variant",
 		blueprint: soongCcLibraryStaticPreamble + `
 cc_library{name: "libc"}
 
@@ -1440,23 +1379,21 @@
     include_build_directory: false,
 }
 `,
-		expectedBazelTargets: []string{`cc_library_static(
-    name = "target_bionic",
-    system_dynamic_deps = select({
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library_static", "target_bionic", attrNameToString{
+				"system_dynamic_deps": `select({
         "//build/bazel/platforms/os:android": [":libc"],
         "//build/bazel/platforms/os:linux_bionic": [":libc"],
         "//conditions:default": [],
-    }),
-)`},
+    })`,
+			}),
+		},
 	})
 }
 
 func TestStaticLibrary_SystemSharedLibsLinuxRootAndLinuxBionic(t *testing.T) {
 	runCcLibraryStaticTestCase(t, bp2buildTestCase{
-		description:                        "cc_library_static system_shared_libs set for root and linux_bionic variant",
-		moduleTypeUnderTest:                "cc_library_static",
-		moduleTypeUnderTestFactory:         cc.LibraryStaticFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.CcLibraryStaticBp2Build,
+		description: "cc_library_static system_shared_libs set for root and linux_bionic variant",
 		blueprint: soongCcLibraryStaticPreamble + `
 cc_library{name: "libc"}
 cc_library{name: "libm"}
@@ -1472,12 +1409,13 @@
     include_build_directory: false,
 }
 `,
-		expectedBazelTargets: []string{`cc_library_static(
-    name = "target_linux_bionic",
-    system_dynamic_deps = [":libc"] + select({
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_library_static", "target_linux_bionic", attrNameToString{
+				"system_dynamic_deps": `[":libc"] + select({
         "//build/bazel/platforms/os:linux_bionic": [":libm"],
         "//conditions:default": [],
-    }),
-)`},
+    })`,
+			}),
+		},
 	})
 }
diff --git a/bp2build/cc_object_conversion_test.go b/bp2build/cc_object_conversion_test.go
index c4b276a..5ab9129 100644
--- a/bp2build/cc_object_conversion_test.go
+++ b/bp2build/cc_object_conversion_test.go
@@ -28,15 +28,15 @@
 
 func runCcObjectTestCase(t *testing.T, tc bp2buildTestCase) {
 	t.Helper()
+	(&tc).moduleTypeUnderTest = "cc_object"
+	(&tc).moduleTypeUnderTestFactory = cc.ObjectFactory
+	(&tc).moduleTypeUnderTestBp2BuildMutator = cc.ObjectBp2Build
 	runBp2BuildTestCase(t, registerCcObjectModuleTypes, tc)
 }
 
 func TestCcObjectSimple(t *testing.T) {
 	runCcObjectTestCase(t, bp2buildTestCase{
-		description:                        "simple cc_object generates cc_object with include header dep",
-		moduleTypeUnderTest:                "cc_object",
-		moduleTypeUnderTestFactory:         cc.ObjectFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.ObjectBp2Build,
+		description: "simple cc_object generates cc_object with include header dep",
 		filesystem: map[string]string{
 			"a/b/foo.h":     "",
 			"a/b/bar.h":     "",
@@ -58,30 +58,27 @@
     exclude_srcs: ["a/b/exclude.c"],
 }
 `,
-		expectedBazelTargets: []string{`cc_object(
-    name = "foo",
-    copts = [
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_object", "foo", attrNameToString{
+				"copts": `[
         "-fno-addrsig",
         "-Wno-gcc-compat",
         "-Wall",
         "-Werror",
-    ],
-    local_includes = [
+    ]`,
+				"local_includes": `[
         "include",
         ".",
-    ],
-    srcs = ["a/b/c.c"],
-    system_dynamic_deps = [],
-)`,
+    ]`,
+				"srcs":                `["a/b/c.c"]`,
+				"system_dynamic_deps": `[]`,
+			}),
 		},
 	})
 }
 
 func TestCcObjectDefaults(t *testing.T) {
 	runCcObjectTestCase(t, bp2buildTestCase{
-		moduleTypeUnderTest:                "cc_object",
-		moduleTypeUnderTestFactory:         cc.ObjectFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.ObjectBp2Build,
 		blueprint: `cc_object {
     name: "foo",
     system_shared_libs: [],
@@ -101,33 +98,26 @@
 cc_defaults {
     name: "foo_bar_defaults",
     cflags: [
-        "-Wno-gcc-compat",
-        "-Wall",
         "-Werror",
     ],
 }
 `,
-		expectedBazelTargets: []string{`cc_object(
-    name = "foo",
-    copts = [
-        "-Wno-gcc-compat",
-        "-Wall",
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_object", "foo", attrNameToString{
+				"copts": `[
         "-Werror",
         "-fno-addrsig",
-    ],
-    local_includes = ["."],
-    srcs = ["a/b/c.c"],
-    system_dynamic_deps = [],
-)`,
+    ]`,
+				"local_includes":      `["."]`,
+				"srcs":                `["a/b/c.c"]`,
+				"system_dynamic_deps": `[]`,
+			}),
 		}})
 }
 
 func TestCcObjectCcObjetDepsInObjs(t *testing.T) {
 	runCcObjectTestCase(t, bp2buildTestCase{
-		description:                        "cc_object with cc_object deps in objs props",
-		moduleTypeUnderTest:                "cc_object",
-		moduleTypeUnderTestFactory:         cc.ObjectFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.ObjectBp2Build,
+		description: "cc_object with cc_object deps in objs props",
 		filesystem: map[string]string{
 			"a/b/c.c": "",
 			"x/y/z.c": "",
@@ -147,28 +137,24 @@
     include_build_directory: false,
 }
 `,
-		expectedBazelTargets: []string{`cc_object(
-    name = "bar",
-    copts = ["-fno-addrsig"],
-    srcs = ["x/y/z.c"],
-    system_dynamic_deps = [],
-)`, `cc_object(
-    name = "foo",
-    copts = ["-fno-addrsig"],
-    deps = [":bar"],
-    srcs = ["a/b/c.c"],
-    system_dynamic_deps = [],
-)`,
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_object", "bar", attrNameToString{
+				"copts":               `["-fno-addrsig"]`,
+				"srcs":                `["x/y/z.c"]`,
+				"system_dynamic_deps": `[]`,
+			}), makeBazelTarget("cc_object", "foo", attrNameToString{
+				"copts":               `["-fno-addrsig"]`,
+				"deps":                `[":bar"]`,
+				"srcs":                `["a/b/c.c"]`,
+				"system_dynamic_deps": `[]`,
+			}),
 		},
 	})
 }
 
 func TestCcObjectIncludeBuildDirFalse(t *testing.T) {
 	runCcObjectTestCase(t, bp2buildTestCase{
-		description:                        "cc_object with include_build_dir: false",
-		moduleTypeUnderTest:                "cc_object",
-		moduleTypeUnderTestFactory:         cc.ObjectFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.ObjectBp2Build,
+		description: "cc_object with include_build_dir: false",
 		filesystem: map[string]string{
 			"a/b/c.c": "",
 			"x/y/z.c": "",
@@ -180,22 +166,19 @@
     include_build_directory: false,
 }
 `,
-		expectedBazelTargets: []string{`cc_object(
-    name = "foo",
-    copts = ["-fno-addrsig"],
-    srcs = ["a/b/c.c"],
-    system_dynamic_deps = [],
-)`,
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_object", "foo", attrNameToString{
+				"copts":               `["-fno-addrsig"]`,
+				"srcs":                `["a/b/c.c"]`,
+				"system_dynamic_deps": `[]`,
+			}),
 		},
 	})
 }
 
 func TestCcObjectProductVariable(t *testing.T) {
 	runCcObjectTestCase(t, bp2buildTestCase{
-		description:                        "cc_object with product variable",
-		moduleTypeUnderTest:                "cc_object",
-		moduleTypeUnderTestFactory:         cc.ObjectFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.ObjectBp2Build,
+		description: "cc_object with product variable",
 		blueprint: `cc_object {
     name: "foo",
     system_shared_libs: [],
@@ -208,26 +191,23 @@
     srcs: ["src.S"],
 }
 `,
-		expectedBazelTargets: []string{`cc_object(
-    name = "foo",
-    asflags = select({
+		expectedBazelTargets: []string{
+			makeBazelTarget("cc_object", "foo", attrNameToString{
+				"asflags": `select({
         "//build/bazel/product_variables:platform_sdk_version": ["-DPLATFORM_SDK_VERSION=$(Platform_sdk_version)"],
         "//conditions:default": [],
-    }),
-    copts = ["-fno-addrsig"],
-    srcs_as = ["src.S"],
-    system_dynamic_deps = [],
-)`,
+    })`,
+				"copts":               `["-fno-addrsig"]`,
+				"srcs_as":             `["src.S"]`,
+				"system_dynamic_deps": `[]`,
+			}),
 		},
 	})
 }
 
 func TestCcObjectCflagsOneArch(t *testing.T) {
 	runCcObjectTestCase(t, bp2buildTestCase{
-		description:                        "cc_object setting cflags for one arch",
-		moduleTypeUnderTest:                "cc_object",
-		moduleTypeUnderTestFactory:         cc.ObjectFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.ObjectBp2Build,
+		description: "cc_object setting cflags for one arch",
 		blueprint: `cc_object {
     name: "foo",
     system_shared_libs: [],
@@ -244,28 +224,24 @@
 }
 `,
 		expectedBazelTargets: []string{
-			`cc_object(
-    name = "foo",
-    copts = ["-fno-addrsig"] + select({
+			makeBazelTarget("cc_object", "foo", attrNameToString{
+				"copts": `["-fno-addrsig"] + select({
         "//build/bazel/platforms/arch:x86": ["-fPIC"],
         "//conditions:default": [],
-    }),
-    srcs = ["a.cpp"] + select({
+    })`,
+				"srcs": `["a.cpp"] + select({
         "//build/bazel/platforms/arch:arm": ["arch/arm/file.cpp"],
         "//conditions:default": [],
-    }),
-    system_dynamic_deps = [],
-)`,
+    })`,
+				"system_dynamic_deps": `[]`,
+			}),
 		},
 	})
 }
 
 func TestCcObjectCflagsFourArch(t *testing.T) {
 	runCcObjectTestCase(t, bp2buildTestCase{
-		description:                        "cc_object setting cflags for 4 architectures",
-		moduleTypeUnderTest:                "cc_object",
-		moduleTypeUnderTestFactory:         cc.ObjectFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.ObjectBp2Build,
+		description: "cc_object setting cflags for 4 architectures",
 		blueprint: `cc_object {
     name: "foo",
     system_shared_libs: [],
@@ -292,34 +268,30 @@
 }
 `,
 		expectedBazelTargets: []string{
-			`cc_object(
-    name = "foo",
-    copts = ["-fno-addrsig"] + select({
+			makeBazelTarget("cc_object", "foo", attrNameToString{
+				"copts": `["-fno-addrsig"] + select({
         "//build/bazel/platforms/arch:arm": ["-Wall"],
         "//build/bazel/platforms/arch:arm64": ["-Wall"],
         "//build/bazel/platforms/arch:x86": ["-fPIC"],
         "//build/bazel/platforms/arch:x86_64": ["-fPIC"],
         "//conditions:default": [],
-    }),
-    srcs = ["base.cpp"] + select({
+    })`,
+				"srcs": `["base.cpp"] + select({
         "//build/bazel/platforms/arch:arm": ["arm.cpp"],
         "//build/bazel/platforms/arch:arm64": ["arm64.cpp"],
         "//build/bazel/platforms/arch:x86": ["x86.cpp"],
         "//build/bazel/platforms/arch:x86_64": ["x86_64.cpp"],
         "//conditions:default": [],
-    }),
-    system_dynamic_deps = [],
-)`,
+    })`,
+				"system_dynamic_deps": `[]`,
+			}),
 		},
 	})
 }
 
 func TestCcObjectLinkerScript(t *testing.T) {
 	runCcObjectTestCase(t, bp2buildTestCase{
-		description:                        "cc_object setting linker_script",
-		moduleTypeUnderTest:                "cc_object",
-		moduleTypeUnderTestFactory:         cc.ObjectFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.ObjectBp2Build,
+		description: "cc_object setting linker_script",
 		blueprint: `cc_object {
     name: "foo",
     srcs: ["base.cpp"],
@@ -328,22 +300,18 @@
 }
 `,
 		expectedBazelTargets: []string{
-			`cc_object(
-    name = "foo",
-    copts = ["-fno-addrsig"],
-    linker_script = "bunny.lds",
-    srcs = ["base.cpp"],
-)`,
+			makeBazelTarget("cc_object", "foo", attrNameToString{
+				"copts":         `["-fno-addrsig"]`,
+				"linker_script": `"bunny.lds"`,
+				"srcs":          `["base.cpp"]`,
+			}),
 		},
 	})
 }
 
 func TestCcObjectDepsAndLinkerScriptSelects(t *testing.T) {
 	runCcObjectTestCase(t, bp2buildTestCase{
-		description:                        "cc_object setting deps and linker_script across archs",
-		moduleTypeUnderTest:                "cc_object",
-		moduleTypeUnderTestFactory:         cc.ObjectFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.ObjectBp2Build,
+		description: "cc_object setting deps and linker_script across archs",
 		blueprint: `cc_object {
     name: "foo",
     srcs: ["base.cpp"],
@@ -389,33 +357,29 @@
 }
 `,
 		expectedBazelTargets: []string{
-			`cc_object(
-    name = "foo",
-    copts = ["-fno-addrsig"],
-    deps = select({
+			makeBazelTarget("cc_object", "foo", attrNameToString{
+				"copts": `["-fno-addrsig"]`,
+				"deps": `select({
         "//build/bazel/platforms/arch:arm": [":arm_obj"],
         "//build/bazel/platforms/arch:x86": [":x86_obj"],
         "//build/bazel/platforms/arch:x86_64": [":x86_64_obj"],
         "//conditions:default": [],
-    }),
-    linker_script = select({
+    })`,
+				"linker_script": `select({
         "//build/bazel/platforms/arch:arm": "arm.lds",
         "//build/bazel/platforms/arch:x86": "x86.lds",
         "//build/bazel/platforms/arch:x86_64": "x86_64.lds",
         "//conditions:default": None,
-    }),
-    srcs = ["base.cpp"],
-)`,
+    })`,
+				"srcs": `["base.cpp"]`,
+			}),
 		},
 	})
 }
 
 func TestCcObjectSelectOnLinuxAndBionicArchs(t *testing.T) {
 	runCcObjectTestCase(t, bp2buildTestCase{
-		description:                        "cc_object setting srcs based on linux and bionic archs",
-		moduleTypeUnderTest:                "cc_object",
-		moduleTypeUnderTestFactory:         cc.ObjectFactory,
-		moduleTypeUnderTestBp2BuildMutator: cc.ObjectBp2Build,
+		description: "cc_object setting srcs based on linux and bionic archs",
 		blueprint: `cc_object {
     name: "foo",
     srcs: ["base.cpp"],
@@ -434,10 +398,9 @@
 }
 `,
 		expectedBazelTargets: []string{
-			`cc_object(
-    name = "foo",
-    copts = ["-fno-addrsig"],
-    srcs = ["base.cpp"] + select({
+			makeBazelTarget("cc_object", "foo", attrNameToString{
+				"copts": `["-fno-addrsig"]`,
+				"srcs": `["base.cpp"] + select({
         "//build/bazel/platforms/os_arch:android_arm64": [
             "linux_arm64.cpp",
             "bionic_arm64.cpp",
@@ -450,8 +413,8 @@
         "//build/bazel/platforms/os_arch:linux_glibc_x86": ["linux_x86.cpp"],
         "//build/bazel/platforms/os_arch:linux_musl_x86": ["linux_x86.cpp"],
         "//conditions:default": [],
-    }),
-)`,
+    })`,
+			}),
 		},
 	})
 }
diff --git a/bp2build/cc_prebuilt_library_shared_test.go b/bp2build/cc_prebuilt_library_shared_test.go
index 97545c8..bac3908 100644
--- a/bp2build/cc_prebuilt_library_shared_test.go
+++ b/bp2build/cc_prebuilt_library_shared_test.go
@@ -23,10 +23,9 @@
 	bazel_module: { bp2build_available: true },
 }`,
 			expectedBazelTargets: []string{
-				`prebuilt_library_shared(
-    name = "libtest",
-    shared_library = "libf.so",
-)`,
+				makeBazelTarget("prebuilt_library_shared", "libtest", attrNameToString{
+					"shared_library": `"libf.so"`,
+				}),
 			},
 		})
 }
@@ -52,14 +51,13 @@
 	bazel_module: { bp2build_available: true },
 }`,
 			expectedBazelTargets: []string{
-				`prebuilt_library_shared(
-    name = "libtest",
-    shared_library = select({
+				makeBazelTarget("prebuilt_library_shared", "libtest", attrNameToString{
+					"shared_library": `select({
         "//build/bazel/platforms/arch:arm": "libg.so",
         "//build/bazel/platforms/arch:arm64": "libf.so",
         "//conditions:default": None,
-    }),
-)`,
+    })`,
+				}),
 			},
 		})
 }
diff --git a/bp2build/filegroup_conversion_test.go b/bp2build/filegroup_conversion_test.go
index ad99236..9f4add2 100644
--- a/bp2build/filegroup_conversion_test.go
+++ b/bp2build/filegroup_conversion_test.go
@@ -23,6 +23,9 @@
 
 func runFilegroupTestCase(t *testing.T, tc bp2buildTestCase) {
 	t.Helper()
+	(&tc).moduleTypeUnderTest = "filegroup"
+	(&tc).moduleTypeUnderTestFactory = android.FileGroupFactory
+	(&tc).moduleTypeUnderTestBp2BuildMutator = android.FilegroupBp2Build
 	runBp2BuildTestCase(t, registerFilegroupModuleTypes, tc)
 }
 
@@ -30,11 +33,8 @@
 
 func TestFilegroupSameNameAsFile_OneFile(t *testing.T) {
 	runFilegroupTestCase(t, bp2buildTestCase{
-		description:                        "filegroup - same name as file, with one file",
-		moduleTypeUnderTest:                "filegroup",
-		moduleTypeUnderTestFactory:         android.FileGroupFactory,
-		moduleTypeUnderTestBp2BuildMutator: android.FilegroupBp2Build,
-		filesystem:                         map[string]string{},
+		description: "filegroup - same name as file, with one file",
+		filesystem:  map[string]string{},
 		blueprint: `
 filegroup {
     name: "foo",
@@ -46,11 +46,8 @@
 
 func TestFilegroupSameNameAsFile_MultipleFiles(t *testing.T) {
 	runFilegroupTestCase(t, bp2buildTestCase{
-		description:                        "filegroup - same name as file, with multiple files",
-		moduleTypeUnderTest:                "filegroup",
-		moduleTypeUnderTestFactory:         android.FileGroupFactory,
-		moduleTypeUnderTestBp2BuildMutator: android.FilegroupBp2Build,
-		filesystem:                         map[string]string{},
+		description: "filegroup - same name as file, with multiple files",
+		filesystem:  map[string]string{},
 		blueprint: `
 filegroup {
 	name: "foo",
diff --git a/bp2build/genrule_conversion_test.go b/bp2build/genrule_conversion_test.go
index f3bc1ba..5976666 100644
--- a/bp2build/genrule_conversion_test.go
+++ b/bp2build/genrule_conversion_test.go
@@ -17,10 +17,21 @@
 import (
 	"android/soong/android"
 	"android/soong/genrule"
-	"strings"
 	"testing"
 )
 
+func registerGenruleModuleTypes(ctx android.RegistrationContext) {
+	ctx.RegisterModuleType("genrule_defaults", func() android.Module { return genrule.DefaultsFactory() })
+}
+
+func runGenruleTestCase(t *testing.T, tc bp2buildTestCase) {
+	t.Helper()
+	(&tc).moduleTypeUnderTest = "genrule"
+	(&tc).moduleTypeUnderTestFactory = genrule.GenRuleFactory
+	(&tc).moduleTypeUnderTestBp2BuildMutator = genrule.GenruleBp2Build
+	runBp2BuildTestCase(t, registerGenruleModuleTypes, tc)
+}
+
 func TestGenruleBp2Build(t *testing.T) {
 	otherGenruleBp := map[string]string{
 		"other/Android.bp": `genrule {
@@ -39,10 +50,7 @@
 
 	testCases := []bp2buildTestCase{
 		{
-			description:                        "genrule with command line variable replacements",
-			moduleTypeUnderTest:                "genrule",
-			moduleTypeUnderTestFactory:         genrule.GenRuleFactory,
-			moduleTypeUnderTestBp2BuildMutator: genrule.GenruleBp2Build,
+			description: "genrule with command line variable replacements",
 			blueprint: `genrule {
     name: "foo.tool",
     out: ["foo_tool.out"],
@@ -60,26 +68,21 @@
     bazel_module: { bp2build_available: true },
 }`,
 			expectedBazelTargets: []string{
-				`genrule(
-    name = "foo",
-    cmd = "$(location :foo.tool) --genDir=$(GENDIR) arg $(SRCS) $(OUTS)",
-    outs = ["foo.out"],
-    srcs = ["foo.in"],
-    tools = [":foo.tool"],
-)`,
-				`genrule(
-    name = "foo.tool",
-    cmd = "cp $(SRCS) $(OUTS)",
-    outs = ["foo_tool.out"],
-    srcs = ["foo_tool.in"],
-)`,
+				makeBazelTarget("genrule", "foo", attrNameToString{
+					"cmd":   `"$(location :foo.tool) --genDir=$(GENDIR) arg $(SRCS) $(OUTS)"`,
+					"outs":  `["foo.out"]`,
+					"srcs":  `["foo.in"]`,
+					"tools": `[":foo.tool"]`,
+				}),
+				makeBazelTarget("genrule", "foo.tool", attrNameToString{
+					"cmd":  `"cp $(SRCS) $(OUTS)"`,
+					"outs": `["foo_tool.out"]`,
+					"srcs": `["foo_tool.in"]`,
+				}),
 			},
 		},
 		{
-			description:                        "genrule using $(locations :label)",
-			moduleTypeUnderTest:                "genrule",
-			moduleTypeUnderTestFactory:         genrule.GenRuleFactory,
-			moduleTypeUnderTestBp2BuildMutator: genrule.GenruleBp2Build,
+			description: "genrule using $(locations :label)",
 			blueprint: `genrule {
     name: "foo.tools",
     out: ["foo_tool.out", "foo_tool2.out"],
@@ -96,29 +99,25 @@
     cmd: "$(locations :foo.tools) -s $(out) $(in)",
     bazel_module: { bp2build_available: true },
 }`,
-			expectedBazelTargets: []string{`genrule(
-    name = "foo",
-    cmd = "$(locations :foo.tools) -s $(OUTS) $(SRCS)",
-    outs = ["foo.out"],
-    srcs = ["foo.in"],
-    tools = [":foo.tools"],
-)`,
-				`genrule(
-    name = "foo.tools",
-    cmd = "cp $(SRCS) $(OUTS)",
-    outs = [
+			expectedBazelTargets: []string{
+				makeBazelTarget("genrule", "foo", attrNameToString{
+					"cmd":   `"$(locations :foo.tools) -s $(OUTS) $(SRCS)"`,
+					"outs":  `["foo.out"]`,
+					"srcs":  `["foo.in"]`,
+					"tools": `[":foo.tools"]`,
+				}),
+				makeBazelTarget("genrule", "foo.tools", attrNameToString{
+					"cmd": `"cp $(SRCS) $(OUTS)"`,
+					"outs": `[
         "foo_tool.out",
         "foo_tool2.out",
-    ],
-    srcs = ["foo_tool.in"],
-)`,
+    ]`,
+					"srcs": `["foo_tool.in"]`,
+				}),
 			},
 		},
 		{
-			description:                        "genrule using $(locations //absolute:label)",
-			moduleTypeUnderTest:                "genrule",
-			moduleTypeUnderTestFactory:         genrule.GenRuleFactory,
-			moduleTypeUnderTestBp2BuildMutator: genrule.GenruleBp2Build,
+			description: "genrule using $(locations //absolute:label)",
 			blueprint: `genrule {
     name: "foo",
     out: ["foo.out"],
@@ -127,21 +126,18 @@
     cmd: "$(locations :foo.tool) -s $(out) $(in)",
     bazel_module: { bp2build_available: true },
 }`,
-			expectedBazelTargets: []string{`genrule(
-    name = "foo",
-    cmd = "$(locations //other:foo.tool) -s $(OUTS) $(SRCS)",
-    outs = ["foo.out"],
-    srcs = ["foo.in"],
-    tools = ["//other:foo.tool"],
-)`,
+			expectedBazelTargets: []string{
+				makeBazelTarget("genrule", "foo", attrNameToString{
+					"cmd":   `"$(locations //other:foo.tool) -s $(OUTS) $(SRCS)"`,
+					"outs":  `["foo.out"]`,
+					"srcs":  `["foo.in"]`,
+					"tools": `["//other:foo.tool"]`,
+				}),
 			},
 			filesystem: otherGenruleBp,
 		},
 		{
-			description:                        "genrule srcs using $(locations //absolute:label)",
-			moduleTypeUnderTest:                "genrule",
-			moduleTypeUnderTestFactory:         genrule.GenRuleFactory,
-			moduleTypeUnderTestBp2BuildMutator: genrule.GenruleBp2Build,
+			description: "genrule srcs using $(locations //absolute:label)",
 			blueprint: `genrule {
     name: "foo",
     out: ["foo.out"],
@@ -150,21 +146,18 @@
     cmd: "$(locations :foo.tool) -s $(out) $(location :other.tool)",
     bazel_module: { bp2build_available: true },
 }`,
-			expectedBazelTargets: []string{`genrule(
-    name = "foo",
-    cmd = "$(locations //other:foo.tool) -s $(OUTS) $(location //other:other.tool)",
-    outs = ["foo.out"],
-    srcs = ["//other:other.tool"],
-    tools = ["//other:foo.tool"],
-)`,
+			expectedBazelTargets: []string{
+				makeBazelTarget("genrule", "foo", attrNameToString{
+					"cmd":   `"$(locations //other:foo.tool) -s $(OUTS) $(location //other:other.tool)"`,
+					"outs":  `["foo.out"]`,
+					"srcs":  `["//other:other.tool"]`,
+					"tools": `["//other:foo.tool"]`,
+				}),
 			},
 			filesystem: otherGenruleBp,
 		},
 		{
-			description:                        "genrule using $(location) label should substitute first tool label automatically",
-			moduleTypeUnderTest:                "genrule",
-			moduleTypeUnderTestFactory:         genrule.GenRuleFactory,
-			moduleTypeUnderTestBp2BuildMutator: genrule.GenruleBp2Build,
+			description: "genrule using $(location) label should substitute first tool label automatically",
 			blueprint: `genrule {
     name: "foo",
     out: ["foo.out"],
@@ -173,24 +166,21 @@
     cmd: "$(location) -s $(out) $(in)",
     bazel_module: { bp2build_available: true },
 }`,
-			expectedBazelTargets: []string{`genrule(
-    name = "foo",
-    cmd = "$(location //other:foo.tool) -s $(OUTS) $(SRCS)",
-    outs = ["foo.out"],
-    srcs = ["foo.in"],
-    tools = [
+			expectedBazelTargets: []string{
+				makeBazelTarget("genrule", "foo", attrNameToString{
+					"cmd":  `"$(location //other:foo.tool) -s $(OUTS) $(SRCS)"`,
+					"outs": `["foo.out"]`,
+					"srcs": `["foo.in"]`,
+					"tools": `[
         "//other:foo.tool",
         "//other:other.tool",
-    ],
-)`,
+    ]`,
+				}),
 			},
 			filesystem: otherGenruleBp,
 		},
 		{
-			description:                        "genrule using $(locations) label should substitute first tool label automatically",
-			moduleTypeUnderTest:                "genrule",
-			moduleTypeUnderTestFactory:         genrule.GenRuleFactory,
-			moduleTypeUnderTestBp2BuildMutator: genrule.GenruleBp2Build,
+			description: "genrule using $(locations) label should substitute first tool label automatically",
 			blueprint: `genrule {
     name: "foo",
     out: ["foo.out"],
@@ -199,24 +189,21 @@
     cmd: "$(locations) -s $(out) $(in)",
     bazel_module: { bp2build_available: true },
 }`,
-			expectedBazelTargets: []string{`genrule(
-    name = "foo",
-    cmd = "$(locations //other:foo.tool) -s $(OUTS) $(SRCS)",
-    outs = ["foo.out"],
-    srcs = ["foo.in"],
-    tools = [
+			expectedBazelTargets: []string{
+				makeBazelTarget("genrule", "foo", attrNameToString{
+					"cmd":  `"$(locations //other:foo.tool) -s $(OUTS) $(SRCS)"`,
+					"outs": `["foo.out"]`,
+					"srcs": `["foo.in"]`,
+					"tools": `[
         "//other:foo.tool",
         "//other:other.tool",
-    ],
-)`,
+    ]`,
+				}),
 			},
 			filesystem: otherGenruleBp,
 		},
 		{
-			description:                        "genrule without tools or tool_files can convert successfully",
-			moduleTypeUnderTest:                "genrule",
-			moduleTypeUnderTestFactory:         genrule.GenRuleFactory,
-			moduleTypeUnderTestBp2BuildMutator: genrule.GenruleBp2Build,
+			description: "genrule without tools or tool_files can convert successfully",
 			blueprint: `genrule {
     name: "foo",
     out: ["foo.out"],
@@ -224,85 +211,28 @@
     cmd: "cp $(in) $(out)",
     bazel_module: { bp2build_available: true },
 }`,
-			expectedBazelTargets: []string{`genrule(
-    name = "foo",
-    cmd = "cp $(SRCS) $(OUTS)",
-    outs = ["foo.out"],
-    srcs = ["foo.in"],
-)`,
+			expectedBazelTargets: []string{
+				makeBazelTarget("genrule", "foo", attrNameToString{
+					"cmd":  `"cp $(SRCS) $(OUTS)"`,
+					"outs": `["foo.out"]`,
+					"srcs": `["foo.in"]`,
+				}),
 			},
 		},
 	}
 
-	dir := "."
 	for _, testCase := range testCases {
-		fs := make(map[string][]byte)
-		toParse := []string{
-			"Android.bp",
-		}
-		for f, content := range testCase.filesystem {
-			if strings.HasSuffix(f, "Android.bp") {
-				toParse = append(toParse, f)
-			}
-			fs[f] = []byte(content)
-		}
-		config := android.TestConfig(buildDir, nil, testCase.blueprint, fs)
-		ctx := android.NewTestContext(config)
-		ctx.RegisterModuleType(testCase.moduleTypeUnderTest, testCase.moduleTypeUnderTestFactory)
-		ctx.RegisterBp2BuildMutator(testCase.moduleTypeUnderTest, testCase.moduleTypeUnderTestBp2BuildMutator)
-		ctx.RegisterForBazelConversion()
-
-		_, errs := ctx.ParseFileList(dir, toParse)
-		if errored(t, testCase, errs) {
-			continue
-		}
-		_, errs = ctx.ResolveDependencies(config)
-		if errored(t, testCase, errs) {
-			continue
-		}
-
-		checkDir := dir
-		if testCase.dir != "" {
-			checkDir = testCase.dir
-		}
-
-		codegenCtx := NewCodegenContext(config, *ctx.Context, Bp2Build)
-		bazelTargets, err := generateBazelTargetsForDir(codegenCtx, checkDir)
-		android.FailIfErrored(t, err)
-		if actualCount, expectedCount := len(bazelTargets), len(testCase.expectedBazelTargets); actualCount != expectedCount {
-			t.Errorf("%s: Expected %d bazel target, got %d", testCase.description, expectedCount, actualCount)
-		} else {
-			for i, target := range bazelTargets {
-				if w, g := testCase.expectedBazelTargets[i], target.content; w != g {
-					t.Errorf(
-						"%s: Expected generated Bazel target to be '%s', got '%s'",
-						testCase.description,
-						w,
-						g,
-					)
-				}
-			}
-		}
+		t.Run(testCase.description, func(t *testing.T) {
+			runGenruleTestCase(t, testCase)
+		})
 	}
 }
 
 func TestBp2BuildInlinesDefaults(t *testing.T) {
-	testCases := []struct {
-		moduleTypesUnderTest      map[string]android.ModuleFactory
-		bp2buildMutatorsUnderTest map[string]bp2buildMutator
-		bp                        string
-		expectedBazelTarget       string
-		description               string
-	}{
+	testCases := []bp2buildTestCase{
 		{
-			moduleTypesUnderTest: map[string]android.ModuleFactory{
-				"genrule":          genrule.GenRuleFactory,
-				"genrule_defaults": func() android.Module { return genrule.DefaultsFactory() },
-			},
-			bp2buildMutatorsUnderTest: map[string]bp2buildMutator{
-				"genrule": genrule.GenruleBp2Build,
-			},
-			bp: `genrule_defaults {
+			description: "genrule applies properties from a genrule_defaults dependency if not specified",
+			blueprint: `genrule_defaults {
     name: "gen_defaults",
     cmd: "do-something $(in) $(out)",
 }
@@ -314,23 +244,17 @@
     bazel_module: { bp2build_available: true },
 }
 `,
-			expectedBazelTarget: `genrule(
-    name = "gen",
-    cmd = "do-something $(SRCS) $(OUTS)",
-    outs = ["out"],
-    srcs = ["in1"],
-)`,
-			description: "genrule applies properties from a genrule_defaults dependency if not specified",
+			expectedBazelTargets: []string{
+				makeBazelTarget("genrule", "gen", attrNameToString{
+					"cmd":  `"do-something $(SRCS) $(OUTS)"`,
+					"outs": `["out"]`,
+					"srcs": `["in1"]`,
+				}),
+			},
 		},
 		{
-			moduleTypesUnderTest: map[string]android.ModuleFactory{
-				"genrule":          genrule.GenRuleFactory,
-				"genrule_defaults": func() android.Module { return genrule.DefaultsFactory() },
-			},
-			bp2buildMutatorsUnderTest: map[string]bp2buildMutator{
-				"genrule": genrule.GenruleBp2Build,
-			},
-			bp: `genrule_defaults {
+			description: "genrule does merges properties from a genrule_defaults dependency, latest-first",
+			blueprint: `genrule_defaults {
     name: "gen_defaults",
     out: ["out-from-defaults"],
     srcs: ["in-from-defaults"],
@@ -345,29 +269,23 @@
     bazel_module: { bp2build_available: true },
 }
 `,
-			expectedBazelTarget: `genrule(
-    name = "gen",
-    cmd = "do-something $(SRCS) $(OUTS)",
-    outs = [
+			expectedBazelTargets: []string{
+				makeBazelTarget("genrule", "gen", attrNameToString{
+					"cmd": `"do-something $(SRCS) $(OUTS)"`,
+					"outs": `[
         "out-from-defaults",
         "out",
-    ],
-    srcs = [
+    ]`,
+					"srcs": `[
         "in-from-defaults",
         "in1",
-    ],
-)`,
-			description: "genrule does merges properties from a genrule_defaults dependency, latest-first",
+    ]`,
+				}),
+			},
 		},
 		{
-			moduleTypesUnderTest: map[string]android.ModuleFactory{
-				"genrule":          genrule.GenRuleFactory,
-				"genrule_defaults": func() android.Module { return genrule.DefaultsFactory() },
-			},
-			bp2buildMutatorsUnderTest: map[string]bp2buildMutator{
-				"genrule": genrule.GenruleBp2Build,
-			},
-			bp: `genrule_defaults {
+			description: "genrule applies properties from list of genrule_defaults",
+			blueprint: `genrule_defaults {
     name: "gen_defaults1",
     cmd: "cp $(in) $(out)",
 }
@@ -384,23 +302,17 @@
     bazel_module: { bp2build_available: true },
 }
 `,
-			expectedBazelTarget: `genrule(
-    name = "gen",
-    cmd = "cp $(SRCS) $(OUTS)",
-    outs = ["out"],
-    srcs = ["in1"],
-)`,
-			description: "genrule applies properties from list of genrule_defaults",
+			expectedBazelTargets: []string{
+				makeBazelTarget("genrule", "gen", attrNameToString{
+					"cmd":  `"cp $(SRCS) $(OUTS)"`,
+					"outs": `["out"]`,
+					"srcs": `["in1"]`,
+				}),
+			},
 		},
 		{
-			moduleTypesUnderTest: map[string]android.ModuleFactory{
-				"genrule":          genrule.GenRuleFactory,
-				"genrule_defaults": func() android.Module { return genrule.DefaultsFactory() },
-			},
-			bp2buildMutatorsUnderTest: map[string]bp2buildMutator{
-				"genrule": genrule.GenruleBp2Build,
-			},
-			bp: `genrule_defaults {
+			description: "genrule applies properties from genrule_defaults transitively",
+			blueprint: `genrule_defaults {
     name: "gen_defaults1",
     defaults: ["gen_defaults2"],
     cmd: "cmd1 $(in) $(out)", // overrides gen_defaults2's cmd property value.
@@ -427,55 +339,26 @@
     bazel_module: { bp2build_available: true },
 }
 `,
-			expectedBazelTarget: `genrule(
-    name = "gen",
-    cmd = "cmd1 $(SRCS) $(OUTS)",
-    outs = [
+			expectedBazelTargets: []string{
+				makeBazelTarget("genrule", "gen", attrNameToString{
+					"cmd": `"cmd1 $(SRCS) $(OUTS)"`,
+					"outs": `[
         "out-from-3",
         "out-from-2",
         "out",
-    ],
-    srcs = [
+    ]`,
+					"srcs": `[
         "srcs-from-3",
         "in1",
-    ],
-)`,
-			description: "genrule applies properties from genrule_defaults transitively",
+    ]`,
+				}),
+			},
 		},
 	}
 
-	dir := "."
 	for _, testCase := range testCases {
-		config := android.TestConfig(buildDir, nil, testCase.bp, nil)
-		ctx := android.NewTestContext(config)
-		for m, factory := range testCase.moduleTypesUnderTest {
-			ctx.RegisterModuleType(m, factory)
-		}
-		for mutator, f := range testCase.bp2buildMutatorsUnderTest {
-			ctx.RegisterBp2BuildMutator(mutator, f)
-		}
-		ctx.RegisterForBazelConversion()
-
-		_, errs := ctx.ParseFileList(dir, []string{"Android.bp"})
-		android.FailIfErrored(t, errs)
-		_, errs = ctx.ResolveDependencies(config)
-		android.FailIfErrored(t, errs)
-
-		codegenCtx := NewCodegenContext(config, *ctx.Context, Bp2Build)
-		bazelTargets, err := generateBazelTargetsForDir(codegenCtx, dir)
-		android.FailIfErrored(t, err)
-		if actualCount := len(bazelTargets); actualCount != 1 {
-			t.Fatalf("%s: Expected 1 bazel target, got %d", testCase.description, actualCount)
-		}
-
-		actualBazelTarget := bazelTargets[0]
-		if actualBazelTarget.content != testCase.expectedBazelTarget {
-			t.Errorf(
-				"%s: Expected generated Bazel target to be '%s', got '%s'",
-				testCase.description,
-				testCase.expectedBazelTarget,
-				actualBazelTarget.content,
-			)
-		}
+		t.Run(testCase.description, func(t *testing.T) {
+			runGenruleTestCase(t, testCase)
+		})
 	}
 }
diff --git a/bp2build/prebuilt_etc_conversion_test.go b/bp2build/prebuilt_etc_conversion_test.go
index 62e407b..1189309 100644
--- a/bp2build/prebuilt_etc_conversion_test.go
+++ b/bp2build/prebuilt_etc_conversion_test.go
@@ -23,6 +23,9 @@
 
 func runPrebuiltEtcTestCase(t *testing.T, tc bp2buildTestCase) {
 	t.Helper()
+	(&tc).moduleTypeUnderTest = "prebuilt_etc"
+	(&tc).moduleTypeUnderTestFactory = etc.PrebuiltEtcFactory
+	(&tc).moduleTypeUnderTestBp2BuildMutator = etc.PrebuiltEtcBp2Build
 	runBp2BuildTestCase(t, registerPrebuiltEtcModuleTypes, tc)
 }
 
@@ -31,11 +34,8 @@
 
 func TestPrebuiltEtcSimple(t *testing.T) {
 	runPrebuiltEtcTestCase(t, bp2buildTestCase{
-		description:                        "prebuilt_etc - simple example",
-		moduleTypeUnderTest:                "prebuilt_etc",
-		moduleTypeUnderTestFactory:         etc.PrebuiltEtcFactory,
-		moduleTypeUnderTestBp2BuildMutator: etc.PrebuiltEtcBp2Build,
-		filesystem:                         map[string]string{},
+		description: "prebuilt_etc - simple example",
+		filesystem:  map[string]string{},
 		blueprint: `
 prebuilt_etc {
     name: "apex_tz_version",
@@ -45,22 +45,19 @@
     installable: false,
 }
 `,
-		expectedBazelTargets: []string{`prebuilt_etc(
-    name = "apex_tz_version",
-    filename = "tz_version",
-    installable = False,
-    src = "version/tz_version",
-    sub_dir = "tz",
-)`}})
+		expectedBazelTargets: []string{
+			makeBazelTarget("prebuilt_etc", "apex_tz_version", attrNameToString{
+				"filename":    `"tz_version"`,
+				"installable": `False`,
+				"src":         `"version/tz_version"`,
+				"sub_dir":     `"tz"`,
+			})}})
 }
 
 func TestPrebuiltEtcArchVariant(t *testing.T) {
 	runPrebuiltEtcTestCase(t, bp2buildTestCase{
-		description:                        "prebuilt_etc - simple example",
-		moduleTypeUnderTest:                "prebuilt_etc",
-		moduleTypeUnderTestFactory:         etc.PrebuiltEtcFactory,
-		moduleTypeUnderTestBp2BuildMutator: etc.PrebuiltEtcBp2Build,
-		filesystem:                         map[string]string{},
+		description: "prebuilt_etc - arch variant",
+		filesystem:  map[string]string{},
 		blueprint: `
 prebuilt_etc {
     name: "apex_tz_version",
@@ -78,15 +75,15 @@
     }
 }
 `,
-		expectedBazelTargets: []string{`prebuilt_etc(
-    name = "apex_tz_version",
-    filename = "tz_version",
-    installable = False,
-    src = select({
+		expectedBazelTargets: []string{
+			makeBazelTarget("prebuilt_etc", "apex_tz_version", attrNameToString{
+				"filename":    `"tz_version"`,
+				"installable": `False`,
+				"src": `select({
         "//build/bazel/platforms/arch:arm": "arm",
         "//build/bazel/platforms/arch:arm64": "arm64",
         "//conditions:default": "version/tz_version",
-    }),
-    sub_dir = "tz",
-)`}})
+    })`,
+				"sub_dir": `"tz"`,
+			})}})
 }
diff --git a/bp2build/python_binary_conversion_test.go b/bp2build/python_binary_conversion_test.go
index 5b4829e..01b6aa2 100644
--- a/bp2build/python_binary_conversion_test.go
+++ b/bp2build/python_binary_conversion_test.go
@@ -7,7 +7,8 @@
 	"android/soong/python"
 )
 
-func runBp2BuildTestCaseWithLibs(t *testing.T, tc bp2buildTestCase) {
+func runBp2BuildTestCaseWithPythonLibraries(t *testing.T, tc bp2buildTestCase) {
+	t.Helper()
 	runBp2BuildTestCase(t, func(ctx android.RegistrationContext) {
 		ctx.RegisterModuleType("python_library", python.PythonLibraryFactory)
 		ctx.RegisterModuleType("python_library_host", python.PythonLibraryHostFactory)
@@ -15,7 +16,7 @@
 }
 
 func TestPythonBinaryHostSimple(t *testing.T) {
-	runBp2BuildTestCaseWithLibs(t, bp2buildTestCase{
+	runBp2BuildTestCaseWithPythonLibraries(t, bp2buildTestCase{
 		description:                        "simple python_binary_host converts to a native py_binary",
 		moduleTypeUnderTest:                "python_binary_host",
 		moduleTypeUnderTestFactory:         python.PythonBinaryHostFactory,
@@ -41,17 +42,17 @@
       srcs: ["b/e.py"],
       bazel_module: { bp2build_available: true },
     }`,
-		expectedBazelTargets: []string{`py_binary(
-    name = "foo",
-    data = ["files/data.txt"],
-    deps = [":bar"],
-    main = "a.py",
-    srcs = [
+		expectedBazelTargets: []string{
+			makeBazelTarget("py_binary", "foo", attrNameToString{
+				"data": `["files/data.txt"]`,
+				"deps": `[":bar"]`,
+				"main": `"a.py"`,
+				"srcs": `[
         "a.py",
         "b/c.py",
         "b/d.py",
-    ],
-)`,
+    ]`,
+			}),
 		},
 	})
 }
@@ -77,11 +78,11 @@
     bazel_module: { bp2build_available: true },
 }
 `,
-		expectedBazelTargets: []string{`py_binary(
-    name = "foo",
-    python_version = "PY2",
-    srcs = ["a.py"],
-)`,
+		expectedBazelTargets: []string{
+			makeBazelTarget("py_binary", "foo", attrNameToString{
+				"python_version": `"PY2"`,
+				"srcs":           `["a.py"]`,
+			}),
 		},
 	})
 }
@@ -109,10 +110,9 @@
 `,
 		expectedBazelTargets: []string{
 			// python_version is PY3 by default.
-			`py_binary(
-    name = "foo",
-    srcs = ["a.py"],
-)`,
+			makeBazelTarget("py_binary", "foo", attrNameToString{
+				"srcs": `["a.py"]`,
+			}),
 		},
 	})
 }
@@ -139,14 +139,13 @@
 					},
 				 }`,
 		expectedBazelTargets: []string{
-			`py_binary(
-    name = "foo-arm",
-    srcs = select({
+			makeBazelTarget("py_binary", "foo-arm", attrNameToString{
+				"srcs": `select({
         "//build/bazel/platforms/arch:arm": ["arm.py"],
         "//build/bazel/platforms/arch:x86": ["x86.py"],
         "//conditions:default": [],
-    }),
-)`,
+    })`,
+			}),
 		},
 	})
 }
diff --git a/bp2build/python_library_conversion_test.go b/bp2build/python_library_conversion_test.go
index 7f983ad..e334592 100644
--- a/bp2build/python_library_conversion_test.go
+++ b/bp2build/python_library_conversion_test.go
@@ -11,38 +11,49 @@
 // TODO(alexmarquez): Should be lifted into a generic Bp2Build file
 type PythonLibBp2Build func(ctx android.TopDownMutatorContext)
 
-func TestPythonLibrary(t *testing.T) {
-	testPythonLib(t, "python_library",
-		python.PythonLibraryFactory, python.PythonLibraryBp2Build,
-		func(ctx android.RegistrationContext) {})
-}
-
-func TestPythonLibraryHost(t *testing.T) {
-	testPythonLib(t, "python_library_host",
-		python.PythonLibraryHostFactory, python.PythonLibraryHostBp2Build,
-		func(ctx android.RegistrationContext) {
-			ctx.RegisterModuleType("python_library", python.PythonLibraryFactory)
-		})
-}
-
-func testPythonLib(t *testing.T, modType string,
-	factory android.ModuleFactory, mutator PythonLibBp2Build,
-	registration func(ctx android.RegistrationContext)) {
+func runPythonLibraryTestCase(t *testing.T, tc bp2buildTestCase) {
 	t.Helper()
-	// Simple
-	runBp2BuildTestCase(t, registration, bp2buildTestCase{
-		description:                        fmt.Sprintf("simple %s converts to a native py_library", modType),
-		moduleTypeUnderTest:                modType,
-		moduleTypeUnderTestFactory:         factory,
-		moduleTypeUnderTestBp2BuildMutator: mutator,
-		filesystem: map[string]string{
-			"a.py":           "",
-			"b/c.py":         "",
-			"b/d.py":         "",
-			"b/e.py":         "",
-			"files/data.txt": "",
-		},
-		blueprint: fmt.Sprintf(`%s {
+	testCase := tc
+	testCase.description = fmt.Sprintf(testCase.description, "python_library")
+	testCase.blueprint = fmt.Sprintf(testCase.blueprint, "python_library")
+	testCase.moduleTypeUnderTest = "python_library"
+	testCase.moduleTypeUnderTestFactory = python.PythonLibraryFactory
+	testCase.moduleTypeUnderTestBp2BuildMutator = python.PythonLibraryBp2Build
+	runBp2BuildTestCaseSimple(t, testCase)
+}
+
+func runPythonLibraryHostTestCase(t *testing.T, tc bp2buildTestCase) {
+	t.Helper()
+	testCase := tc
+	testCase.description = fmt.Sprintf(testCase.description, "python_library_host")
+	testCase.blueprint = fmt.Sprintf(testCase.blueprint, "python_library_host")
+	testCase.moduleTypeUnderTest = "python_library_host"
+	testCase.moduleTypeUnderTestFactory = python.PythonLibraryHostFactory
+	testCase.moduleTypeUnderTestBp2BuildMutator = python.PythonLibraryHostBp2Build
+	runBp2BuildTestCase(t, func(ctx android.RegistrationContext) {
+		ctx.RegisterModuleType("python_library", python.PythonLibraryFactory)
+	},
+		testCase)
+}
+
+func runPythonLibraryTestCases(t *testing.T, tc bp2buildTestCase) {
+	t.Helper()
+	runPythonLibraryTestCase(t, tc)
+	runPythonLibraryHostTestCase(t, tc)
+}
+
+func TestSimplePythonLib(t *testing.T) {
+	testCases := []bp2buildTestCase{
+		{
+			description: "simple %s converts to a native py_library",
+			filesystem: map[string]string{
+				"a.py":           "",
+				"b/c.py":         "",
+				"b/d.py":         "",
+				"b/e.py":         "",
+				"files/data.txt": "",
+			},
+			blueprint: `%s {
     name: "foo",
     srcs: ["**/*.py"],
     exclude_srcs: ["b/e.py"],
@@ -54,28 +65,23 @@
       name: "bar",
       srcs: ["b/e.py"],
       bazel_module: { bp2build_available: false },
-    }`, modType),
-		expectedBazelTargets: []string{`py_library(
-    name = "foo",
-    data = ["files/data.txt"],
-    deps = [":bar"],
-    srcs = [
+    }`,
+			expectedBazelTargets: []string{
+				makeBazelTarget("py_library", "foo", attrNameToString{
+					"data": `["files/data.txt"]`,
+					"deps": `[":bar"]`,
+					"srcs": `[
         "a.py",
         "b/c.py",
         "b/d.py",
-    ],
-    srcs_version = "PY3",
-)`,
+    ]`,
+					"srcs_version": `"PY3"`,
+				}),
+			},
 		},
-	})
-
-	// PY2
-	runBp2BuildTestCaseSimple(t, bp2buildTestCase{
-		description:                        fmt.Sprintf("py2 %s converts to a native py_library", modType),
-		moduleTypeUnderTest:                modType,
-		moduleTypeUnderTestFactory:         factory,
-		moduleTypeUnderTestBp2BuildMutator: mutator,
-		blueprint: fmt.Sprintf(`%s {
+		{
+			description: "py2 %s converts to a native py_library",
+			blueprint: `%s {
     name: "foo",
     srcs: ["a.py"],
     version: {
@@ -88,22 +94,17 @@
     },
 
     bazel_module: { bp2build_available: true },
-}`, modType),
-		expectedBazelTargets: []string{`py_library(
-    name = "foo",
-    srcs = ["a.py"],
-    srcs_version = "PY2",
-)`,
+}`,
+			expectedBazelTargets: []string{
+				makeBazelTarget("py_library", "foo", attrNameToString{
+					"srcs":         `["a.py"]`,
+					"srcs_version": `"PY2"`,
+				}),
+			},
 		},
-	})
-
-	// PY3
-	runBp2BuildTestCaseSimple(t, bp2buildTestCase{
-		description:                        fmt.Sprintf("py3 %s converts to a native py_library", modType),
-		moduleTypeUnderTest:                modType,
-		moduleTypeUnderTestFactory:         factory,
-		moduleTypeUnderTestBp2BuildMutator: mutator,
-		blueprint: fmt.Sprintf(`%s {
+		{
+			description: "py3 %s converts to a native py_library",
+			blueprint: `%s {
     name: "foo",
     srcs: ["a.py"],
     version: {
@@ -116,22 +117,17 @@
     },
 
     bazel_module: { bp2build_available: true },
-}`, modType),
-		expectedBazelTargets: []string{`py_library(
-    name = "foo",
-    srcs = ["a.py"],
-    srcs_version = "PY3",
-)`,
+}`,
+			expectedBazelTargets: []string{
+				makeBazelTarget("py_library", "foo", attrNameToString{
+					"srcs":         `["a.py"]`,
+					"srcs_version": `"PY3"`,
+				}),
+			},
 		},
-	})
-
-	// Both
-	runBp2BuildTestCaseSimple(t, bp2buildTestCase{
-		description:                        fmt.Sprintf("py2&3 %s converts to a native py_library", modType),
-		moduleTypeUnderTest:                modType,
-		moduleTypeUnderTestFactory:         factory,
-		moduleTypeUnderTestBp2BuildMutator: mutator,
-		blueprint: fmt.Sprintf(`%s {
+		{
+			description: "py2&3 %s converts to a native py_library",
+			blueprint: `%s {
     name: "foo",
     srcs: ["a.py"],
     version: {
@@ -144,44 +140,31 @@
     },
 
     bazel_module: { bp2build_available: true },
-}`, modType),
-		expectedBazelTargets: []string{
-			// srcs_version is PY2ANDPY3 by default.
-			`py_library(
-    name = "foo",
-    srcs = ["a.py"],
-)`,
+}`,
+			expectedBazelTargets: []string{
+				// srcs_version is PY2ANDPY3 by default.
+				makeBazelTarget("py_library", "foo", attrNameToString{
+					"srcs": `["a.py"]`,
+				}),
+			},
 		},
-	})
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.description, func(t *testing.T) {
+			runPythonLibraryTestCases(t, tc)
+		})
+	}
 }
 
-func TestPythonLibraryArchVariance(t *testing.T) {
-	testPythonArchVariance(t, "python_library", "py_library",
-		python.PythonLibraryFactory, python.PythonLibraryBp2Build,
-		func(ctx android.RegistrationContext) {})
-}
-
-func TestPythonLibraryHostArchVariance(t *testing.T) {
-	testPythonArchVariance(t, "python_library_host", "py_library",
-		python.PythonLibraryHostFactory, python.PythonLibraryHostBp2Build,
-		func(ctx android.RegistrationContext) {})
-}
-
-// TODO: refactor python_binary_conversion_test to use this
-func testPythonArchVariance(t *testing.T, modType, bazelTarget string,
-	factory android.ModuleFactory, mutator PythonLibBp2Build,
-	registration func(ctx android.RegistrationContext)) {
-	t.Helper()
-	runBp2BuildTestCase(t, registration, bp2buildTestCase{
-		description:                        fmt.Sprintf("test %s arch variants", modType),
-		moduleTypeUnderTest:                modType,
-		moduleTypeUnderTestFactory:         factory,
-		moduleTypeUnderTestBp2BuildMutator: mutator,
+func TestPythonArchVariance(t *testing.T) {
+	runPythonLibraryTestCases(t, bp2buildTestCase{
+		description: "test %s arch variants",
 		filesystem: map[string]string{
 			"dir/arm.py": "",
 			"dir/x86.py": "",
 		},
-		blueprint: fmt.Sprintf(`%s {
+		blueprint: `%s {
 					 name: "foo",
 					 arch: {
 						 arm: {
@@ -191,17 +174,16 @@
 							 srcs: ["x86.py"],
 						 },
 					},
-				 }`, modType),
+				 }`,
 		expectedBazelTargets: []string{
-			fmt.Sprintf(`%s(
-    name = "foo",
-    srcs = select({
+			makeBazelTarget("py_library", "foo", attrNameToString{
+				"srcs": `select({
         "//build/bazel/platforms/arch:arm": ["arm.py"],
         "//build/bazel/platforms/arch:x86": ["x86.py"],
         "//conditions:default": [],
-    }),
-    srcs_version = "PY3",
-)`, bazelTarget),
+    })`,
+				"srcs_version": `"PY3"`,
+			}),
 		},
 	})
 }
diff --git a/bp2build/sh_conversion_test.go b/bp2build/sh_conversion_test.go
index 82e0a14..1ca4a0e 100644
--- a/bp2build/sh_conversion_test.go
+++ b/bp2build/sh_conversion_test.go
@@ -64,9 +64,9 @@
     src: "foo.sh",
     bazel_module: { bp2build_available: true },
 }`,
-		expectedBazelTargets: []string{`sh_binary(
-    name = "foo",
-    srcs = ["foo.sh"],
-)`},
+		expectedBazelTargets: []string{
+			makeBazelTarget("sh_binary", "foo", attrNameToString{
+				"srcs": `["foo.sh"]`,
+			})},
 	})
 }
diff --git a/bp2build/soong_config_module_type_conversion_test.go b/bp2build/soong_config_module_type_conversion_test.go
index 4118479..2e6f54c 100644
--- a/bp2build/soong_config_module_type_conversion_test.go
+++ b/bp2build/soong_config_module_type_conversion_test.go
@@ -68,7 +68,7 @@
 		expectedBazelTargets: []string{`cc_library_static(
     name = "foo",
     copts = select({
-        "//build/bazel/product_variables:acme__feature1": ["-DFEATURE1"],
+        "//build/bazel/product_variables:acme__feature1__enabled": ["-DFEATURE1"],
         "//conditions:default": ["-DDEFAULT1"],
     }),
     local_includes = ["."],
@@ -118,7 +118,7 @@
 		expectedBazelTargets: []string{`cc_library_static(
     name = "foo",
     copts = select({
-        "//build/bazel/product_variables:acme__feature1": ["-DFEATURE1"],
+        "//build/bazel/product_variables:acme__feature1__enabled": ["-DFEATURE1"],
         "//conditions:default": ["-DDEFAULT1"],
     }),
     local_includes = ["."],
@@ -246,12 +246,81 @@
         "//build/bazel/product_variables:acme__board__soc_b": ["-DSOC_B"],
         "//conditions:default": ["-DSOC_DEFAULT"],
     }) + select({
-        "//build/bazel/product_variables:acme__feature1": ["-DFEATURE1"],
+        "//build/bazel/product_variables:acme__feature1__enabled": ["-DFEATURE1"],
         "//conditions:default": ["-DDEFAULT1"],
     }) + select({
-        "//build/bazel/product_variables:acme__feature2": ["-DFEATURE2"],
+        "//build/bazel/product_variables:acme__feature2__enabled": ["-DFEATURE2"],
         "//conditions:default": ["-DDEFAULT2"],
     }),
     local_includes = ["."],
 )`}})
 }
+
+func TestSoongConfigModuleType_StringVar_LabelListDeps(t *testing.T) {
+	bp := `
+soong_config_string_variable {
+	name: "board",
+	values: ["soc_a", "soc_b", "soc_c"],
+}
+
+soong_config_module_type {
+	name: "custom_cc_library_static",
+	module_type: "cc_library_static",
+	config_namespace: "acme",
+	variables: ["board"],
+	properties: ["cflags", "static_libs"],
+	bazel_module: { bp2build_available: true },
+}
+
+custom_cc_library_static {
+	name: "foo",
+	bazel_module: { bp2build_available: true },
+	soong_config_variables: {
+		board: {
+			soc_a: {
+				cflags: ["-DSOC_A"],
+				static_libs: ["soc_a_dep"],
+			},
+			soc_b: {
+				cflags: ["-DSOC_B"],
+				static_libs: ["soc_b_dep"],
+			},
+			soc_c: {},
+			conditions_default: {
+				cflags: ["-DSOC_DEFAULT"],
+				static_libs: ["soc_default_static_dep"],
+			},
+		},
+	},
+}`
+
+	otherDeps := `
+cc_library_static { name: "soc_a_dep", bazel_module: { bp2build_available: false } }
+cc_library_static { name: "soc_b_dep", bazel_module: { bp2build_available: false } }
+cc_library_static { name: "soc_default_static_dep", bazel_module: { bp2build_available: false } }
+`
+
+	runSoongConfigModuleTypeTest(t, bp2buildTestCase{
+		description:                        "soong config variables - generates selects for label list attributes",
+		moduleTypeUnderTest:                "cc_library_static",
+		moduleTypeUnderTestFactory:         cc.LibraryStaticFactory,
+		moduleTypeUnderTestBp2BuildMutator: cc.CcLibraryStaticBp2Build,
+		blueprint:                          bp,
+		filesystem: map[string]string{
+			"foo/bar/Android.bp": otherDeps,
+		},
+		expectedBazelTargets: []string{`cc_library_static(
+    name = "foo",
+    copts = select({
+        "//build/bazel/product_variables:acme__board__soc_a": ["-DSOC_A"],
+        "//build/bazel/product_variables:acme__board__soc_b": ["-DSOC_B"],
+        "//conditions:default": ["-DSOC_DEFAULT"],
+    }),
+    implementation_deps = select({
+        "//build/bazel/product_variables:acme__board__soc_a": ["//foo/bar:soc_a_dep"],
+        "//build/bazel/product_variables:acme__board__soc_b": ["//foo/bar:soc_b_dep"],
+        "//conditions:default": ["//foo/bar:soc_default_static_dep"],
+    }),
+    local_includes = ["."],
+)`}})
+}
diff --git a/bp2build/testing.go b/bp2build/testing.go
index 7c2f43a..daa9c22 100644
--- a/bp2build/testing.go
+++ b/bp2build/testing.go
@@ -364,3 +364,16 @@
 		bazel_module: { bp2build_available: false },
 }`, typ, name)
 }
+
+type attrNameToString map[string]string
+
+func makeBazelTarget(typ, name string, attrs attrNameToString) string {
+	attrStrings := make([]string, 0, len(attrs)+1)
+	attrStrings = append(attrStrings, fmt.Sprintf(`    name = "%s",`, name))
+	for _, k := range android.SortedStringKeys(attrs) {
+		attrStrings = append(attrStrings, fmt.Sprintf("    %s = %s,", k, attrs[k]))
+	}
+	return fmt.Sprintf(`%s(
+%s
+)`, typ, strings.Join(attrStrings, "\n"))
+}
diff --git a/cc/androidmk.go b/cc/androidmk.go
index 5c4ef17..45bb1ca 100644
--- a/cc/androidmk.go
+++ b/cc/androidmk.go
@@ -80,7 +80,7 @@
 		// to be installed. And this is breaking some older devices (like marlin)
 		// where system.img is small.
 		Required: c.Properties.AndroidMkRuntimeLibs,
-		Include:  "$(BUILD_SYSTEM)/soong_cc_prebuilt.mk",
+		Include:  "$(BUILD_SYSTEM)/soong_cc_rust_prebuilt.mk",
 
 		ExtraEntries: []android.AndroidMkExtraEntriesFunc{
 			func(ctx android.AndroidMkExtraEntriesContext, entries *android.AndroidMkEntries) {
@@ -582,8 +582,8 @@
 		if p.properties.Check_elf_files != nil {
 			entries.SetBool("LOCAL_CHECK_ELF_FILES", *p.properties.Check_elf_files)
 		} else {
-			// soong_cc_prebuilt.mk does not include check_elf_file.mk by default
-			// because cc_library_shared and cc_binary use soong_cc_prebuilt.mk as well.
+			// soong_cc_rust_prebuilt.mk does not include check_elf_file.mk by default
+			// because cc_library_shared and cc_binary use soong_cc_rust_prebuilt.mk as well.
 			// In order to turn on prebuilt ABI checker, set `LOCAL_CHECK_ELF_FILES` to
 			// true if `p.properties.Check_elf_files` is not specified.
 			entries.SetBool("LOCAL_CHECK_ELF_FILES", true)
diff --git a/cc/bp2build.go b/cc/bp2build.go
index 2059f5e..dde8dd4 100644
--- a/cc/bp2build.go
+++ b/cc/bp2build.go
@@ -320,14 +320,14 @@
 		"CppFlags": &ca.cppFlags,
 	}
 	for propName, attr := range productVarPropNameToAttribute {
-		if props, exists := productVariableProps[propName]; exists {
-			for _, prop := range props {
-				flags, ok := prop.Property.([]string)
+		if productConfigProps, exists := productVariableProps[propName]; exists {
+			for productConfigProp, prop := range productConfigProps {
+				flags, ok := prop.([]string)
 				if !ok {
 					ctx.ModuleErrorf("Could not convert product variable %s property", proptools.PropertyNameForField(propName))
 				}
-				newFlags, _ := bazel.TryVariableSubstitutions(flags, prop.ProductConfigVariable)
-				attr.SetSelectValue(prop.ConfigurationAxis(), prop.FullConfig, newFlags)
+				newFlags, _ := bazel.TryVariableSubstitutions(flags, productConfigProp.Name)
+				attr.SetSelectValue(productConfigProp.ConfigurationAxis(), productConfigProp.SelectKey(), newFlags)
 			}
 		}
 	}
@@ -587,31 +587,35 @@
 		if !exists && !excludesExists {
 			continue
 		}
-		// collect all the configurations that an include or exclude property exists for.
-		// we want to iterate all configurations rather than either the include or exclude because for a
-		// particular configuration we may have only and include or only an exclude to handle
-		configs := make(map[string]bool, len(props)+len(excludeProps))
-		for config := range props {
-			configs[config] = true
+		// Collect all the configurations that an include or exclude property exists for.
+		// We want to iterate all configurations rather than either the include or exclude because, for a
+		// particular configuration, we may have either only an include or an exclude to handle.
+		productConfigProps := make(map[android.ProductConfigProperty]bool, len(props)+len(excludeProps))
+		for p := range props {
+			productConfigProps[p] = true
 		}
-		for config := range excludeProps {
-			configs[config] = true
+		for p := range excludeProps {
+			productConfigProps[p] = true
 		}
 
-		for config := range configs {
-			prop, includesExists := props[config]
-			excludesProp, excludesExists := excludeProps[config]
+		for productConfigProp := range productConfigProps {
+			prop, includesExists := props[productConfigProp]
+			excludesProp, excludesExists := excludeProps[productConfigProp]
 			var includes, excludes []string
 			var ok bool
 			// if there was no includes/excludes property, casting fails and that's expected
-			if includes, ok = prop.Property.([]string); includesExists && !ok {
+			if includes, ok = prop.([]string); includesExists && !ok {
 				ctx.ModuleErrorf("Could not convert product variable %s property", name)
 			}
-			if excludes, ok = excludesProp.Property.([]string); excludesExists && !ok {
+			if excludes, ok = excludesProp.([]string); excludesExists && !ok {
 				ctx.ModuleErrorf("Could not convert product variable %s property", dep.excludesField)
 			}
 
-			dep.attribute.SetSelectValue(prop.ConfigurationAxis(), config, dep.depResolutionFunc(ctx, android.FirstUniqueStrings(includes), excludes))
+			dep.attribute.SetSelectValue(
+				productConfigProp.ConfigurationAxis(),
+				productConfigProp.SelectKey(),
+				dep.depResolutionFunc(ctx, android.FirstUniqueStrings(includes), excludes),
+			)
 		}
 	}
 }
diff --git a/cc/cc_test.go b/cc/cc_test.go
index ad2ccae..9ffe48d 100644
--- a/cc/cc_test.go
+++ b/cc/cc_test.go
@@ -730,9 +730,16 @@
 			gtest: false,
 		}
 
+		cc_binary {
+			name: "test_bin",
+			relative_install_path: "foo/bar/baz",
+			compile_multilib: "both",
+		}
+
 		cc_test {
 			name: "main_test",
 			data_libs: ["test_lib"],
+			data_bins: ["test_bin"],
 			gtest: false,
 		}
  `
@@ -750,10 +757,10 @@
 		t.Fatalf("Expected cc_test to produce output files, error: %s", err)
 	}
 	if len(outputFiles) != 1 {
-		t.Errorf("expected exactly one output file. output files: [%s]", outputFiles)
+		t.Fatalf("expected exactly one output file. output files: [%s]", outputFiles)
 	}
-	if len(testBinary.dataPaths()) != 1 {
-		t.Errorf("expected exactly one test data file. test data files: [%s]", testBinary.dataPaths())
+	if len(testBinary.dataPaths()) != 2 {
+		t.Fatalf("expected exactly one test data file. test data files: [%s]", testBinary.dataPaths())
 	}
 
 	outputPath := outputFiles[0].String()
@@ -766,6 +773,10 @@
 		t.Errorf("expected LOCAL_TEST_DATA to end with `:test_lib.so:foo/bar/baz`,"+
 			" but was '%s'", entries.EntryMap["LOCAL_TEST_DATA"][0])
 	}
+	if !strings.HasSuffix(entries.EntryMap["LOCAL_TEST_DATA"][1], ":test_bin:foo/bar/baz") {
+		t.Errorf("expected LOCAL_TEST_DATA to end with `:test_bin:foo/bar/baz`,"+
+			" but was '%s'", entries.EntryMap["LOCAL_TEST_DATA"][1])
+	}
 }
 
 func TestVndkWhenVndkVersionIsNotSet(t *testing.T) {
diff --git a/cc/test.go b/cc/test.go
index c589165..f37fdae 100644
--- a/cc/test.go
+++ b/cc/test.go
@@ -378,31 +378,26 @@
 
 	ctx.VisitDirectDepsWithTag(dataLibDepTag, func(dep android.Module) {
 		depName := ctx.OtherModuleName(dep)
-		ccDep, ok := dep.(LinkableInterface)
-
+		linkableDep, ok := dep.(LinkableInterface)
 		if !ok {
-			ctx.ModuleErrorf("data_lib %q is not a linkable cc module", depName)
+			ctx.ModuleErrorf("data_lib %q is not a LinkableInterface module", depName)
 		}
-		ccModule, ok := dep.(*Module)
-		if !ok {
-			ctx.ModuleErrorf("data_lib %q is not a cc module", depName)
-		}
-		if ccDep.OutputFile().Valid() {
+		if linkableDep.OutputFile().Valid() {
 			test.data = append(test.data,
-				android.DataPath{SrcPath: ccDep.OutputFile().Path(),
-					RelativeInstallPath: ccModule.installer.relativeInstallPath()})
+				android.DataPath{SrcPath: linkableDep.OutputFile().Path(),
+					RelativeInstallPath: linkableDep.RelativeInstallPath()})
 		}
 	})
 	ctx.VisitDirectDepsWithTag(dataBinDepTag, func(dep android.Module) {
 		depName := ctx.OtherModuleName(dep)
-		ccModule, ok := dep.(*Module)
+		linkableDep, ok := dep.(LinkableInterface)
 		if !ok {
-			ctx.ModuleErrorf("data_bin %q is not a cc module", depName)
+			ctx.ModuleErrorf("data_bin %q is not a LinkableInterface module", depName)
 		}
-		if ccModule.OutputFile().Valid() {
+		if linkableDep.OutputFile().Valid() {
 			test.data = append(test.data,
-				android.DataPath{SrcPath: ccModule.OutputFile().Path(),
-					RelativeInstallPath: ccModule.installer.relativeInstallPath()})
+				android.DataPath{SrcPath: linkableDep.OutputFile().Path(),
+					RelativeInstallPath: linkableDep.RelativeInstallPath()})
 		}
 	})
 
diff --git a/cc/testing.go b/cc/testing.go
index b0a220c..3bf936d 100644
--- a/cc/testing.go
+++ b/cc/testing.go
@@ -763,3 +763,11 @@
 	}
 	return paths
 }
+
+func AssertExcludeFromRecoverySnapshotIs(t *testing.T, ctx *android.TestContext, name string, expected bool, variant string) {
+	t.Helper()
+	m := ctx.ModuleForTests(name, variant).Module().(LinkableInterface)
+	if m.ExcludeFromRecoverySnapshot() != expected {
+		t.Errorf("expected %q ExcludeFromRecoverySnapshot to be %t", m.String(), expected)
+	}
+}
diff --git a/cc/vendor_snapshot_test.go b/cc/vendor_snapshot_test.go
index ca2f569..b5022c8 100644
--- a/cc/vendor_snapshot_test.go
+++ b/cc/vendor_snapshot_test.go
@@ -1020,14 +1020,6 @@
 	assertString(t, staticCfiModule.outputFile.Path().Base(), "libsnapshot.cfi.a")
 }
 
-func assertExcludeFromRecoverySnapshotIs(t *testing.T, ctx *android.TestContext, name string, expected bool) {
-	t.Helper()
-	m := ctx.ModuleForTests(name, recoveryVariant).Module().(*Module)
-	if m.ExcludeFromRecoverySnapshot() != expected {
-		t.Errorf("expected %q ExcludeFromRecoverySnapshot to be %t", m.String(), expected)
-	}
-}
-
 func TestVendorSnapshotExclude(t *testing.T) {
 
 	// This test verifies that the exclude_from_vendor_snapshot property
@@ -1371,13 +1363,13 @@
 	android.FailIfErrored(t, errs)
 
 	// Test an include and exclude framework module.
-	assertExcludeFromRecoverySnapshotIs(t, ctx, "libinclude", false)
-	assertExcludeFromRecoverySnapshotIs(t, ctx, "libexclude", true)
-	assertExcludeFromRecoverySnapshotIs(t, ctx, "libavailable_exclude", true)
+	AssertExcludeFromRecoverySnapshotIs(t, ctx, "libinclude", false, recoveryVariant)
+	AssertExcludeFromRecoverySnapshotIs(t, ctx, "libexclude", true, recoveryVariant)
+	AssertExcludeFromRecoverySnapshotIs(t, ctx, "libavailable_exclude", true, recoveryVariant)
 
 	// A recovery module is excluded, but by its path, not the
 	// exclude_from_recovery_snapshot property.
-	assertExcludeFromRecoverySnapshotIs(t, ctx, "librecovery", false)
+	AssertExcludeFromRecoverySnapshotIs(t, ctx, "librecovery", false, recoveryVariant)
 
 	// Verify the content of the recovery snapshot.
 
diff --git a/licenses/Android.bp b/licenses/Android.bp
index a983b5b..5b764dc 100644
--- a/licenses/Android.bp
+++ b/licenses/Android.bp
@@ -492,36 +492,36 @@
 
 license_kind {
     name: "SPDX-license-identifier-CC-BY-ND",
-    conditions: ["restricted"],
+    conditions: ["by_exception_only"],
 }
 
 license_kind {
     name: "SPDX-license-identifier-CC-BY-ND-1.0",
-    conditions: ["restricted"],
+    conditions: ["by_exception_only"],
     url: "https://spdx.org/licenses/CC-BY-ND-1.0.html",
 }
 
 license_kind {
     name: "SPDX-license-identifier-CC-BY-ND-2.0",
-    conditions: ["restricted"],
+    conditions: ["by_exception_only"],
     url: "https://spdx.org/licenses/CC-BY-ND-2.0.html",
 }
 
 license_kind {
     name: "SPDX-license-identifier-CC-BY-ND-2.5",
-    conditions: ["restricted"],
+    conditions: ["by_exception_only"],
     url: "https://spdx.org/licenses/CC-BY-ND-2.5.html",
 }
 
 license_kind {
     name: "SPDX-license-identifier-CC-BY-ND-3.0",
-    conditions: ["restricted"],
+    conditions: ["by_exception_only"],
     url: "https://spdx.org/licenses/CC-BY-ND-3.0.html",
 }
 
 license_kind {
     name: "SPDX-license-identifier-CC-BY-ND-4.0",
-    conditions: ["restricted"],
+    conditions: ["by_exception_only"],
     url: "https://spdx.org/licenses/CC-BY-ND-4.0.html",
 }
 
@@ -562,7 +562,10 @@
 
 license_kind {
     name: "SPDX-license-identifier-CC-BY-SA-ND",
-    conditions: ["restricted"],
+    conditions: [
+        "restricted",
+        "by_exception_only",
+    ],
 }
 
 license_kind {
diff --git a/mk2rbc/expr.go b/mk2rbc/expr.go
index 0bb8b95..8279d2e 100644
--- a/mk2rbc/expr.go
+++ b/mk2rbc/expr.go
@@ -197,6 +197,51 @@
 	}
 }
 
+type toStringExpr struct {
+	expr starlarkExpr
+}
+
+func (s *toStringExpr) eval(valueMap map[string]starlarkExpr) (res starlarkExpr, same bool) {
+	if x, same := s.expr.eval(valueMap); same {
+		res = s
+	} else {
+		res = &toStringExpr{expr: x}
+	}
+	return
+}
+
+func (s *toStringExpr) emit(ctx *generationContext) {
+	switch s.expr.typ() {
+	case starlarkTypeString, starlarkTypeUnknown:
+		// Assume unknown types are strings already.
+		s.expr.emit(ctx)
+	case starlarkTypeList:
+		ctx.write(`" ".join(`)
+		s.expr.emit(ctx)
+		ctx.write(")")
+	case starlarkTypeInt:
+		ctx.write(`("%d" % (`)
+		s.expr.emit(ctx)
+		ctx.write("))")
+	case starlarkTypeBool:
+		ctx.write("((")
+		s.expr.emit(ctx)
+		ctx.write(`) ? "true" : "")`)
+	case starlarkTypeVoid:
+		ctx.write(`""`)
+	default:
+		panic("Unknown starlark type!")
+	}
+}
+
+func (s *toStringExpr) typ() starlarkType {
+	return starlarkTypeString
+}
+
+func (s *toStringExpr) emitListVarCopy(gctx *generationContext) {
+	s.emit(gctx)
+}
+
 type notExpr struct {
 	expr starlarkExpr
 }
@@ -255,6 +300,12 @@
 		return
 
 	}
+
+	if eq.left.typ() != eq.right.typ() {
+		eq.left = &toStringExpr{expr: eq.left}
+		eq.right = &toStringExpr{expr: eq.right}
+	}
+
 	// General case
 	eq.left.emit(gctx)
 	if eq.isEq {
diff --git a/mk2rbc/mk2rbc.go b/mk2rbc/mk2rbc.go
index 7ce1834..b87ab41 100644
--- a/mk2rbc/mk2rbc.go
+++ b/mk2rbc/mk2rbc.go
@@ -1044,48 +1044,54 @@
 	args[1].TrimLeftSpaces()
 
 	isEq := !strings.HasSuffix(cond.Name, "neq")
-	switch xLeft := ctx.parseMakeString(cond, args[0]).(type) {
-	case *stringLiteralExpr, *variableRefExpr:
-		switch xRight := ctx.parseMakeString(cond, args[1]).(type) {
-		case *stringLiteralExpr, *variableRefExpr:
-			return &eqExpr{left: xLeft, right: xRight, isEq: isEq}
-		case *badExpr:
-			return xRight
-		default:
-			expr, ok := ctx.parseCheckFunctionCallResult(cond, xLeft, args[1])
-			if ok {
-				return expr
-			}
-			return ctx.newBadExpr(cond, "right operand is too complex: %s", args[1].Dump())
-		}
-	case *badExpr:
-		return xLeft
-	default:
-		switch xRight := ctx.parseMakeString(cond, args[1]).(type) {
-		case *stringLiteralExpr, *variableRefExpr:
-			expr, ok := ctx.parseCheckFunctionCallResult(cond, xRight, args[0])
-			if ok {
-				return expr
-			}
-			return ctx.newBadExpr(cond, "left operand is too complex: %s", args[0].Dump())
-		case *badExpr:
-			return xRight
-		default:
-			return ctx.newBadExpr(cond, "operands are too complex: (%s,%s)", args[0].Dump(), args[1].Dump())
-		}
+	xLeft := ctx.parseMakeString(cond, args[0])
+	xRight := ctx.parseMakeString(cond, args[1])
+	if bad, ok := xLeft.(*badExpr); ok {
+		return bad
 	}
+	if bad, ok := xRight.(*badExpr); ok {
+		return bad
+	}
+
+	if expr, ok := ctx.parseCompareSpecialCases(cond, xLeft, xRight); ok {
+		return expr
+	}
+
+	return &eqExpr{left: xLeft, right: xRight, isEq: isEq}
 }
 
-func (ctx *parseContext) parseCheckFunctionCallResult(directive *mkparser.Directive, xValue starlarkExpr,
-	varArg *mkparser.MakeString) (starlarkExpr, bool) {
-	mkSingleVar, ok := varArg.SingleVariable()
-	if !ok {
+// Given an if statement's directive and the left/right starlarkExprs,
+// check if the starlarkExprs are one of a few hardcoded special cases
+// that can be converted to a simpler equalify expression than simply comparing
+// the two.
+func (ctx *parseContext) parseCompareSpecialCases(directive *mkparser.Directive, left starlarkExpr,
+	right starlarkExpr) (starlarkExpr, bool) {
+	isEq := !strings.HasSuffix(directive.Name, "neq")
+
+	// All the special cases require a call on one side and a
+	// string literal/variable on the other. Turn the left/right variables into
+	// call/value variables, and return false if that's not possible.
+	var value starlarkExpr = nil
+	call, ok := left.(*callExpr)
+	if ok {
+		switch right.(type) {
+		case *stringLiteralExpr, *variableRefExpr:
+			value = right
+		}
+	} else {
+		call, _ = right.(*callExpr)
+		switch left.(type) {
+		case *stringLiteralExpr, *variableRefExpr:
+			value = left
+		}
+	}
+
+	if call == nil || value == nil {
 		return nil, false
 	}
-	expr := ctx.parseReference(directive, mkSingleVar)
-	negate := strings.HasSuffix(directive.Name, "neq")
+
 	checkIsSomethingFunction := func(xCall *callExpr) starlarkExpr {
-		s, ok := maybeString(xValue)
+		s, ok := maybeString(value)
 		if !ok || s != "true" {
 			return ctx.newBadExpr(directive,
 				fmt.Sprintf("the result of %s can be compared only to 'true'", xCall.name))
@@ -1095,97 +1101,90 @@
 		}
 		return nil
 	}
-	switch x := expr.(type) {
-	case *callExpr:
-		switch x.name {
-		case "filter":
-			return ctx.parseCompareFilterFuncResult(directive, x, xValue, !negate), true
-		case "filter-out":
-			return ctx.parseCompareFilterFuncResult(directive, x, xValue, negate), true
-		case "wildcard":
-			return ctx.parseCompareWildcardFuncResult(directive, x, xValue, negate), true
-		case "findstring":
-			return ctx.parseCheckFindstringFuncResult(directive, x, xValue, negate), true
-		case "strip":
-			return ctx.parseCompareStripFuncResult(directive, x, xValue, negate), true
-		case "is-board-platform":
-			if xBad := checkIsSomethingFunction(x); xBad != nil {
-				return xBad, true
-			}
-			return &eqExpr{
-				left:  &variableRefExpr{ctx.addVariable("TARGET_BOARD_PLATFORM"), false},
-				right: x.args[0],
-				isEq:  !negate,
-			}, true
-		case "is-board-platform-in-list":
-			if xBad := checkIsSomethingFunction(x); xBad != nil {
-				return xBad, true
-			}
-			return &inExpr{
-				expr:  &variableRefExpr{ctx.addVariable("TARGET_BOARD_PLATFORM"), false},
-				list:  maybeConvertToStringList(x.args[0]),
-				isNot: negate,
-			}, true
-		case "is-product-in-list":
-			if xBad := checkIsSomethingFunction(x); xBad != nil {
-				return xBad, true
-			}
-			return &inExpr{
-				expr:  &variableRefExpr{ctx.addVariable("TARGET_PRODUCT"), true},
-				list:  maybeConvertToStringList(x.args[0]),
-				isNot: negate,
-			}, true
-		case "is-vendor-board-platform":
-			if xBad := checkIsSomethingFunction(x); xBad != nil {
-				return xBad, true
-			}
-			s, ok := maybeString(x.args[0])
-			if !ok {
-				return ctx.newBadExpr(directive, "cannot handle non-constant argument to is-vendor-board-platform"), true
-			}
-			return &inExpr{
-				expr:  &variableRefExpr{ctx.addVariable("TARGET_BOARD_PLATFORM"), false},
-				list:  &variableRefExpr{ctx.addVariable(s + "_BOARD_PLATFORMS"), true},
-				isNot: negate,
-			}, true
 
-		case "is-board-platform2", "is-board-platform-in-list2":
-			if s, ok := maybeString(xValue); !ok || s != "" {
-				return ctx.newBadExpr(directive,
-					fmt.Sprintf("the result of %s can be compared only to empty", x.name)), true
-			}
-			if len(x.args) != 1 {
-				return ctx.newBadExpr(directive, "%s requires an argument", x.name), true
-			}
-			cc := &callExpr{
-				name:       x.name,
-				args:       []starlarkExpr{x.args[0]},
-				returnType: starlarkTypeBool,
-			}
-			if !negate {
-				return &notExpr{cc}, true
-			}
-			return cc, true
-		case "is-vendor-board-qcom":
-			if s, ok := maybeString(xValue); !ok || s != "" {
-				return ctx.newBadExpr(directive,
-					fmt.Sprintf("the result of %s can be compared only to empty", x.name)), true
-			}
-			// if the expression is ifneq (,$(call is-vendor-board-platform,...)), negate==true,
-			// so we should set inExpr.isNot to false
-			return &inExpr{
-				expr:  &variableRefExpr{ctx.addVariable("TARGET_BOARD_PLATFORM"), false},
-				list:  &variableRefExpr{ctx.addVariable("QCOM_BOARD_PLATFORMS"), true},
-				isNot: !negate,
-			}, true
-		default:
-			return ctx.newBadExpr(directive, "Unknown function in ifeq: %s", x.name), true
+	switch call.name {
+	case "filter":
+		return ctx.parseCompareFilterFuncResult(directive, call, value, isEq), true
+	case "filter-out":
+		return ctx.parseCompareFilterFuncResult(directive, call, value, !isEq), true
+	case "wildcard":
+		return ctx.parseCompareWildcardFuncResult(directive, call, value, !isEq), true
+	case "findstring":
+		return ctx.parseCheckFindstringFuncResult(directive, call, value, !isEq), true
+	case "strip":
+		return ctx.parseCompareStripFuncResult(directive, call, value, !isEq), true
+	case "is-board-platform":
+		if xBad := checkIsSomethingFunction(call); xBad != nil {
+			return xBad, true
 		}
-	case *badExpr:
-		return x, true
-	default:
-		return nil, false
+		return &eqExpr{
+			left:  &variableRefExpr{ctx.addVariable("TARGET_BOARD_PLATFORM"), false},
+			right: call.args[0],
+			isEq:  isEq,
+		}, true
+	case "is-board-platform-in-list":
+		if xBad := checkIsSomethingFunction(call); xBad != nil {
+			return xBad, true
+		}
+		return &inExpr{
+			expr:  &variableRefExpr{ctx.addVariable("TARGET_BOARD_PLATFORM"), false},
+			list:  maybeConvertToStringList(call.args[0]),
+			isNot: !isEq,
+		}, true
+	case "is-product-in-list":
+		if xBad := checkIsSomethingFunction(call); xBad != nil {
+			return xBad, true
+		}
+		return &inExpr{
+			expr:  &variableRefExpr{ctx.addVariable("TARGET_PRODUCT"), true},
+			list:  maybeConvertToStringList(call.args[0]),
+			isNot: !isEq,
+		}, true
+	case "is-vendor-board-platform":
+		if xBad := checkIsSomethingFunction(call); xBad != nil {
+			return xBad, true
+		}
+		s, ok := maybeString(call.args[0])
+		if !ok {
+			return ctx.newBadExpr(directive, "cannot handle non-constant argument to is-vendor-board-platform"), true
+		}
+		return &inExpr{
+			expr:  &variableRefExpr{ctx.addVariable("TARGET_BOARD_PLATFORM"), false},
+			list:  &variableRefExpr{ctx.addVariable(s + "_BOARD_PLATFORMS"), true},
+			isNot: !isEq,
+		}, true
+
+	case "is-board-platform2", "is-board-platform-in-list2":
+		if s, ok := maybeString(value); !ok || s != "" {
+			return ctx.newBadExpr(directive,
+				fmt.Sprintf("the result of %s can be compared only to empty", call.name)), true
+		}
+		if len(call.args) != 1 {
+			return ctx.newBadExpr(directive, "%s requires an argument", call.name), true
+		}
+		cc := &callExpr{
+			name:       call.name,
+			args:       []starlarkExpr{call.args[0]},
+			returnType: starlarkTypeBool,
+		}
+		if isEq {
+			return &notExpr{cc}, true
+		}
+		return cc, true
+	case "is-vendor-board-qcom":
+		if s, ok := maybeString(value); !ok || s != "" {
+			return ctx.newBadExpr(directive,
+				fmt.Sprintf("the result of %s can be compared only to empty", call.name)), true
+		}
+		// if the expression is ifneq (,$(call is-vendor-board-platform,...)), negate==true,
+		// so we should set inExpr.isNot to false
+		return &inExpr{
+			expr:  &variableRefExpr{ctx.addVariable("TARGET_BOARD_PLATFORM"), false},
+			list:  &variableRefExpr{ctx.addVariable("QCOM_BOARD_PLATFORMS"), true},
+			isNot: isEq,
+		}, true
 	}
+	return nil, false
 }
 
 func (ctx *parseContext) parseCompareFilterFuncResult(cond *mkparser.Directive,
diff --git a/mk2rbc/mk2rbc_test.go b/mk2rbc/mk2rbc_test.go
index 1e79552..c2c4654 100644
--- a/mk2rbc/mk2rbc_test.go
+++ b/mk2rbc/mk2rbc_test.go
@@ -353,6 +353,21 @@
 `,
 	},
 	{
+		desc:   "ifeq with soong_config_get",
+		mkname: "product.mk",
+		in: `
+ifeq (true,$(call soong_config_get,art_module,source_build))
+endif
+`,
+		expected: `load("//build/make/core:product_config.rbc", "rblf")
+
+def init(g, handle):
+  cfg = rblf.cfg(handle)
+  if "true" == rblf.soong_config_get(g, "art_module", "source_build"):
+    pass
+`,
+	},
+	{
 		desc:   "Check filter result",
 		mkname: "product.mk",
 		in: `
@@ -515,6 +530,21 @@
 `,
 	},
 	{
+		desc:   "if with interpolation",
+		mkname: "product.mk",
+		in: `
+ifeq ($(VARIABLE1)text$(VARIABLE2),true)
+endif
+`,
+		expected: `load("//build/make/core:product_config.rbc", "rblf")
+
+def init(g, handle):
+  cfg = rblf.cfg(handle)
+  if "%stext%s" % (g.get("VARIABLE1", ""), g.get("VARIABLE2", "")) == "true":
+    pass
+`,
+	},
+	{
 		desc:   "ifneq $(X),true",
 		mkname: "product.mk",
 		in: `
diff --git a/rust/androidmk.go b/rust/androidmk.go
index c9f6486..1f18b4a 100644
--- a/rust/androidmk.go
+++ b/rust/androidmk.go
@@ -51,7 +51,7 @@
 
 	ret := android.AndroidMkEntries{
 		OutputFile: android.OptionalPathForPath(mod.UnstrippedOutputFile()),
-		Include:    "$(BUILD_SYSTEM)/soong_rust_prebuilt.mk",
+		Include:    "$(BUILD_SYSTEM)/soong_cc_rust_prebuilt.mk",
 		ExtraEntries: []android.AndroidMkExtraEntriesFunc{
 			func(ctx android.AndroidMkExtraEntriesContext, entries *android.AndroidMkEntries) {
 				entries.AddStrings("LOCAL_RLIB_LIBRARIES", mod.Properties.AndroidMkRlibs...)
diff --git a/rust/rust.go b/rust/rust.go
index 3cc7868..0fd781e 100644
--- a/rust/rust.go
+++ b/rust/rust.go
@@ -393,6 +393,10 @@
 	WholeStaticLibs []string
 	HeaderLibs      []string
 
+	// Used for data dependencies adjacent to tests
+	DataLibs []string
+	DataBins []string
+
 	CrtBegin, CrtEnd string
 }
 
@@ -982,6 +986,8 @@
 	procMacroDepTag     = dependencyTag{name: "procMacro", procMacro: true}
 	testPerSrcDepTag    = dependencyTag{name: "rust_unit_tests"}
 	sourceDepTag        = dependencyTag{name: "source"}
+	dataLibDepTag       = dependencyTag{name: "data lib"}
+	dataBinDepTag       = dependencyTag{name: "data bin"}
 )
 
 func IsDylibDepTag(depTag blueprint.DependencyTag) bool {
@@ -1428,6 +1434,12 @@
 		}
 	}
 
+	actx.AddVariationDependencies([]blueprint.Variation{
+		{Mutator: "link", Variation: "shared"},
+	}, dataLibDepTag, deps.DataLibs...)
+
+	actx.AddVariationDependencies(nil, dataBinDepTag, deps.DataBins...)
+
 	// proc_macros are compiler plugins, and so we need the host arch variant as a dependendcy.
 	actx.AddFarVariationDependencies(ctx.Config().BuildOSTarget.Variations(), procMacroDepTag, deps.ProcMacros...)
 }
diff --git a/rust/rust_test.go b/rust/rust_test.go
index b99b1e6..97bd541 100644
--- a/rust/rust_test.go
+++ b/rust/rust_test.go
@@ -79,8 +79,10 @@
 }
 
 const (
-	sharedVendorVariant = "android_vendor.29_arm64_armv8-a_shared"
-	rlibVendorVariant   = "android_vendor.29_arm64_armv8-a_rlib_rlib-std"
+	sharedVendorVariant   = "android_vendor.29_arm64_armv8-a_shared"
+	rlibVendorVariant     = "android_vendor.29_arm64_armv8-a_rlib_rlib-std"
+	sharedRecoveryVariant = "android_recovery_arm64_armv8-a_shared"
+	rlibRecoveryVariant   = "android_recovery_arm64_armv8-a_rlib_rlib-std"
 )
 
 func testRustVndkFs(t *testing.T, bp string, fs android.MockFS) *android.TestContext {
@@ -101,7 +103,22 @@
 		),
 	).RunTestWithBp(t, bp)
 	return result.TestContext
+}
 
+func testRustRecoveryFsVersions(t *testing.T, bp string, fs android.MockFS, device_version, vndk_version, recovery_version string) *android.TestContext {
+	skipTestIfOsNotSupported(t)
+	result := android.GroupFixturePreparers(
+		prepareForRustTest,
+		fs.AddToFixture(),
+		android.FixtureModifyProductVariables(
+			func(variables android.FixtureProductVariables) {
+				variables.DeviceVndkVersion = StringPtr(device_version)
+				variables.RecoverySnapshotVersion = StringPtr(recovery_version)
+				variables.Platform_vndk_version = StringPtr(vndk_version)
+			},
+		),
+	).RunTestWithBp(t, bp)
+	return result.TestContext
 }
 
 // testRustCov returns a TestContext in which a basic environment has been
diff --git a/rust/test.go b/rust/test.go
index bb877a9..3eea0ad 100644
--- a/rust/test.go
+++ b/rust/test.go
@@ -18,6 +18,7 @@
 	"github.com/google/blueprint/proptools"
 
 	"android/soong/android"
+	"android/soong/cc"
 	"android/soong/tradefed"
 )
 
@@ -49,6 +50,12 @@
 	// the test
 	Data []string `android:"path,arch_variant"`
 
+	// list of shared library modules that should be installed alongside the test
+	Data_libs []string `android:"arch_variant"`
+
+	// list of binary modules that should be installed alongside the test
+	Data_bins []string `android:"arch_variant"`
+
 	// Flag to indicate whether or not to create test config automatically. If AndroidTest.xml
 	// doesn't exist next to the Android.bp, this attribute doesn't need to be set to true
 	// explicitly.
@@ -137,6 +144,32 @@
 
 	dataSrcPaths := android.PathsForModuleSrc(ctx, test.Properties.Data)
 
+	ctx.VisitDirectDepsWithTag(dataLibDepTag, func(dep android.Module) {
+		depName := ctx.OtherModuleName(dep)
+		linkableDep, ok := dep.(cc.LinkableInterface)
+		if !ok {
+			ctx.ModuleErrorf("data_lib %q is not a linkable module", depName)
+		}
+		if linkableDep.OutputFile().Valid() {
+			test.data = append(test.data,
+				android.DataPath{SrcPath: linkableDep.OutputFile().Path(),
+					RelativeInstallPath: linkableDep.RelativeInstallPath()})
+		}
+	})
+
+	ctx.VisitDirectDepsWithTag(dataBinDepTag, func(dep android.Module) {
+		depName := ctx.OtherModuleName(dep)
+		linkableDep, ok := dep.(cc.LinkableInterface)
+		if !ok {
+			ctx.ModuleErrorf("data_bin %q is not a linkable module", depName)
+		}
+		if linkableDep.OutputFile().Valid() {
+			test.data = append(test.data,
+				android.DataPath{SrcPath: linkableDep.OutputFile().Path(),
+					RelativeInstallPath: linkableDep.RelativeInstallPath()})
+		}
+	})
+
 	for _, dataSrcPath := range dataSrcPaths {
 		test.data = append(test.data, android.DataPath{SrcPath: dataSrcPath})
 	}
@@ -194,6 +227,9 @@
 
 	deps.Rustlibs = append(deps.Rustlibs, "libtest")
 
+	deps.DataLibs = append(deps.DataLibs, test.Properties.Data_libs...)
+	deps.DataBins = append(deps.DataBins, test.Properties.Data_bins...)
+
 	return deps
 }
 
diff --git a/rust/test_test.go b/rust/test_test.go
index 892761a..1124176 100644
--- a/rust/test_test.go
+++ b/rust/test_test.go
@@ -74,3 +74,129 @@
 		t.Errorf("Device rust_test module 'my_test' does not link libstd as an rlib")
 	}
 }
+
+func TestDataLibs(t *testing.T) {
+	bp := `
+		cc_library {
+			name: "test_lib",
+			srcs: ["test_lib.cpp"],
+		}
+
+		rust_binary {
+			name: "rusty",
+			srcs: ["foo.rs"],
+			compile_multilib: "both",
+		}
+
+		rust_ffi {
+			name: "librust_test_lib",
+			crate_name: "rust_test_lib",
+			srcs: ["test_lib.rs"],
+			relative_install_path: "foo/bar/baz",
+			compile_multilib: "64",
+		}
+
+		rust_test {
+			name: "main_test",
+			srcs: ["foo.rs"],
+			data_libs: ["test_lib"],
+			data_bins: ["rusty"],
+		}
+ `
+
+	ctx := testRust(t, bp)
+
+	module := ctx.ModuleForTests("main_test", "android_arm64_armv8-a").Module()
+	testBinary := module.(*Module).compiler.(*testDecorator)
+	outputFiles, err := module.(android.OutputFileProducer).OutputFiles("")
+	if err != nil {
+		t.Fatalf("Expected rust_test to produce output files, error: %s", err)
+	}
+	if len(outputFiles) != 1 {
+		t.Fatalf("expected exactly one output file. output files: [%s]", outputFiles)
+	}
+	if len(testBinary.dataPaths()) != 2 {
+		t.Fatalf("expected exactly two test data files. test data files: [%s]", testBinary.dataPaths())
+	}
+
+	outputPath := outputFiles[0].String()
+	dataLibraryPath := testBinary.dataPaths()[0].SrcPath.String()
+	dataBinaryPath := testBinary.dataPaths()[1].SrcPath.String()
+
+	if !strings.HasSuffix(outputPath, "/main_test") {
+		t.Errorf("expected test output file to be 'main_test', but was '%s'", outputPath)
+	}
+	if !strings.HasSuffix(dataLibraryPath, "/test_lib.so") {
+		t.Errorf("expected test data file to be 'test_lib.so', but was '%s'", dataLibraryPath)
+	}
+	if !strings.HasSuffix(dataBinaryPath, "/rusty") {
+		t.Errorf("expected test data file to be 'test_lib.so', but was '%s'", dataBinaryPath)
+	}
+}
+
+func TestDataLibsRelativeInstallPath(t *testing.T) {
+	bp := `
+		cc_library {
+			name: "test_lib",
+			srcs: ["test_lib.cpp"],
+			relative_install_path: "foo/bar/baz",
+			compile_multilib: "64",
+		}
+
+		rust_ffi {
+			name: "librust_test_lib",
+			crate_name: "rust_test_lib",
+			srcs: ["test_lib.rs"],
+			relative_install_path: "foo/bar/baz",
+			compile_multilib: "64",
+		}
+
+		rust_binary {
+			name: "rusty",
+			srcs: ["foo.rs"],
+			relative_install_path: "foo/bar/baz",
+			compile_multilib: "64",
+		}
+
+		rust_test {
+			name: "main_test",
+			srcs: ["foo.rs"],
+			data_libs: ["test_lib", "librust_test_lib"],
+			data_bins: ["rusty"],
+			compile_multilib: "64",
+		}
+ `
+
+	ctx := testRust(t, bp)
+	module := ctx.ModuleForTests("main_test", "android_arm64_armv8-a").Module()
+	testBinary := module.(*Module).compiler.(*testDecorator)
+	outputFiles, err := module.(android.OutputFileProducer).OutputFiles("")
+	if err != nil {
+		t.Fatalf("Expected rust_test to produce output files, error: %s", err)
+	}
+	if len(outputFiles) != 1 {
+		t.Fatalf("expected exactly one output file. output files: [%s]", outputFiles)
+	}
+	if len(testBinary.dataPaths()) != 3 {
+		t.Fatalf("expected exactly two test data files. test data files: [%s]", testBinary.dataPaths())
+	}
+
+	outputPath := outputFiles[0].String()
+
+	if !strings.HasSuffix(outputPath, "/main_test") {
+		t.Errorf("expected test output file to be 'main_test', but was '%s'", outputPath)
+	}
+	entries := android.AndroidMkEntriesForTest(t, ctx, module)[0]
+	if !strings.HasSuffix(entries.EntryMap["LOCAL_TEST_DATA"][0], ":test_lib.so:foo/bar/baz") {
+		t.Errorf("expected LOCAL_TEST_DATA to end with `:test_lib.so:foo/bar/baz`,"+
+			" but was '%s'", entries.EntryMap["LOCAL_TEST_DATA"][0])
+	}
+	if !strings.HasSuffix(entries.EntryMap["LOCAL_TEST_DATA"][1], ":librust_test_lib.so:foo/bar/baz") {
+		t.Errorf("expected LOCAL_TEST_DATA to end with `:librust_test_lib.so:foo/bar/baz`,"+
+			" but was '%s'", entries.EntryMap["LOCAL_TEST_DATA"][1])
+	}
+	if !strings.HasSuffix(entries.EntryMap["LOCAL_TEST_DATA"][2], ":rusty:foo/bar/baz") {
+		t.Errorf("expected LOCAL_TEST_DATA to end with `:rusty:foo/bar/baz`,"+
+			" but was '%s'", entries.EntryMap["LOCAL_TEST_DATA"][2])
+	}
+}
diff --git a/rust/testing.go b/rust/testing.go
index 9f8ed54..1b34dfe 100644
--- a/rust/testing.go
+++ b/rust/testing.go
@@ -75,6 +75,7 @@
 			apex_available: ["//apex_available:platform", "//apex_available:anyapex"],
 			min_sdk_version: "29",
 			vendor_available: true,
+			recovery_available: true,
 			llndk: {
 				symbol_file: "liblog.map.txt",
 			},
@@ -106,6 +107,7 @@
 			host_supported: true,
 			vendor_available: true,
 			vendor_ramdisk_available: true,
+			recovery_available: true,
 			native_coverage: false,
 			sysroot: true,
 			apex_available: ["//apex_available:platform", "//apex_available:anyapex"],
@@ -118,6 +120,7 @@
 			host_supported: true,
 			vendor_available: true,
 			vendor_ramdisk_available: true,
+			recovery_available: true,
 			native_coverage: false,
 			apex_available: ["//apex_available:platform", "//apex_available:anyapex"],
 			min_sdk_version: "29",
diff --git a/rust/vendor_snapshot_test.go b/rust/vendor_snapshot_test.go
index bfa6f36..03bd867 100644
--- a/rust/vendor_snapshot_test.go
+++ b/rust/vendor_snapshot_test.go
@@ -1028,3 +1028,354 @@
 		t.Errorf("wanted memtag_binary AndroidMkStaticLibs %q, got %q", w, g)
 	}
 }
+
+func TestRecoverySnapshotCapture(t *testing.T) {
+	bp := `
+	rust_ffi {
+		name: "librecovery",
+		recovery: true,
+		srcs: ["foo.rs"],
+		crate_name: "recovery",
+	}
+
+	rust_ffi {
+		name: "librecovery_available",
+		recovery_available: true,
+		srcs: ["foo.rs"],
+		crate_name: "recovery_available",
+	}
+
+	rust_library_rlib {
+		name: "librecovery_rlib",
+		recovery: true,
+		srcs: ["foo.rs"],
+		crate_name: "recovery_rlib",
+	}
+
+	rust_library_rlib {
+		name: "librecovery_available_rlib",
+		recovery_available: true,
+		srcs: ["foo.rs"],
+		crate_name: "recovery_available_rlib",
+	}
+
+	rust_binary {
+		name: "recovery_bin",
+		recovery: true,
+		srcs: ["foo.rs"],
+	}
+
+	rust_binary {
+		name: "recovery_available_bin",
+		recovery_available: true,
+		srcs: ["foo.rs"],
+	}
+
+`
+	// Check Recovery snapshot output.
+
+	ctx := testRustRecoveryFsVersions(t, bp, rustMockedFiles, "", "29", "current")
+	snapshotDir := "recovery-snapshot"
+	snapshotVariantPath := filepath.Join("out/soong", snapshotDir, "arm64")
+	snapshotSingleton := ctx.SingletonForTests("recovery-snapshot")
+
+	var jsonFiles []string
+
+	for _, arch := range [][]string{
+		[]string{"arm64", "armv8-a"},
+	} {
+		archType := arch[0]
+		archVariant := arch[1]
+		archDir := fmt.Sprintf("arch-%s-%s", archType, archVariant)
+
+		// For shared libraries, all recovery:true and recovery_available modules are captured.
+		sharedVariant := fmt.Sprintf("android_recovery_%s_%s_shared", archType, archVariant)
+		sharedDir := filepath.Join(snapshotVariantPath, archDir, "shared")
+		cc.CheckSnapshot(t, ctx, snapshotSingleton, "librecovery", "librecovery.so", sharedDir, sharedVariant)
+		cc.CheckSnapshot(t, ctx, snapshotSingleton, "librecovery_available", "librecovery_available.so", sharedDir, sharedVariant)
+		jsonFiles = append(jsonFiles,
+			filepath.Join(sharedDir, "librecovery.so.json"),
+			filepath.Join(sharedDir, "librecovery_available.so.json"))
+
+		// For static libraries, all recovery:true and recovery_available modules are captured.
+		staticVariant := fmt.Sprintf("android_recovery_%s_%s_static", archType, archVariant)
+		staticDir := filepath.Join(snapshotVariantPath, archDir, "static")
+		cc.CheckSnapshot(t, ctx, snapshotSingleton, "librecovery", "librecovery.a", staticDir, staticVariant)
+		cc.CheckSnapshot(t, ctx, snapshotSingleton, "librecovery_available", "librecovery_available.a", staticDir, staticVariant)
+		jsonFiles = append(jsonFiles,
+			filepath.Join(staticDir, "librecovery.a.json"),
+			filepath.Join(staticDir, "librecovery_available.a.json"))
+
+		// For rlib libraries, all recovery:true and recovery_available modules are captured.
+		rlibVariant := fmt.Sprintf("android_recovery_%s_%s_rlib_rlib-std", archType, archVariant)
+		rlibDir := filepath.Join(snapshotVariantPath, archDir, "rlib")
+		cc.CheckSnapshot(t, ctx, snapshotSingleton, "librecovery_rlib", "librecovery_rlib.rlib", rlibDir, rlibVariant)
+		cc.CheckSnapshot(t, ctx, snapshotSingleton, "librecovery_available_rlib", "librecovery_available_rlib.rlib", rlibDir, rlibVariant)
+		jsonFiles = append(jsonFiles,
+			filepath.Join(rlibDir, "librecovery_rlib.rlib.json"),
+			filepath.Join(rlibDir, "librecovery_available_rlib.rlib.json"))
+
+		// For binary executables, all recovery:true and recovery_available modules are captured.
+		if archType == "arm64" {
+			binaryVariant := fmt.Sprintf("android_recovery_%s_%s", archType, archVariant)
+			binaryDir := filepath.Join(snapshotVariantPath, archDir, "binary")
+			cc.CheckSnapshot(t, ctx, snapshotSingleton, "recovery_bin", "recovery_bin", binaryDir, binaryVariant)
+			cc.CheckSnapshot(t, ctx, snapshotSingleton, "recovery_available_bin", "recovery_available_bin", binaryDir, binaryVariant)
+			jsonFiles = append(jsonFiles,
+				filepath.Join(binaryDir, "recovery_bin.json"),
+				filepath.Join(binaryDir, "recovery_available_bin.json"))
+		}
+	}
+
+	for _, jsonFile := range jsonFiles {
+		// verify all json files exist
+		if snapshotSingleton.MaybeOutput(jsonFile).Rule == nil {
+			t.Errorf("%q expected but not found", jsonFile)
+		}
+	}
+}
+
+func TestRecoverySnapshotExclude(t *testing.T) {
+	// This test verifies that the exclude_from_recovery_snapshot property
+	// makes its way from the Android.bp source file into the module data
+	// structure. It also verifies that modules are correctly included or
+	// excluded in the recovery snapshot based on their path (framework or
+	// vendor) and the exclude_from_recovery_snapshot property.
+
+	frameworkBp := `
+		rust_ffi_shared {
+			name: "libinclude",
+			srcs: ["src/include.rs"],
+			recovery_available: true,
+			crate_name: "include",
+		}
+		rust_ffi_shared {
+			name: "libexclude",
+			srcs: ["src/exclude.rs"],
+			recovery: true,
+			exclude_from_recovery_snapshot: true,
+			crate_name: "exclude",
+		}
+		rust_ffi_shared {
+			name: "libavailable_exclude",
+			srcs: ["src/exclude.rs"],
+			recovery_available: true,
+			exclude_from_recovery_snapshot: true,
+			crate_name: "available_exclude",
+		}
+		rust_library_rlib {
+			name: "libinclude_rlib",
+			srcs: ["src/include.rs"],
+			recovery_available: true,
+			crate_name: "include_rlib",
+		}
+		rust_library_rlib {
+			name: "libexclude_rlib",
+			srcs: ["src/exclude.rs"],
+			recovery: true,
+			exclude_from_recovery_snapshot: true,
+			crate_name: "exclude_rlib",
+		}
+		rust_library_rlib {
+			name: "libavailable_exclude_rlib",
+			srcs: ["src/exclude.rs"],
+			recovery_available: true,
+			exclude_from_recovery_snapshot: true,
+			crate_name: "available_exclude_rlib",
+		}
+	`
+
+	vendorProprietaryBp := `
+		rust_ffi_shared {
+			name: "librecovery",
+			srcs: ["recovery.rs"],
+			recovery: true,
+			crate_name: "recovery",
+		}
+		rust_library_rlib {
+			name: "librecovery_rlib",
+			srcs: ["recovery.rs"],
+			recovery: true,
+			crate_name: "recovery_rlib",
+		}
+	`
+
+	mockFS := map[string][]byte{
+		"framework/Android.bp": []byte(frameworkBp),
+		"framework/include.rs": nil,
+		"framework/exclude.rs": nil,
+		"device/Android.bp":    []byte(vendorProprietaryBp),
+		"device/recovery.rs":   nil,
+	}
+
+	ctx := testRustRecoveryFsVersions(t, "", mockFS, "", "29", "current")
+
+	// Test an include and exclude framework module.
+	cc.AssertExcludeFromRecoverySnapshotIs(t, ctx, "libinclude", false, sharedRecoveryVariant)
+	cc.AssertExcludeFromRecoverySnapshotIs(t, ctx, "libexclude", true, sharedRecoveryVariant)
+	cc.AssertExcludeFromRecoverySnapshotIs(t, ctx, "libavailable_exclude", true, sharedRecoveryVariant)
+	cc.AssertExcludeFromRecoverySnapshotIs(t, ctx, "libinclude_rlib", false, rlibRecoveryVariant)
+	cc.AssertExcludeFromRecoverySnapshotIs(t, ctx, "libexclude_rlib", true, rlibRecoveryVariant)
+	cc.AssertExcludeFromRecoverySnapshotIs(t, ctx, "libavailable_exclude_rlib", true, rlibRecoveryVariant)
+
+	// A recovery module is excluded, but by its path not the exclude_from_recovery_snapshot property
+	// ('device/' and 'vendor/' are default excluded). See snapshot/recovery_snapshot.go for more detail.
+	cc.AssertExcludeFromRecoverySnapshotIs(t, ctx, "librecovery", false, sharedRecoveryVariant)
+	cc.AssertExcludeFromRecoverySnapshotIs(t, ctx, "librecovery_rlib", false, rlibRecoveryVariant)
+
+	// Verify the content of the recovery snapshot.
+
+	snapshotDir := "recovery-snapshot"
+	snapshotVariantPath := filepath.Join("out/soong", snapshotDir, "arm64")
+	snapshotSingleton := ctx.SingletonForTests("recovery-snapshot")
+
+	var includeJsonFiles []string
+	var excludeJsonFiles []string
+
+	for _, arch := range [][]string{
+		[]string{"arm64", "armv8-a"},
+	} {
+		archType := arch[0]
+		archVariant := arch[1]
+		archDir := fmt.Sprintf("arch-%s-%s", archType, archVariant)
+
+		sharedVariant := fmt.Sprintf("android_recovery_%s_%s_shared", archType, archVariant)
+		rlibVariant := fmt.Sprintf("android_recovery_%s_%s_rlib_rlib-std", archType, archVariant)
+		sharedDir := filepath.Join(snapshotVariantPath, archDir, "shared")
+		rlibDir := filepath.Join(snapshotVariantPath, archDir, "rlib")
+
+		// Included modules
+		cc.CheckSnapshot(t, ctx, snapshotSingleton, "libinclude", "libinclude.so", sharedDir, sharedVariant)
+		includeJsonFiles = append(includeJsonFiles, filepath.Join(sharedDir, "libinclude.so.json"))
+		cc.CheckSnapshot(t, ctx, snapshotSingleton, "libinclude_rlib", "libinclude_rlib.rlib", rlibDir, rlibVariant)
+		includeJsonFiles = append(includeJsonFiles, filepath.Join(rlibDir, "libinclude_rlib.rlib.json"))
+
+		// Excluded modules
+		cc.CheckSnapshotExclude(t, ctx, snapshotSingleton, "libexclude", "libexclude.so", sharedDir, sharedVariant)
+		excludeJsonFiles = append(excludeJsonFiles, filepath.Join(sharedDir, "libexclude.so.json"))
+		cc.CheckSnapshotExclude(t, ctx, snapshotSingleton, "librecovery", "librecovery.so", sharedDir, sharedVariant)
+		excludeJsonFiles = append(excludeJsonFiles, filepath.Join(sharedDir, "librecovery.so.json"))
+		cc.CheckSnapshotExclude(t, ctx, snapshotSingleton, "libavailable_exclude", "libavailable_exclude.so", sharedDir, sharedVariant)
+		excludeJsonFiles = append(excludeJsonFiles, filepath.Join(sharedDir, "libavailable_exclude.so.json"))
+		cc.CheckSnapshotExclude(t, ctx, snapshotSingleton, "libexclude_rlib", "libexclude_rlib.rlib", rlibDir, rlibVariant)
+		excludeJsonFiles = append(excludeJsonFiles, filepath.Join(rlibDir, "libexclude_rlib.rlib.json"))
+		cc.CheckSnapshotExclude(t, ctx, snapshotSingleton, "librecovery_rlib", "librecovery_rlib.rlib", rlibDir, rlibVariant)
+		excludeJsonFiles = append(excludeJsonFiles, filepath.Join(rlibDir, "librecovery_rlib.rlib.json"))
+		cc.CheckSnapshotExclude(t, ctx, snapshotSingleton, "libavailable_exclude_rlib", "libavailable_exclude_rlib.rlib", rlibDir, rlibVariant)
+		excludeJsonFiles = append(excludeJsonFiles, filepath.Join(rlibDir, "libavailable_exclude_rlib.rlib.json"))
+	}
+
+	// Verify that each json file for an included module has a rule.
+	for _, jsonFile := range includeJsonFiles {
+		if snapshotSingleton.MaybeOutput(jsonFile).Rule == nil {
+			t.Errorf("include json file %q not found", jsonFile)
+		}
+	}
+
+	// Verify that each json file for an excluded module has no rule.
+	for _, jsonFile := range excludeJsonFiles {
+		if snapshotSingleton.MaybeOutput(jsonFile).Rule != nil {
+			t.Errorf("exclude json file %q found", jsonFile)
+		}
+	}
+}
+
+func TestRecoverySnapshotDirected(t *testing.T) {
+	bp := `
+	rust_ffi_shared {
+		name: "librecovery",
+		recovery: true,
+		crate_name: "recovery",
+		srcs: ["foo.rs"],
+	}
+
+	rust_ffi_shared {
+		name: "librecovery_available",
+		recovery_available: true,
+		crate_name: "recovery_available",
+		srcs: ["foo.rs"],
+	}
+
+	rust_library_rlib {
+		name: "librecovery_rlib",
+		recovery: true,
+		crate_name: "recovery",
+		srcs: ["foo.rs"],
+	}
+
+	rust_library_rlib {
+		name: "librecovery_available_rlib",
+		recovery_available: true,
+		crate_name: "recovery_available",
+		srcs: ["foo.rs"],
+	}
+
+	/* TODO: Uncomment when Rust supports the "prefer" property for prebuilts
+	rust_library_rlib {
+		name: "libfoo_rlib",
+		recovery: true,
+		crate_name: "foo",
+	}
+
+	rust_prebuilt_rlib {
+		name: "libfoo_rlib",
+		recovery: true,
+		prefer: true,
+		srcs: ["libfoo.rlib"],
+		crate_name: "foo",
+	}
+	*/
+`
+	ctx := testRustRecoveryFsVersions(t, bp, rustMockedFiles, "current", "29", "current")
+	ctx.Config().TestProductVariables.RecoverySnapshotModules = make(map[string]bool)
+	ctx.Config().TestProductVariables.RecoverySnapshotModules["librecovery"] = true
+	ctx.Config().TestProductVariables.RecoverySnapshotModules["librecovery_rlib"] = true
+	ctx.Config().TestProductVariables.DirectedRecoverySnapshot = true
+
+	// Check recovery snapshot output.
+	snapshotDir := "recovery-snapshot"
+	snapshotVariantPath := filepath.Join("out/soong", snapshotDir, "arm64")
+	snapshotSingleton := ctx.SingletonForTests("recovery-snapshot")
+
+	var includeJsonFiles []string
+
+	for _, arch := range [][]string{
+		[]string{"arm64", "armv8-a"},
+	} {
+		archType := arch[0]
+		archVariant := arch[1]
+		archDir := fmt.Sprintf("arch-%s-%s", archType, archVariant)
+
+		sharedVariant := fmt.Sprintf("android_recovery_%s_%s_shared", archType, archVariant)
+		rlibVariant := fmt.Sprintf("android_recovery_%s_%s_rlib_rlib-std", archType, archVariant)
+		sharedDir := filepath.Join(snapshotVariantPath, archDir, "shared")
+		rlibDir := filepath.Join(snapshotVariantPath, archDir, "rlib")
+
+		// Included modules
+		cc.CheckSnapshot(t, ctx, snapshotSingleton, "librecovery", "librecovery.so", sharedDir, sharedVariant)
+		includeJsonFiles = append(includeJsonFiles, filepath.Join(sharedDir, "librecovery.so.json"))
+		cc.CheckSnapshot(t, ctx, snapshotSingleton, "librecovery_rlib", "librecovery_rlib.rlib", rlibDir, rlibVariant)
+		includeJsonFiles = append(includeJsonFiles, filepath.Join(rlibDir, "librecovery_rlib.rlib.json"))
+
+		// TODO: When Rust supports the "prefer" property for prebuilts, perform this check.
+		/*
+			// Check that snapshot captures "prefer: true" prebuilt
+			cc.CheckSnapshot(t, ctx, snapshotSingleton, "prebuilt_libfoo_rlib", "libfoo_rlib.rlib", rlibDir, rlibVariant)
+			includeJsonFiles = append(includeJsonFiles, filepath.Join(sharedDir, "libfoo_rlib.rlib.json"))
+		*/
+
+		// Excluded modules. Modules not included in the directed recovery snapshot
+		// are still included as fake modules.
+		cc.CheckSnapshotRule(t, ctx, snapshotSingleton, "librecovery_available", "librecovery_available.so", sharedDir, sharedVariant)
+		includeJsonFiles = append(includeJsonFiles, filepath.Join(sharedDir, "librecovery_available.so.json"))
+		cc.CheckSnapshotRule(t, ctx, snapshotSingleton, "librecovery_available_rlib", "librecovery_available_rlib.rlib", rlibDir, rlibVariant)
+		includeJsonFiles = append(includeJsonFiles, filepath.Join(rlibDir, "librecovery_available_rlib.rlib.json"))
+	}
+
+	// Verify that each json file for an included module has a rule.
+	for _, jsonFile := range includeJsonFiles {
+		if snapshotSingleton.MaybeOutput(jsonFile).Rule == nil {
+			t.Errorf("include json file %q not found, %#v", jsonFile, includeJsonFiles)
+		}
+	}
+}
diff --git a/sh/sh_binary.go b/sh/sh_binary.go
index d5033ef..c32cde0 100644
--- a/sh/sh_binary.go
+++ b/sh/sh_binary.go
@@ -284,7 +284,7 @@
 	return []android.AndroidMkEntries{android.AndroidMkEntries{
 		Class:      "EXECUTABLES",
 		OutputFile: android.OptionalPathForPath(s.outputFilePath),
-		Include:    "$(BUILD_SYSTEM)/soong_cc_prebuilt.mk",
+		Include:    "$(BUILD_SYSTEM)/soong_cc_rust_prebuilt.mk",
 		ExtraEntries: []android.AndroidMkExtraEntriesFunc{
 			func(ctx android.AndroidMkExtraEntriesContext, entries *android.AndroidMkEntries) {
 				s.customAndroidMkEntries(entries)
@@ -433,7 +433,7 @@
 	return []android.AndroidMkEntries{android.AndroidMkEntries{
 		Class:      "NATIVE_TESTS",
 		OutputFile: android.OptionalPathForPath(s.outputFilePath),
-		Include:    "$(BUILD_SYSTEM)/soong_cc_prebuilt.mk",
+		Include:    "$(BUILD_SYSTEM)/soong_cc_rust_prebuilt.mk",
 		ExtraEntries: []android.AndroidMkExtraEntriesFunc{
 			func(ctx android.AndroidMkExtraEntriesContext, entries *android.AndroidMkEntries) {
 				s.customAndroidMkEntries(entries)