Merge "current.txt: remove libadf since it is no longer supported"
diff --git a/core/config_sanitizers.mk b/core/config_sanitizers.mk
index c92cea2..f39b84a 100644
--- a/core/config_sanitizers.mk
+++ b/core/config_sanitizers.mk
@@ -53,6 +53,18 @@
   endif
 endif
 
+# Disable global memtag_heap in excluded paths
+ifneq ($(filter memtag_heap, $(my_global_sanitize)),)
+  combined_exclude_paths := $(MEMTAG_HEAP_EXCLUDE_PATHS) \
+                            $(PRODUCT_MEMTAG_HEAP_EXCLUDE_PATHS)
+
+  ifneq ($(strip $(foreach dir,$(subst $(comma),$(space),$(combined_exclude_paths)),\
+         $(filter $(dir)%,$(LOCAL_PATH)))),)
+    my_global_sanitize := $(filter-out memtag_heap,$(my_global_sanitize))
+    my_global_sanitize_diag := $(filter-out memtag_heap,$(my_global_sanitize_diag))
+  endif
+endif
+
 ifneq ($(my_global_sanitize),)
   my_sanitize := $(my_global_sanitize) $(my_sanitize)
 endif
@@ -116,6 +128,25 @@
   endif
 endif
 
+# Enable memtag_heap in included paths (for Arm64 only).
+ifeq ($(filter memtag_heap, $(my_sanitize)),)
+  ifneq ($(filter arm64,$(TARGET_$(LOCAL_2ND_ARCH_VAR_PREFIX)ARCH)),)
+    combined_sync_include_paths := $(MEMTAG_HEAP_SYNC_INCLUDE_PATHS) \
+                                   $(PRODUCT_MEMTAG_HEAP_SYNC_INCLUDE_PATHS)
+    combined_async_include_paths := $(MEMTAG_HEAP_ASYNC_INCLUDE_PATHS) \
+                                    $(PRODUCT_MEMTAG_HEAP_ASYNC_INCLUDE_PATHS)
+
+    ifneq ($(strip $(foreach dir,$(subst $(comma),$(space),$(combined_sync_include_paths)),\
+           $(filter $(dir)%,$(LOCAL_PATH)))),)
+      my_sanitize := memtag_heap $(my_sanitize)
+      my_sanitize_diag := memtag_heap $(my_sanitize)
+    else ifneq ($(strip $(foreach dir,$(subst $(comma),$(space),$(combined_async_include_paths)),\
+           $(filter $(dir)%,$(LOCAL_PATH)))),)
+      my_sanitize := memtag_heap $(my_sanitize)
+    endif
+  endif
+endif
+
 # If CFI is disabled globally, remove it from my_sanitize.
 ifeq ($(strip $(ENABLE_CFI)),false)
   my_sanitize := $(filter-out cfi,$(my_sanitize))
@@ -164,6 +195,7 @@
 
 ifneq ($(filter arm x86 x86_64,$(TARGET_$(LOCAL_2ND_ARCH_VAR_PREFIX)ARCH)),)
   my_sanitize := $(filter-out hwaddress,$(my_sanitize))
+  my_sanitize := $(filter-out memtag_heap,$(my_sanitize))
 endif
 
 ifneq ($(filter hwaddress,$(my_sanitize)),)
@@ -183,6 +215,20 @@
   endif
 endif
 
+ifneq ($(filter memtag_heap,$(my_sanitize)),)
+  # Add memtag ELF note.
+  ifneq ($(filter memtag_heap,$(my_sanitize_diag)),)
+    my_whole_static_libraries += note_memtag_heap_sync
+  else
+    my_whole_static_libraries += note_memtag_heap_async
+  endif
+  # This is all that memtag_heap does - it is not an actual -fsanitize argument.
+  # Remove it from the list.
+  my_sanitize := $(filter-out memtag_heap,$(my_sanitize))
+endif
+
+my_sanitize_diag := $(filter-out memtag_heap,$(my_sanitize_diag))
+
 # TSAN is not supported on 32-bit architectures. For non-multilib cases, make
 # its use an error. For multilib cases, don't use it for the 32-bit case.
 ifneq ($(filter thread,$(my_sanitize)),)
