Update add3prf.py

Change the license detection logic to return if a dual-license was
found. Update the metadata comment as discussed in b/179902335.
Add unit tests for add_module_license and decide_license_type.

Bug: 179902335
Test: atest --host add3prf_test; manually run add3prf.py in {ahash,
  libloading, parking_lot, ring}
Change-Id: I3184b5cf0955d124170b9e5987308507bdef6e9a
diff --git a/scripts/add3prf.py b/scripts/add3prf.py
index bc23306..5fd155f 100755
--- a/scripts/add3prf.py
+++ b/scripts/add3prf.py
@@ -15,7 +15,9 @@
 # limitations under the License.
 """Add files to a Rust package for third party review."""
 
+import collections
 import datetime
+import enum
 import glob
 import json
 import os
@@ -47,6 +49,8 @@
 MIT_MATCHER = re.compile(MIT_PATTERN)
 BSD_PATTERN = r"^.*BSD .*License.*$"
 BSD_MATCHER = re.compile(BSD_PATTERN)
+MULTI_LICENSE_COMMENT = ("# Dual-licensed, using the least restrictive "
+        "per go/thirdpartylicenses#same.\n  ")
 
 # default owners added to OWNERS
 DEFAULT_OWNERS = "include platform/prebuilts/rust:/OWNERS\n"
@@ -55,23 +59,23 @@
 # "license_type: NOTICE" might be optional,
 # but it is already used in most rust crate METADATA.
 # This line format should match the output of external_updater.
