Handle license conversions in androidmk

1. Support license conversions in the androidmk tool. More details can be
found at go/license-androidmk;
2. As usage examples, locally this functionality has been applied to
aosp/1881088 and aosp/1869664;

Bug: 202964622
Test: m androidmk
Test: manually ran androidmk <typical Android.mk files>
Test: TreeHugger
Change-Id: I7d5013d25ad8961e997109d0378f20f5085e0ecf
diff --git a/androidmk/androidmk/android.go b/androidmk/androidmk/android.go
index f3ad152..1045ca6 100644
--- a/androidmk/androidmk/android.go
+++ b/androidmk/androidmk/android.go
@@ -58,6 +58,7 @@
 	"LOCAL_MODULE_STEM":                    stem,
 	"LOCAL_MODULE_HOST_OS":                 hostOs,
 	"LOCAL_RESOURCE_DIR":                   localizePathList("resource_dirs"),
+	"LOCAL_NOTICE_FILE":                    localizePathList("android_license_files"),
 	"LOCAL_SANITIZE":                       sanitize(""),
 	"LOCAL_SANITIZE_DIAG":                  sanitize("diag."),
 	"LOCAL_STRIP_MODULE":                   strip(),
@@ -111,7 +112,6 @@
 			"LOCAL_PROTOC_OPTIMIZE_TYPE":    "proto.type",
 			"LOCAL_MODULE_OWNER":            "owner",
 			"LOCAL_RENDERSCRIPT_TARGET_API": "renderscript.target_api",
-			"LOCAL_NOTICE_FILE":             "notice",
 			"LOCAL_JAVA_LANGUAGE_VERSION":   "java_version",
 			"LOCAL_INSTRUMENTATION_FOR":     "instrumentation_for",
 			"LOCAL_MANIFEST_FILE":           "manifest",
@@ -185,6 +185,12 @@
 			"LOCAL_JACK_COVERAGE_EXCLUDE_FILTER": "jacoco.exclude_filter",
 
 			"LOCAL_FULL_LIBS_MANIFEST_FILES": "additional_manifests",
+
+			// will be rewrite later to "license_kinds:" by byfix
+			"LOCAL_LICENSE_KINDS": "android_license_kinds",
+			// will be removed later by byfix
+			// TODO: does this property matter in the license module?
+			"LOCAL_LICENSE_CONDITIONS": "android_license_conditions",
 		})
 
 	addStandardProperties(bpparser.BoolType,
diff --git a/androidmk/androidmk/androidmk_test.go b/androidmk/androidmk/androidmk_test.go
index 775a9a8..ca40aaa 100644
--- a/androidmk/androidmk/androidmk_test.go
+++ b/androidmk/androidmk/androidmk_test.go
@@ -1516,7 +1516,8 @@
     ],
 }
 `,
-	}, {
+	},
+	{
 		desc: "Obsolete LOCAL_MODULE_PATH",
 		in: `
 include $(CLEAR_VARS)
@@ -1532,7 +1533,37 @@
   name: "foo",
 
 }
-`},
+`,
+	},
+	{
+		desc: "LOCAL_LICENSE_KINDS, LOCAL_LICENSE_CONDITIONS, LOCAL_NOTICE_FILE",
+		// TODO(b/205615944): When valid "android_license_files" exists, the test requires an Android.mk
+		// file (and an Android.bp file is required as well if the license files locates outside the current
+		// directory). So plan to use a mock file system to mock the Android.mk and Android.bp files.
+		in: `
+include $(CLEAR_VARS)
+LOCAL_MODULE := foo
+LOCAL_LICENSE_KINDS := license_kind
+LOCAL_LICENSE_CONDITIONS := license_condition
+LOCAL_NOTICE_FILE := license_notice
+include $(BUILD_PACKAGE)
+`,
+		expected: `
+package {
+    // See: http://go/android-license-faq
+    default_applicable_licenses: [
+	"Android-Apache-2.0",
+    ],
+}
+
+android_app {
+    name: "foo",
+    // ANDROIDMK TRANSLATION ERROR: Only $(LOCAL_PATH)/.. values are allowed
+    // LOCAL_NOTICE_FILE := license_notice
+
+}
+`,
+	},
 }
 
 func TestEndToEnd(t *testing.T) {
diff --git a/bpfix/bpfix/bpfix.go b/bpfix/bpfix/bpfix.go
index a608630..e1140b8 100644
--- a/bpfix/bpfix/bpfix.go
+++ b/bpfix/bpfix/bpfix.go
@@ -19,9 +19,13 @@
 import (
 	"bytes"
 	"errors"
+	"flag"
 	"fmt"
 	"io"
+	"os"
 	"path/filepath"
+	"reflect"
+	"sort"
 	"strings"
 
 	"github.com/google/blueprint/parser"
@@ -137,11 +141,35 @@
 		Fix:  runPatchListMod(removeObsoleteProperty("sanitize.scudo")),
 	},
 	{
+		Name: "removeAndroidLicenseKinds",
+		Fix:  runPatchListMod(removeIncorrectProperties("android_license_kinds")),
+	},
+	{
+		Name: "removeAndroidLicenseConditions",
+		Fix:  runPatchListMod(removeIncorrectProperties("android_license_conditions")),
+	},
+	{
+		Name: "removeAndroidLicenseFiles",
+		Fix:  runPatchListMod(removeIncorrectProperties("android_license_files")),
+	},
+	{
 		Name: "formatFlagProperties",
 		Fix:  runPatchListMod(formatFlagProperties),
 	},
 }
 