diff --git a/target/product/gsi/current.txt b/target/product/gsi/current.txt
index 1c25355..a4cf1d2 100644
--- a/target/product/gsi/current.txt
+++ b/target/product/gsi/current.txt
@@ -86,6 +86,7 @@
 VNDK-core: android.hardware.soundtrigger@2.0-core.so
 VNDK-core: android.hardware.soundtrigger@2.0.so
 VNDK-core: android.hardware.vibrator-V1-ndk_platform.so
+VNDK-core: android.hardware.weaver-V1-ndk_platform.so
 VNDK-core: android.hidl.token@1.0-utils.so
 VNDK-core: android.hidl.token@1.0.so
 VNDK-core: android.system.keystore2-V1-ndk_platform.so
diff --git a/tools/product_config/Android.bp b/tools/product_config/Android.bp
new file mode 100644
index 0000000..287ed5a
--- /dev/null
+++ b/tools/product_config/Android.bp
@@ -0,0 +1,23 @@
+java_defaults {
+    name: "product-config-defaults",
+    srcs: ["src/**/*.java"],
+}
+
+java_binary_host {
+    name: "product-config",
+    defaults: ["product-config-defaults"],
+    manifest: "MANIFEST.MF"
+}
+
+java_test_host {
+    name: "product-config-test",
+    defaults: ["product-config-defaults"],
+    srcs: [
+        "test/**/*.java",
+    ],
+    static_libs: [
+        "junit"
+    ],
+    test_suites: ["general-tests"]
+}
+
diff --git a/tools/product_config/MANIFEST.MF b/tools/product_config/MANIFEST.MF
new file mode 100644
index 0000000..db88df3
--- /dev/null
+++ b/tools/product_config/MANIFEST.MF
@@ -0,0 +1,2 @@
+Manifest-Version: 1.0
+Main-Class: com.android.build.config.Main
diff --git a/tools/product_config/TEST_MAPPING b/tools/product_config/TEST_MAPPING
new file mode 100644
index 0000000..d3568f1
--- /dev/null
+++ b/tools/product_config/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "product_config_test"
+    }
+  ]
+}
diff --git a/tools/product_config/src/com/android/build/config/ErrorReporter.java b/tools/product_config/src/com/android/build/config/ErrorReporter.java
new file mode 100644
index 0000000..f382b4e
--- /dev/null
+++ b/tools/product_config/src/com/android/build/config/ErrorReporter.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.build.config;
+
+import java.lang.reflect.Field;
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Base class for reporting errors.
+ */
+public class ErrorReporter {
+    /**
+     * List of Entries that have occurred.
+     */
+    // Also used as the lock for this object.
+    private final ArrayList<Entry> mEntries = new ArrayList();
+
+    /**
+     * The categories that are for this Errors object.
+     */
+    private Map<Integer, Category> mCategories;
+
+    /**
+     * Whether there has been a warning or an error yet.
+     */
+    private boolean mHadWarningOrError;
+
+    /**
+     * Whether there has been an error yet.
+     */
+    private boolean mHadError;
+
+    /**
+     * Whether errors are errors, warnings or hidden.
+     */
+    public static enum Level {
+        HIDDEN("hidden"),
+        WARNING("warning"),
+        ERROR("error");
+
+        private final String mLabel;
+
+        Level(String label) {
+            mLabel = label;
+        }
+
+        String getLabel() {
+            return mLabel;
+        }
+    }
+
+    /**
+     * The available error codes.
+     */
+    public class Category {
+        private final int mCode;
+        private boolean mIsLevelSettable;
+        private Level mLevel;
+        private String mHelp;
+
+        /**
+         * Construct a Category object.
+         */
+        public Category(int code, boolean isLevelSettable, Level level, String help) {
+            if (!isLevelSettable && level != Level.ERROR) {
+                throw new RuntimeException("Don't have WARNING or HIDDEN without isLevelSettable");
+            }
+            mCode = code;
+            mIsLevelSettable = isLevelSettable;
+            mLevel = level;
+            mHelp = help;
+        }
+
+        /**
+         * Get the numeric code for the Category, which can be used to set the level.
+         */
+        public int getCode() {
+            return mCode;
+        }
+
+        /**
+         * Get whether the level of this Category can be changed.
+         */
+        public boolean isLevelSettable() {
+            return mIsLevelSettable;
+        }
+
+        /**
+         * Set the level of this category.
+         */
+        public void setLevel(Level level) {
+            if (!mIsLevelSettable) {
+                throw new RuntimeException("Can't set level for error " + mCode);
+            }
+            mLevel = level;
+        }
+
+        /**
+         * Return the level, including any overrides.
+         */
+        public Level getLevel() {
+            return mLevel;
+        }
+
+        /**
+         * Return the category's help text.
+         */
+        public String getHelp() {
+            return mHelp;
+        }
+    }
+
+    /**
+     * An instance of an error happening.
+     */
+    public class Entry {
+        private final Category mCategory;
+        private final Position mPosition;
+        private final String mMessage;
+
+        Entry(Category category, Position position, String message) {
+            mCategory = category;
+            mPosition = position;
+            mMessage = message;
+        }
+
+        public Category getCategory() {
+            return mCategory;
+        }
+
+        public Position getPosition() {
+            return mPosition;
+        }
+
+        public String getMessage() {
+            return mMessage;
+        }
+    }
+
+    private void initLocked() {
+        if (mCategories == null) {
+            HashMap<Integer, Category> categories = new HashMap();
+            for (Field field: getClass().getFields()) {
+                if (Category.class.isAssignableFrom(field.getType())) {
+                    Category category = null;
+                    try {
+                        category = (Category)field.get(this);
+                    } catch (IllegalAccessException ex) {
+                        // Wrap and rethrow, this is always on this class, so it's
+                        // our programming error if this happens.
+                        throw new RuntimeException("Categories on Errors should be public.", ex);
+                    }
+                    Category prev = categories.put(category.getCode(), category);
+                    if (prev != null) {
+                        throw new RuntimeException("Duplicate categories with code "
+                                + category.getCode());
+                    }
+                }
+            }
+            mCategories = Collections.unmodifiableMap(categories);
+        }
+    }
+
+    /**
+     * Returns a map of the category codes to the categories.
+     */
+    public Map<Integer, Category> getCategories() {
+        synchronized (mEntries) {
+            initLocked();
+            return mCategories;
+        }
+    }
+
+    /**
+     * Add an error with no source position.
+     */
+    public void add(Category category, String message) {
+        add(category, new Position(), message);
+    }
+
+    /**
+     * Add an error.
+     */
+    public void add(Category category, Position pos, String message) {
+        synchronized (mEntries) {
+            initLocked();
+            if (mCategories.get(category.getCode()) != category) {
+                throw new RuntimeException("Errors.Category used from the wrong Errors object.");
+            }
+            mEntries.add(new Entry(category, pos, message));
+            final Level level = category.getLevel();
+            if (level == Level.WARNING || level == Level.ERROR) {
+                mHadWarningOrError = true;
+            }
+            if (level == Level.ERROR) {
+                mHadError = true;
+            }
+        }
+    }
+
+    /**
+     * Returns whether there has been a warning or an error yet.
+     */
+    public boolean hadWarningOrError() {
+        synchronized (mEntries) {
+            return mHadWarningOrError;
+        }
+    }
+
+    /**
+     * Returns whether there has been an error yet.
+     */
+    public boolean hadError() {
+        synchronized (mEntries) {
+            return mHadError;
+        }
+    }
+
+    /**
+     * Returns a list of all entries that were added.
+     */
+    public List<Entry> getEntries() {
+        synchronized (mEntries) {
+            return new ArrayList<Entry>(mEntries);
+        }
+    }
+
+    /**
+     * Prints the errors.
+     */
+    public void printErrors(PrintStream out) {
+        synchronized (mEntries) {
+            for (Entry entry: mEntries) {
+                final Category category = entry.getCategory();
+                final Level level = category.getLevel();
+                if (level == Level.HIDDEN) {
+                    continue;
+                }
+                out.println(entry.getPosition() + "[" + level.getLabel() + " "
+                        + category.getCode() + "] " + entry.getMessage());
+            }
+        }
+    }
+}
diff --git a/tools/product_config/src/com/android/build/config/Errors.java b/tools/product_config/src/com/android/build/config/Errors.java
new file mode 100644
index 0000000..63792c8
--- /dev/null
+++ b/tools/product_config/src/com/android/build/config/Errors.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.build.config;
+
+import java.lang.reflect.Field;
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Error constants and error reporting.
+ * <p>
+ * <b>Naming Convention:</b>
+ * <ul>
+ *  <li>ERROR_ for Categories with isLevelSettable false and Level.ERROR
+ *  <li>WARNING_ for Categories with isLevelSettable false and default WARNING or HIDDEN
+ *  <li>Don't have isLevelSettable true and not ERROR. (The constructor asserts this).
+ * </ul>
+ */
+public class Errors extends ErrorReporter {
+
+    public final Category ERROR_COMMAND_LINE = new Category(1, false, Level.ERROR,
+            "Error on the command line.");
+
+    public final Category WARNING_UNKNOWN_COMMAND_LINE_ERROR = new Category(2, true, Level.HIDDEN,
+            "Passing unknown errors on the command line.  Hidden by default for\n"
+            + "forward compatibility.");
+}
diff --git a/tools/product_config/src/com/android/build/config/Main.java b/tools/product_config/src/com/android/build/config/Main.java
new file mode 100644
index 0000000..7669742
--- /dev/null
+++ b/tools/product_config/src/com/android/build/config/Main.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.build.config;
+
+public class Main {
+    private final Errors mErrors;
+    private final Options mOptions;
+
+    public Main(Errors errors, Options options) {
+        mErrors = errors;
+        mOptions = options;
+    }
+
+    void run() {
+        System.out.println("Hello World");
+
+        // TODO: Check the build environment to make sure we're running in a real
+        // build environment, e.g. actually inside a source tree, with TARGET_PRODUCT
+        // and TARGET_BUILD_VARIANT defined, etc.
+
+        // TODO: Run kati and extract the variables and convert all that into starlark files.
+
+        // TODO: Run starlark with all the generated ones and the hand written ones.
+
+        // TODO: Get the variables that were defined in starlark and use that to write
+        // out the make, soong and bazel input files.
+    }
+
+    public static void main(String[] args) {
+        Errors errors = new Errors();
+
+        Options options = Options.parse(errors, args);
+        if (errors.hadError()) {
+            Options.printHelp(System.err);
+            System.err.println();
+            errors.printErrors(System.err);
+            System.exit(1);
+        }
+
+        switch (options.getAction()) {
+            case DEFAULT:
+                (new Main(errors, options)).run();
+                errors.printErrors(System.err);
+                return;
+            case HELP:
+                Options.printHelp(System.out);
+                return;
+        }
+    }
+}
diff --git a/tools/product_config/src/com/android/build/config/Options.java b/tools/product_config/src/com/android/build/config/Options.java
new file mode 100644
index 0000000..494b947
--- /dev/null
+++ b/tools/product_config/src/com/android/build/config/Options.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.build.config;
+
+import java.io.PrintStream;
+import java.util.TreeMap;
+
+public class Options {
+    public enum Action {
+        DEFAULT,
+        HELP
+    }
+
+    private Action mAction = Action.DEFAULT;
+
+    public Action getAction() {
+        return mAction;
+    }
+
+    public static void printHelp(PrintStream out) {
+        out.println("usage: product_config");
+        out.println();
+        out.println("OPTIONS");
+        out.println("  --hide ERROR_ID          Suppress this error.");
+        out.println("  --error ERROR_ID         Make this ERROR_ID a fatal error.");
+        out.println("  --help -h                This message.");
+        out.println("  --warning ERROR_ID       Make this ERROR_ID a warning.");
+        out.println();
+        out.println("ERRORS");
+        out.println("  The following are the errors that can be controlled on the");
+        out.println("  commandline with the --hide --warning --error flags.");
+
+        TreeMap<Integer,Errors.Category> sorted = new TreeMap((new Errors()).getCategories());
+
+        for (final Errors.Category category: sorted.values()) {
+            if (category.isLevelSettable()) {
+                out.println(String.format("    %-3d      %s", category.getCode(),
+                category.getHelp().replace("\n", "\n             ")));
+            }
+        }
+    }
+
+    static class Parser {
+        private class ParseException extends Exception {
+            public ParseException(String message) {
+                super(message);
+            }
+        }
+
+        private Errors mErrors;
+        private String[] mArgs;
+        private Options mResult = new Options();
+        private int mIndex;
+
+        public Parser(Errors errors, String[] args) {
+            mErrors = errors;
+            mArgs = args;
+        }
+
+        public Options parse() {
+            try {
+                while (mIndex < mArgs.length) {
+                    final String arg = mArgs[mIndex];
+
+                    if ("--hide".equals(arg)) {
+                        handleErrorCode(arg, Errors.Level.HIDDEN);
+                    } else if ("--error".equals(arg)) {
+                        handleErrorCode(arg, Errors.Level.ERROR);
+                    } else if ("--help".equals(arg) || "-h".equals(arg)) {
+                        // Help overrides all other commands if there isn't an error, but
+                        // we will stop here.
+                        if (!mErrors.hadError()) {
+                            mResult.mAction = Action.HELP;
+                        }
+                        return mResult;
+                    } else if ("--warning".equals(arg)) {
+                        handleErrorCode(arg, Errors.Level.WARNING);
+                    } else {
+                        throw new ParseException("Unknown command line argument: " + arg);
+                    }
+
+                    mIndex++;
+                }
+            } catch (ParseException ex) {
+                mErrors.add(mErrors.ERROR_COMMAND_LINE, ex.getMessage());
+            }
+
+            return mResult;
+        }
+
+        private void addWarning(Errors.Category category, String message) {
+            mErrors.add(category, message);
+        }
+
+        private String getNextNonFlagArg() {
+            if (mIndex == mArgs.length - 1) {
+                return null;
+            }
+            if (mArgs[mIndex + 1].startsWith("-")) {
+                return null;
+            }
+            mIndex++;
+            return mArgs[mIndex];
+        }
+
+        private int requireNextNumberArg(String arg) throws ParseException {
+            final String val = getNextNonFlagArg();
+            if (val == null) {
+                throw new ParseException(arg + " requires a numeric argument.");
+            }
+            try {
+                return Integer.parseInt(val);
+            } catch (NumberFormatException ex) {
+                throw new ParseException(arg + " requires a numeric argument. found: " + val);
+            }
+        }
+
+        private void handleErrorCode(String arg, Errors.Level level) throws ParseException {
+            final int code = requireNextNumberArg(arg);
+            final Errors.Category category = mErrors.getCategories().get(code);
+            if (category == null) {
+                mErrors.add(mErrors.WARNING_UNKNOWN_COMMAND_LINE_ERROR,
+                        "Unknown error code: " + code);
+                return;
+            }
+            if (!category.isLevelSettable()) {
+                mErrors.add(mErrors.ERROR_COMMAND_LINE, "Can't set level for error " + code);
+                return;
+            }
+            category.setLevel(level);
+        }
+    }
+
+    /**
+     * Parse the arguments and return an options object.
+     * <p>
+     * Updates errors with the hidden / warning / error levels.
+     * <p>
+     * Adds errors encountered to Errors object.
+     */
+    public static Options parse(Errors errors, String[] args) {
+        return (new Parser(errors, args)).parse();
+    }
+}
diff --git a/tools/product_config/src/com/android/build/config/Position.java b/tools/product_config/src/com/android/build/config/Position.java
new file mode 100644
index 0000000..7953942
--- /dev/null
+++ b/tools/product_config/src/com/android/build/config/Position.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.build.config;
+
+/**
+ * Position in a source file.
+ */
+public class Position implements Comparable<Position> {
+    /**
+     * Sentinel line number for when there is no known line number.
+     */
+    public static final int NO_LINE = -1;
+
+    private final String mFile;
+    private final int mLine;
+
+    public Position() {
+        mFile = null;
+        mLine = NO_LINE;
+    }
+
+    public Position(String file) {
+        mFile = file;
+        mLine = NO_LINE;
+    }
+
+    public Position(String file, int line) {
+        if (line < NO_LINE) {
+            throw new IllegalArgumentException("Negative line number. file=" + file
+                    + " line=" + line);
+        }
+        mFile = file;
+        mLine = line;
+    }
+
+    public int compareTo(Position that) {
+        int result = mFile.compareTo(that.mFile);
+        if (result != 0) {
+            return result;
+        }
+        return mLine - that.mLine;
+    }
+
+    public String getFile() {
+        return mFile;
+    }
+
+    public int getLine() {
+        return mLine;
+    }
+
+    @Override
+    public String toString() {
+      if (mFile == null && mLine == NO_LINE) {
+        return "";
+      } else if (mFile == null && mLine != NO_LINE) {
+        return "<unknown>:" + mLine + ": ";
+      } else if (mFile != null && mLine == NO_LINE) {
+        return mFile + ": ";
+      } else { // if (mFile != null && mLine != NO_LINE)
+        return mFile + ':' + mLine + ": ";
+      }
+    }
+}
diff --git a/tools/product_config/test/com/android/build/config/ErrorReporterTest.java b/tools/product_config/test/com/android/build/config/ErrorReporterTest.java
new file mode 100644
index 0000000..2cde476
--- /dev/null
+++ b/tools/product_config/test/com/android/build/config/ErrorReporterTest.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.build.config;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.HashSet;
+import java.util.List;
+
+public class ErrorReporterTest {
+    /**
+     * Test that errors can be recorded and retrieved.
+     */
+    @Test
+    public void testAdding() {
+        TestErrors errors = new TestErrors();
+
+        errors.add(errors.ERROR, new Position("a", 12), "Errrororrrr");
+
+        Assert.assertTrue(errors.hadWarningOrError());
+        Assert.assertTrue(errors.hadError());
+
+        List<TestErrors.Entry> entries = errors.getEntries();
+        Assert.assertEquals(1, entries.size());
+
+        TestErrors.Entry entry = entries.get(0);
+        Assert.assertEquals(errors.ERROR, entry.getCategory());
+        Assert.assertEquals("a", entry.getPosition().getFile());
+        Assert.assertEquals(12, entry.getPosition().getLine());
+        Assert.assertEquals("Errrororrrr", entry.getMessage());
+
+        Assert.assertNotEquals("", errors.getErrorMessages());
+    }
+
+    /**
+     * Test that not adding an error doesn't record errors.
+     */
+    @Test
+    public void testNoError() {
+        TestErrors errors = new TestErrors();
+
+        Assert.assertFalse(errors.hadWarningOrError());
+        Assert.assertFalse(errors.hadError());
+        Assert.assertEquals("", errors.getErrorMessages());
+    }
+
+    /**
+     * Test that not adding a warning doesn't record errors.
+     */
+    @Test
+    public void testWarning() {
+        TestErrors errors = new TestErrors();
+
+        errors.add(errors.WARNING, "Waaaaarninggggg");
+
+        Assert.assertTrue(errors.hadWarningOrError());
+        Assert.assertFalse(errors.hadError());
+        Assert.assertNotEquals("", errors.getErrorMessages());
+    }
+
+    /**
+     * Test that hidden warnings don't report.
+     */
+    @Test
+    public void testHidden() {
+        TestErrors errors = new TestErrors();
+
+        errors.add(errors.HIDDEN, "Hidddeennn");
+
+        Assert.assertFalse(errors.hadWarningOrError());
+        Assert.assertFalse(errors.hadError());
+        Assert.assertEquals("", errors.getErrorMessages());
+    }
+
+    /**
+     * Test changing an error level.
+     */
+    @Test
+    public void testSetLevel() {
+        TestErrors errors = new TestErrors();
+        Assert.assertEquals(TestErrors.Level.ERROR, errors.ERROR.getLevel());
+
+        errors.ERROR.setLevel(TestErrors.Level.WARNING);
+
+        Assert.assertEquals(TestErrors.Level.WARNING, errors.ERROR.getLevel());
+    }
+
+    /**
+     * Test that changing a fixed error fails.
+     */
+    @Test
+    public void testSetLevelFails() {
+        TestErrors errors = new TestErrors();
+        Assert.assertEquals(TestErrors.Level.ERROR, errors.ERROR_FIXED.getLevel());
+
+        boolean exceptionThrown = false;
+        try {
+            errors.ERROR_FIXED.setLevel(TestErrors.Level.WARNING);
+        } catch (RuntimeException ex) {
+            exceptionThrown = true;
+        }
+
+        Assert.assertTrue(exceptionThrown);
+        Assert.assertEquals(TestErrors.Level.ERROR, errors.ERROR_FIXED.getLevel());
+    }
+}
diff --git a/tools/product_config/test/com/android/build/config/OptionsTest.java b/tools/product_config/test/com/android/build/config/OptionsTest.java
new file mode 100644
index 0000000..2c36322
--- /dev/null
+++ b/tools/product_config/test/com/android/build/config/OptionsTest.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.build.config;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class OptionsTest {
+    @Test
+    public void testErrorMissingLast() {
+        final Errors errors = new Errors();
+
+        final Options options = Options.parse(errors, new String[] {
+                    "--error"
+                });
+
+        Assert.assertNotEquals("", TestErrors.getErrorMessages(errors));
+        Assert.assertEquals(Options.Action.DEFAULT, options.getAction());
+        TestErrors.assertHasEntry(errors.ERROR_COMMAND_LINE, errors);
+    }
+
+    @Test
+    public void testErrorMissingNotLast() {
+        final Errors errors = new Errors();
+
+        final Options options = Options.parse(errors, new String[] {
+                    "--error", "--warning", "2"
+                });
+
+        Assert.assertNotEquals("", TestErrors.getErrorMessages(errors));
+        Assert.assertEquals(Options.Action.DEFAULT, options.getAction());
+        TestErrors.assertHasEntry(errors.ERROR_COMMAND_LINE, errors);
+    }
+
+    @Test
+    public void testErrorNotNumeric() {
+        final Errors errors = new Errors();
+
+        final Options options = Options.parse(errors, new String[] {
+                    "--error", "notgood"
+                });
+
+        Assert.assertNotEquals("", TestErrors.getErrorMessages(errors));
+        Assert.assertEquals(Options.Action.DEFAULT, options.getAction());
+        TestErrors.assertHasEntry(errors.ERROR_COMMAND_LINE, errors);
+    }
+
+    @Test
+    public void testErrorInvalidError() {
+        final Errors errors = new Errors();
+
+        final Options options = Options.parse(errors, new String[] {
+                    "--error", "50000"
+                });
+
+        Assert.assertEquals("", TestErrors.getErrorMessages(errors));
+        Assert.assertEquals(Options.Action.DEFAULT, options.getAction());
+        TestErrors.assertHasEntry(errors.WARNING_UNKNOWN_COMMAND_LINE_ERROR, errors);
+    }
+
+    @Test
+    public void testErrorOne() {
+        final Errors errors = new Errors();
+
+        final Options options = Options.parse(errors, new String[] {
+                    "--error", "2"
+                });
+
+        Assert.assertEquals("", TestErrors.getErrorMessages(errors));
+        Assert.assertEquals(Options.Action.DEFAULT, options.getAction());
+        Assert.assertFalse(errors.hadWarningOrError());
+    }
+
+    @Test
+    public void testWarningOne() {
+        final Errors errors = new Errors();
+
+        final Options options = Options.parse(errors, new String[] {
+                    "--warning", "2"
+                });
+
+        Assert.assertEquals("", TestErrors.getErrorMessages(errors));
+        Assert.assertEquals(Options.Action.DEFAULT, options.getAction());
+        Assert.assertFalse(errors.hadWarningOrError());
+    }
+
+    @Test
+    public void testHideOne() {
+        final Errors errors = new Errors();
+
+        final Options options = Options.parse(errors, new String[] {
+                    "--hide", "2"
+                });
+
+        Assert.assertEquals("", TestErrors.getErrorMessages(errors));
+        Assert.assertEquals(Options.Action.DEFAULT, options.getAction());
+        Assert.assertFalse(errors.hadWarningOrError());
+    }
+}
+
diff --git a/tools/product_config/test/com/android/build/config/TestErrors.java b/tools/product_config/test/com/android/build/config/TestErrors.java
new file mode 100644
index 0000000..dde88b0
--- /dev/null
+++ b/tools/product_config/test/com/android/build/config/TestErrors.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.build.config;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Errors for testing.
+ */
+public class TestErrors extends ErrorReporter {
+
+    public static final int ERROR_CODE = 1;
+
+    public final Category ERROR = new Category(ERROR_CODE, true, Level.ERROR,
+            "An error.");
+
+    public static final int WARNING_CODE = 2;
+
+    public final Category WARNING = new Category(WARNING_CODE, true, Level.WARNING,
+            "A warning.");
+
+    public static final int HIDDEN_CODE = 3;
+
+    public final Category HIDDEN = new Category(HIDDEN_CODE, true, Level.HIDDEN,
+            "A hidden warning.");
+
+    public static final int ERROR_FIXED_CODE = 4;
+
+    public final Category ERROR_FIXED = new Category(ERROR_FIXED_CODE, false, Level.ERROR,
+            "An error that can't have its level changed.");
+
+    public void assertHasEntry(Errors.Category category) {
+        assertHasEntry(category, this);
+    }
+
+    public String getErrorMessages() {
+        return getErrorMessages(this);
+    }
+
+    public static void assertHasEntry(Errors.Category category, ErrorReporter errors) {
+        StringBuilder found = new StringBuilder();
+        for (Errors.Entry entry: errors.getEntries()) {
+            if (entry.getCategory() == category) {
+                return;
+            }
+            found.append(' ');
+            found.append(entry.getCategory().getCode());
+        }
+        throw new AssertionError("No error category " + category.getCode() + " found."
+                + " Found category codes were:" + found);
+    }
+
+    public static String getErrorMessages(ErrorReporter errors) {
+        final ByteArrayOutputStream stream = new ByteArrayOutputStream();
+        try {
+            errors.printErrors(new PrintStream(stream, true, StandardCharsets.UTF_8.name()));
+        } catch (UnsupportedEncodingException ex) {
+            // utf-8 is always supported
+        }
+        return new String(stream.toByteArray(), StandardCharsets.UTF_8);
+    }
+}
+