-METADATA_CONTENT = """name: "{}"
-description: {}
+METADATA_CONTENT = """name: "{name}"
+description: {description}
 third_party {{
   url {{
     type: HOMEPAGE
-    value: "https://crates.io/crates/{}"
+    value: "https://crates.io/crates/{name}"
   }}
   url {{
     type: ARCHIVE
-    value: "https://static.crates.io/crates/{}/{}-{}.crate"
+    value: "https://static.crates.io/crates/{name}/{name}-{version}.crate"
   }}
-  version: "{}"
-  license_type: NOTICE
+  version: "{version}"
+  {license_comment}license_type: NOTICE
   last_upgrade_date {{
-    year: {}
-    month: {}
-    day: {}
+    year: {year}
+    month: {month}
+    day: {day}
   }}
 }}
 """
@@ -104,17 +108,20 @@
   return today.year, today.month, today.day
 
 
-def add_metadata(name, version, description):
+def add_metadata(name, version, description, multi_license):
   """Update or add METADATA file."""
   if os.path.exists("METADATA"):
     print("### Updating METADATA")
   else:
     print("### Adding METADATA")
   year, month, day = get_metadata_date()
+  license_comment = ""
+  if multi_license:
+    license_comment = MULTI_LICENSE_COMMENT
   with open("METADATA", "w") as outf:
     outf.write(METADATA_CONTENT.format(
-        name, description, name, name, name,
-        version, version, year, month, day))
+        name=name, description=description, version=version,
+        license_comment=license_comment, year=year, month=month, day=day))
 
 
 def grep_license_keyword(license_file):
@@ -122,48 +129,63 @@
   with open(license_file, "r") as input_file:
     for line in input_file:
       if APACHE_MATCHER.match(line):
-        return "APACHE2", license_file
+        return License(LicenseType.APACHE2, license_file)
       if MIT_MATCHER.match(line):
-        return "MIT", license_file
+        return License(LicenseType.MIT, license_file)
       if BSD_MATCHER.match(line):
-        return "BSD_LIKE", license_file
+        return License(LicenseType.BSD_LIKE, license_file)
   print("ERROR: cannot decide license type in", license_file,
-        " assume BSD_LIKE")
-  return "BSD_LIKE", license_file
+        "assume BSD_LIKE")
+  return License(LicenseType.BSD_LIKE, license_file)
+
+
+class LicenseType(enum.IntEnum):
+  """A type of license.
+
+  An IntEnum is used to be able to sort by preference. This is mainly the case
+  for dual-licensed Apache/MIT code, for which we prefer the Apache license.
+  The enum name is used to generate the corresponding MODULE_LICENSE_* file.
+  """
+  APACHE2 = 1
+  MIT = 2
+  BSD_LIKE = 3
+  ISC = 4
+
+
+License = collections.namedtuple('License', ['type', 'filename'])
 
 
 def decide_license_type(cargo_license):
-  """Check LICENSE* files to determine the license type."""
+  """Check LICENSE* files to determine the license type.
+
+  Returns: A list of Licenses. The first element is the license we prefer.
+  """
   # Most crates.io packages have both APACHE and MIT.
   # Some crate like time-macros-impl uses lower case names like LICENSE-Apache.
-  targets = {}
-  license_file = "unknown-file"
-  for license_file in glob.glob("./LICENSE*"):
-    license_file = license_file[2:]
+  licenses = []
+  license_file = None
+  for license_file in glob.glob("LICENSE*"):
     lowered_name = license_file.lower()
     if lowered_name == "license-apache":
-      targets["APACHE2"] = license_file
+      licenses.append(License(LicenseType.APACHE2, license_file))
     elif lowered_name == "license-mit":
-      targets["MIT"] = license_file
-  # Prefer APACHE2 over MIT license type.
-  for license_type in ["APACHE2", "MIT"]:
-    if license_type in targets:
-      return license_type, targets[license_type]
-  # Use cargo_license found in Cargo.toml.
+      licenses.append(License(LicenseType.MIT, license_file))
+  if licenses:
+    licenses.sort(key=lambda l: l.type)
+    return licenses
+  if not license_file:
+    raise FileNotFoundError("No license file has been found.")
+  # There is a LICENSE or LICENSE.txt file, use cargo_license found in
+  # Cargo.toml.
   if "Apache" in cargo_license:
-    return "APACHE2", license_file
+    return [License(LicenseType.APACHE2, license_file)]
   if "MIT" in cargo_license:
-    return "MIT", license_file
+    return [License(LicenseType.MIT, license_file)]
   if "BSD" in cargo_license:
-    return "BSD_LIKE", license_file
+    return [License(LicenseType.BSD_LIKE, license_file)]
   if "ISC" in cargo_license:
-    return "ISC", license_file
-  # Try to find key words in LICENSE* files.
-  for license_file in ["LICENSE", "LICENSE.txt"]:
-    if os.path.exists(license_file):
-      return grep_license_keyword(license_file)
-  print("ERROR: missing LICENSE-{APACHE,MIT}; assume BSD_LIKE")
-  return "BSD_LIKE", "unknown-file"
+    return [License(LicenseType.ISC, license_file)]
+  return [grep_license_keyword(license_file)]
 
 
 def add_notice():
@@ -192,7 +214,7 @@
     if os.path.islink("LICENSE"):
       check_license_link(target)
     else:
-      print("NOTE: found LICENSE and it is not a link!")
+      print("NOTE: found LICENSE and it is not a link.")
     return
   print("### Creating LICENSE link to", target)
   os.symlink(target, "LICENSE")
@@ -204,10 +226,10 @@
   for suffix in ["MIT", "APACHE", "APACHE2", "BSD_LIKE"]:
     module_file = "MODULE_LICENSE_" + suffix
     if os.path.exists(module_file):
-      if license_type != suffix:
-        print("ERROR: found unexpected", module_file)
+      if license_type.name != suffix:
+        raise Exception("Found unexpected license " + module_file)
       return
-  module_file = "MODULE_LICENSE_" + license_type
+  module_file = "MODULE_LICENSE_" + license_type.name.upper()
   pathlib.Path(module_file).touch()
   print("### Touched", module_file)
 
@@ -285,11 +307,12 @@
     print("ERROR: Cannot find name, version, or description in", cargo)
     return
   print("### Cargo.toml license:", cargo_license)
-  add_metadata(name, version, description)
+  licenses = decide_license_type(cargo_license)
+  preferred_license = licenses[0]
+  add_metadata(name, version, description, len(licenses) > 1)
   add_owners()
-  license_type, file_name = decide_license_type(cargo_license)
-  add_license(file_name)
-  add_module_license(license_type)
+  add_license(preferred_license.filename)
+  add_module_license(preferred_license.type)
   # It is unclear yet if a NOTICE file is required.
   # add_notice()