+// for fix that only need to run once
+var fixStepsOnce = []FixStep{
+	{
+		Name: "haveSameLicense",
+		Fix:  haveSameLicense,
+	},
+	{
+		Name: "rewriteLicenseProperties",
+		Fix:  runPatchListMod(rewriteLicenseProperties),
+	},
+}
+
 func NewFixRequest() FixRequest {
 	return FixRequest{}
 }
@@ -196,6 +224,16 @@
 		return nil, err
 	}
 
+	// run fix that is expected to run once first
+	configOnce := NewFixRequest()
+	configOnce.steps = append(configOnce.steps, fixStepsOnce...)
+	if len(configOnce.steps) > 0 {
+		err = f.fixTreeOnce(configOnce)
+		if err != nil {
+			return nil, err
+		}
+	}
+
 	maxNumIterations := 20
 	i := 0
 	for {
@@ -1413,3 +1451,304 @@
 	}
 	return nil
 }
+
+// rewrite the "android_license_kinds" and "android_license_files" properties to a package module
+// (and a license module when needed).
+func rewriteLicenseProperties(mod *parser.Module, buf []byte, patchList *parser.PatchList) error {
+	// if a package module has been added, no more action is needed.
+	for _, patch := range *patchList {
+		if strings.Contains(patch.Replacement, "package {") {
+			return nil
+		}
+	}
+
+	licenseKindsPropertyName := "android_license_kinds"
+	licenseFilesPropertyName := "android_license_files"
+
+	androidBpFileErr := "// Error: No Android.bp file is found at\n" +
+		"// %s\n" +
+		"// Please add one there with the needed license module first.\n"
+	licenseModuleErr := "// Error: Cannot get the name of the license module in the\n" +
+		"// %s file.\n" +
+		"// If no such license module exists, please add one there first.\n"
+
+	defaultApplicableLicense := "Android-Apache-2.0"
+	var licenseModuleName, licensePatch string
+	var hasFileInParentDir bool
+
+	// when LOCAL_NOTICE_FILE is not empty
+	if hasNonEmptyLiteralListProperty(mod, licenseFilesPropertyName) {
+		hasFileInParentDir = hasValueStartWithTwoDotsLiteralList(mod, licenseFilesPropertyName)
+		// if have LOCAL_NOTICE_FILE outside the current directory, need to find and refer to the license
+		// module in the LOCAL_NOTICE_FILE location directly and no new license module needs to be created
+		if hasFileInParentDir {
+			bpPath, ok := getPathFromProperty(mod, licenseFilesPropertyName)
+			if !ok {
+				bpDir, err := getDirFromProperty(mod, licenseFilesPropertyName)
+				if err != nil {
+					return err
+				}
+				licensePatch += fmt.Sprintf(androidBpFileErr, bpDir)
+			} else {
+				licenseModuleName, _ = getModuleName(bpPath, "license")
+				if len(licenseModuleName) == 0 {
+					licensePatch += fmt.Sprintf(licenseModuleErr, bpPath)
+				}
+				defaultApplicableLicense = licenseModuleName
+			}
+		} else {
+			// if have LOCAL_NOTICE_FILE in the current directory, need to create a new license module
+			relativePath := getModuleRelativePath()
+			if len(relativePath) == 0 {
+				return fmt.Errorf("Cannot obtain the relative path of the Android.mk file")
+			}
+			licenseModuleName = strings.Replace(relativePath, "/", "_", -1) + "_license"
+			defaultApplicableLicense = licenseModuleName
+		}
+	}
+
+	//add the package module
+	if hasNonEmptyLiteralListProperty(mod, licenseKindsPropertyName) {
+		licensePatch += "package {\n" +
+			"    // See: http://go/android-license-faq\n" +
+			"    default_applicable_licenses: [\n" +
+			"         \"" + defaultApplicableLicense + "\",\n" +
+			"    ],\n" +
+			"}\n" +
+			"\n"
+	}
+
+	// append the license module when necessary
+	// when LOCAL_NOTICE_FILE is not empty and in the current directory, create a new license module
+	// otherwise, use the above default license directly
+	if hasNonEmptyLiteralListProperty(mod, licenseFilesPropertyName) && !hasFileInParentDir {
+		licenseKinds, err := mergeLiteralListPropertyValue(mod, licenseKindsPropertyName)
+		if err != nil {
+			return err
+		}
+		licenseFiles, err := mergeLiteralListPropertyValue(mod, licenseFilesPropertyName)
+		if err != nil {
+			return err
+		}
+		licensePatch += "license {\n" +
+			"    name: \"" + licenseModuleName + "\",\n" +
+			"    visibility: [\":__subpackages__\"],\n" +
+			"    license_kinds: [\n" +
+			licenseKinds +
+			"    ],\n" +
+			"    license_text: [\n" +
+			licenseFiles +
+			"    ],\n" +
+			"}\n" +
+			"\n"
+	}
+
+	// add to the patchList
+	pos := mod.Pos().Offset
+	err := patchList.Add(pos, pos, licensePatch)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+// merge the string vaules in a list property of a module into one string with expected format
+func mergeLiteralListPropertyValue(mod *parser.Module, property string) (s string, err error) {
+	listValue, ok := getLiteralListPropertyValue(mod, property)
+	if !ok {
+		// if do not find
+		return "", fmt.Errorf("Cannot retrieve the %s.%s field", mod.Type, property)
+	}
+	for i := 0; i < len(listValue); i++ {
+		s += "         \"" + listValue[i] + "\",\n"
+	}
+	return s, nil
+}
+
+// check whether a string list property has any value starting with `../`
+func hasValueStartWithTwoDotsLiteralList(mod *parser.Module, property string) bool {
+	listValue, ok := getLiteralListPropertyValue(mod, property)
+	if ok {
+		for i := 0; i < len(listValue); i++ {
+			if strings.HasPrefix(listValue[i], "../") {
+				return true
+			}
+		}
+	}
+	return false
+}
+
+// get the relative path from ANDROID_BUILD_TOP to the Android.mk file to be converted
+func getModuleRelativePath() string {
+	// get the absolute path of the top of the tree
+	rootPath := os.Getenv("ANDROID_BUILD_TOP")
+	// get the absolute path of the `Android.mk` file to be converted
+	absPath := getModuleAbsolutePath()
+	// get the relative path of the `Android.mk` file to top of the tree
+	relModulePath, err := filepath.Rel(rootPath, absPath)
+	if err != nil {
+		return ""
+	}
+	return relModulePath
+}
+
+// get the absolute path of the Android.mk file to be converted
+func getModuleAbsolutePath() string {
+	// get the absolute path at where the `androidmk` commend is executed
+	curAbsPath, err := filepath.Abs(".")
+	if err != nil {
+		return ""
+	}
+	// the argument for the `androidmk` command could be
+	// 1. "./a/b/c/Android.mk"; 2. "a/b/c/Android.mk"; 3. "Android.mk"
+	argPath := flag.Arg(0)
+	if strings.HasPrefix(argPath, "./") {
+		argPath = strings.TrimPrefix(argPath, ".")
+	}
+	argPath = strings.TrimSuffix(argPath, "Android.mk")
+	if strings.HasSuffix(argPath, "/") {
+		argPath = strings.TrimSuffix(argPath, "/")
+	}
+	if len(argPath) > 0 && !strings.HasPrefix(argPath, "/") {
+		argPath = "/" + argPath
+	}
+	// get the absolute path of the `Android.mk` file to be converted
+	absPath := curAbsPath + argPath
+	return absPath
+}
+
+// check whether a file exists in a directory
+func hasFile(dir string, fileName string) error {
+	_, err := os.Stat(dir + fileName)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+// get the directory where an `Android.bp` file and the property files are expected to locate
+func getDirFromProperty(mod *parser.Module, property string) (string, error) {
+	listValue, ok := getLiteralListPropertyValue(mod, property)
+	if !ok {
+		// if do not find
+		return "", fmt.Errorf("Cannot retrieve the %s.%s property", mod.Type, property)
+	}
+	if len(listValue) == 0 {
+		// if empty
+		return "", fmt.Errorf("Cannot find the value of the %s.%s property", mod.Type, property)
+	}
+	path := getModuleAbsolutePath()
+	for {
+		if !strings.HasPrefix(listValue[0], "../") {
+			break
+		}
+		path = filepath.Dir(path)
+		listValue[0] = strings.TrimPrefix(listValue[0], "../")
+	}
+	return path, nil
+}
+
+// get the path of the `Android.bp` file at the expected location where the property files locate
+func getPathFromProperty(mod *parser.Module, property string) (string, bool) {
+	dir, err := getDirFromProperty(mod, property)
+	if err != nil {
+		return "", false
+	}
+	err = hasFile(dir, "/Android.bp")
+	if err != nil {
+		return "", false
+	}
+	return dir + "/Android.bp", true
+}
+
+// parse an Android.bp file to get the name of the first module with type of moduleType
+func getModuleName(path string, moduleType string) (string, error) {
+	tree, err := parserPath(path)
+	if err != nil {
+		return "", err
+	}
+	for _, def := range tree.Defs {
+		mod, ok := def.(*parser.Module)
+		if !ok || mod.Type != moduleType {
+			continue
+		}
+		prop, ok := mod.GetProperty("name")
+		if !ok {
+			return "", fmt.Errorf("Cannot get the %s."+"name property", mod.Type)
+		}
+		propVal, ok := prop.Value.(*parser.String)
+		if ok {
+			return propVal.Value, nil
+		}
+	}
+	return "", fmt.Errorf("Cannot find the value of the %s."+"name property", moduleType)
+}
+
+// parse an Android.bp file with the specific path
+func parserPath(path string) (tree *parser.File, err error) {
+	fileContent, _ := os.ReadFile(path)
+	tree, err = parse(path, bytes.NewBufferString(string(fileContent)))
+	if err != nil {
+		return tree, err
+	}
+	return tree, nil
+}
+
+// remove the incorrect property that Soong does not support
+func removeIncorrectProperties(propName string) patchListModFunction {
+	return removeObsoleteProperty(propName)
+}
+
+// the modules on the same Android.mk file are expected to have the same license
+func haveSameLicense(f *Fixer) error {
+	androidLicenseProperties := []string{
+		"android_license_kinds",
+		"android_license_conditions",
+		"android_license_files",
+	}
+
+	var prevModuleName string
+	var prevLicenseKindsVals, prevLicenseConditionsVals, prevLicenseFilesVals []string
+	prevLicenseVals := [][]string{
+		prevLicenseKindsVals,
+		prevLicenseConditionsVals,
+		prevLicenseFilesVals,
+	}
+
+	for _, def := range f.tree.Defs {
+		mod, ok := def.(*parser.Module)
+		if !ok {
+			continue
+		}
+		for idx, property := range androidLicenseProperties {
+			curModuleName, ok := getLiteralStringPropertyValue(mod, "name")
+			// some modules in the existing test cases in the androidmk_test.go do not have name property
+			hasNameProperty := hasProperty(mod, "name")
+			if hasNameProperty && (!ok || len(curModuleName) == 0) {
+				return fmt.Errorf("Cannot retrieve the name property of a module of %s type.", mod.Type)
+			}
+			curVals, ok := getLiteralListPropertyValue(mod, property)
+			// some modules in the existing test cases in the androidmk_test.go do not have license-related property
+			hasLicenseProperty := hasProperty(mod, property)
+			if hasLicenseProperty && (!ok || len(curVals) == 0) {
+				// if do not find the property, or no value is found for the property
+				return fmt.Errorf("Cannot retrieve the %s.%s property", mod.Type, property)
+			}
+			if len(prevLicenseVals[idx]) > 0 {
+				if !reflect.DeepEqual(prevLicenseVals[idx], curVals) {
+					return fmt.Errorf("Modules %s and %s are expected to have the same %s property.",
+						prevModuleName, curModuleName, property)
+				}
+			}
+			sort.Strings(curVals)
+			prevLicenseVals[idx] = curVals
+			prevModuleName = curModuleName
+		}
+	}
+	return nil
+}
+
+func hasProperty(mod *parser.Module, propName string) bool {
+	_, ok := mod.GetProperty(propName)
+	return ok
+}
diff --git a/bpfix/bpfix/bpfix_test.go b/bpfix/bpfix/bpfix_test.go
index d8772c1..e6b6af5 100644
--- a/bpfix/bpfix/bpfix_test.go
+++ b/bpfix/bpfix/bpfix_test.go
@@ -125,34 +125,103 @@
 	implFilterListTest(t, []string{}, []string{}, []string{})
 }
 
-func runPass(t *testing.T, in, out string, innerTest func(*Fixer) error) {
-	expected, err := Reformat(out)
+func checkError(t *testing.T, in, expectedErr string, innerTest func(*Fixer) error) {
+	expected := preProcessOutErr(expectedErr)
+	runTestOnce(t, in, expected, innerTest)
+}
+
+func runTestOnce(t *testing.T, in, expected string, innerTest func(*Fixer) error) {
+	fixer, err := preProcessIn(in)
 	if err != nil {
 		t.Fatal(err)
 	}
 
+	out, err := runFixerOnce(fixer, innerTest)
+	if err != nil {
+		out = err.Error()
+	}
+
+	compareResult := compareOutExpected(in, out, expected)
+	if len(compareResult) > 0 {
+		t.Errorf(compareResult)
+	}
+}
+
+func preProcessOutErr(expectedErr string) string {
+	expected := strings.TrimSpace(expectedErr)
+	return expected
+}
+
+func preProcessOut(out string) (expected string, err error) {
+	expected, err = Reformat(out)
+	if err != nil {
+		return expected, err
+	}
+	return expected, nil
+}
+
+func preProcessIn(in string) (fixer *Fixer, err error) {
 	in, err = Reformat(in)
 	if err != nil {
-		t.Fatal(err)
+		return fixer, err
 	}
 
 	tree, errs := parser.Parse("<testcase>", bytes.NewBufferString(in), parser.NewScope(nil))
 	if errs != nil {
-		t.Fatal(errs)
+		return fixer, err
 	}
 
-	fixer := NewFixer(tree)
+	fixer = NewFixer(tree)
+
+	return fixer, nil
+}
+
+func runFixerOnce(fixer *Fixer, innerTest func(*Fixer) error) (string, error) {
+	err := innerTest(fixer)
+	if err != nil {
+		return "", err
+	}
+
+	out, err := parser.Print(fixer.tree)
+	if err != nil {
+		return "", err
+	}
+	return string(out), nil
+}
+
+func compareOutExpected(in, out, expected string) string {
+	if out != expected {
+		return fmt.Sprintf("output didn't match:\ninput:\n%s\n\nexpected:\n%s\ngot:\n%s\n",
+			in, expected, out)
+	}
+	return ""
+}
+
+func runPassOnce(t *testing.T, in, out string, innerTest func(*Fixer) error) {
+	expected, err := preProcessOut(out)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	runTestOnce(t, in, expected, innerTest)
+}
+
+func runPass(t *testing.T, in, out string, innerTest func(*Fixer) error) {
+	expected, err := preProcessOut(out)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	fixer, err := preProcessIn(in)
+	if err != nil {
+		t.Fatal(err)
+	}
 
 	got := ""
 	prev := "foo"
 	passes := 0
 	for got != prev && passes < 10 {
-		err := innerTest(fixer)
-		if err != nil {
-			t.Fatal(err)
-		}
-
-		out, err := parser.Print(fixer.tree)
+		out, err = runFixerOnce(fixer, innerTest)
 		if err != nil {
 			t.Fatal(err)
 		}
@@ -162,9 +231,9 @@
 		passes++
 	}
 
-	if got != expected {
-		t.Errorf("output didn't match:\ninput:\n%s\n\nexpected:\n%s\ngot:\n%s\n",
-			in, expected, got)
+	compareResult := compareOutExpected(in, out, expected)
+	if len(compareResult) > 0 {
+		t.Errorf(compareResult)
 	}
 }
 
@@ -1608,3 +1677,157 @@
 		})
 	}
 }
+
+func TestRewriteLicenseProperties(t *testing.T) {
+	tests := []struct {
+		name string
+		in   string
+		out  string
+	}{
+		{
+			name: "license rewriting with one module",
+			in: `
+				android_test {
+					name: "foo",
+					android_license_kinds: ["license_kind"],
+					android_license_conditions: ["license_notice"],
+				}
+			`,
+			out: `
+				package {
+					// See: http://go/android-license-faq
+					default_applicable_licenses: [
+						"Android-Apache-2.0",
+					],
+				}
+
+				android_test {
+					name: "foo",
+					android_license_kinds: ["license_kind"],
+					android_license_conditions: ["license_notice"],
+				}
+			`,
+		},
+		{
+			name: "license rewriting with two modules",
+			in: `
+				android_test {
+					name: "foo1",
+					android_license_kinds: ["license_kind1"],
+					android_license_conditions: ["license_notice1"],
+				}
+
+				android_test {
+					name: "foo2",
+					android_license_kinds: ["license_kind2"],
+					android_license_conditions: ["license_notice2"],
+				}
+			`,
+			out: `
+				package {
+					// See: http://go/android-license-faq
+					default_applicable_licenses: [
+						"Android-Apache-2.0",
+					],
+				}
+
+				android_test {
+					name: "foo1",
+					android_license_kinds: ["license_kind1"],
+					android_license_conditions: ["license_notice1"],
+				}
+
+				android_test {
+					name: "foo2",
+					android_license_kinds: ["license_kind2"],
+					android_license_conditions: ["license_notice2"],
+				}
+			`,
+		},
+		// TODO(b/205615944): When valid "android_license_files" exists, the test requires an Android.mk
+		// file (and an Android.bp file is required as well if the license files locates outside the current
+		// directory). So plan to use a mock file system to mock the Android.mk and Android.bp files.
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			runPassOnce(t, test.in, test.out, runPatchListMod(rewriteLicenseProperties))
+		})
+	}
+}
+
+func TestHaveSameLicense(t *testing.T) {
+	tests := []struct {
+		name string
+		in   string
+		out  string
+	}{
+		{
+			name: "two modules with the same license",
+			in: `
+				android_test {
+					name: "foo1",
+					android_license_kinds: ["license_kind"],
+					android_license_conditions: ["license_notice"],
+				}
+
+				android_test {
+					name: "foo2",
+					android_license_kinds: ["license_kind"],
+					android_license_conditions: ["license_notice"],
+				}
+			`,
+			out: `
+				android_test {
+					name: "foo1",
+					android_license_kinds: ["license_kind"],
+					android_license_conditions: ["license_notice"],
+				}
+
+				android_test {
+					name: "foo2",
+					android_license_kinds: ["license_kind"],
+					android_license_conditions: ["license_notice"],
+				}
+			`,
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			runPassOnce(t, test.in, test.out, func(fixer *Fixer) error {
+				return haveSameLicense(fixer)
+			})
+		})
+	}
+	testErrs := []struct {
+		name        string
+		in          string
+		expectedErr string
+	}{
+		{
+			name: "two modules will different licenses",
+			in: `
+				android_test {
+					name: "foo1",
+					android_license_kinds: ["license_kind1"],
+					android_license_conditions: ["license_notice1"],
+				}
+
+				android_test {
+					name: "foo2",
+					android_license_kinds: ["license_kind2"],
+					android_license_conditions: ["license_notice2"],
+				}
+			`,
+			expectedErr: `
+				Modules foo1 and foo2 are expected to have the same android_license_kinds property.
+			`,
+		},
+	}
+	for _, test := range testErrs {
+		t.Run(test.name, func(t *testing.T) {
+			checkError(t, test.in, test.expectedErr, func(fixer *Fixer) error {
+				return haveSameLicense(fixer)
+			})
+		})
+	}
+}