Export more testing infrastructure.

PiperOrigin-RevId: 533158991
Change-Id: Ic1ebfa3dd2048ecc1b879867af6843d8dfe6326e
diff --git a/defs.bzl b/defs.bzl
index 8abd100..0bfb9a8 100644
--- a/defs.bzl
+++ b/defs.bzl
@@ -14,6 +14,7 @@
 
 """Workspace setup macro for rules_android."""
 
+load("@bazel_skylib//:workspace.bzl", "bazel_skylib_workspace")
 load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies", "go_repository")
 load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps")
 load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies")
@@ -22,6 +23,8 @@
 
 def rules_android_workspace():
     """ Sets up workspace dependencies for rules_android."""
+    bazel_skylib_workspace()
+
     protobuf_deps()
 
     maven_install(
diff --git a/kokoro/presubmit/kokoro_presubmit.sh b/kokoro/presubmit/kokoro_presubmit.sh
index c7ec3cb..b90b12c 100644
--- a/kokoro/presubmit/kokoro_presubmit.sh
+++ b/kokoro/presubmit/kokoro_presubmit.sh
@@ -61,6 +61,7 @@
   "--verbose_failures"
   "--experimental_google_legacy_api"
   "--experimental_enable_android_migration_apis"
+  "--build_tests_only"
 )
 
 # Go to rules_android workspace and run relevant tests.
@@ -71,7 +72,8 @@
 "$bazel" aquery 'deps(...)' 2>&1 > /dev/null
 
 "$bazel" test "${COMMON_ARGS[@]}" //src/common/golang/... \
-  //src/tools/ak/...
+  //src/tools/ak/... \
+  //test/...
 
 # Go to basic app workspace in the source tree
 cd "${KOKORO_ARTIFACTS_DIR}/git/rules_android/examples/basicapp"
diff --git a/test/utils/BUILD b/test/utils/BUILD
new file mode 100644
index 0000000..81cb750
--- /dev/null
+++ b/test/utils/BUILD
@@ -0,0 +1,27 @@
+load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
+
+package(
+    default_applicable_licenses = ["//:license"],
+    default_visibility = ["//visibility:private"],
+)
+
+licenses(["notice"])
+
+exports_files(
+    ["lib.bzl"],
+    visibility = [
+        "//test:__subpackages__",
+    ],
+)
+
+filegroup(
+    name = "testing",
+    srcs = glob(["*"]),
+    visibility = ["//test:__pkg__"],
+)
+
+bzl_library(
+    name = "bzl",
+    srcs = glob(["*.bzl"]),
+    visibility = ["//tools/build_defs/android:__subpackages__"],
+)
diff --git a/test/utils/asserts.bzl b/test/utils/asserts.bzl
new file mode 100644
index 0000000..a1e5a1d
--- /dev/null
+++ b/test/utils/asserts.bzl
@@ -0,0 +1,610 @@
+# Copyright 2020 The Bazel Authors. All rights reserved.
+#
+# 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.
+
+"""Bazel testing library asserts."""
+
+load(
+    "//rules:providers.bzl",
+    "ResourcesNodeInfo",
+    "StarlarkAndroidResourcesInfo",
+)
+
+_ATTRS = dict(
+    expected_default_info = attr.string_list_dict(),
+    expected_java_info = attr.string_list_dict(),
+    expected_proguard_spec_provider = attr.string_list_dict(),
+    expected_starlark_android_resources_info = attr.label(),
+    expected_output_group_info = attr.string_list_dict(),
+    expected_native_libs_info = attr.label(),
+)
+
+def _expected_resources_node_info_impl(ctx):
+    return [
+        ResourcesNodeInfo(
+            label = ctx.attr.label.label,
+            assets = ctx.files.assets,
+            assets_dir = ctx.attr.assets_dir,
+            assets_symbols = ctx.attr.assets_symbols if ctx.attr.assets_symbols else None,
+            compiled_assets = ctx.attr.compiled_assets if ctx.attr.compiled_assets else None,
+            compiled_resources = ctx.attr.compiled_resources if ctx.attr.compiled_resources else None,
+            r_txt = ctx.attr.r_txt if ctx.attr.r_txt else None,
+            manifest = ctx.attr.manifest if ctx.attr.manifest else None,
+            exports_manifest = ctx.attr.exports_manifest,
+        ),
+    ]
+
+_expected_resources_node_info = rule(
+    implementation = _expected_resources_node_info_impl,
+    attrs = dict(
+        label = attr.label(),
+        assets = attr.label_list(allow_files = True),
+        assets_dir = attr.string(),
+        assets_symbols = attr.string(),
+        compiled_assets = attr.string(),
+        compiled_resources = attr.string(),
+        r_txt = attr.string(),
+        manifest = attr.string(),
+        exports_manifest = attr.bool(default = False),
+    ),
+)
+
+def ExpectedResourcesNodeInfo(
+        label,
+        assets = [],
+        assets_dir = "",
+        assets_symbols = None,
+        compiled_assets = None,
+        compiled_resources = None,
+        r_txt = None,
+        manifest = None,
+        exports_manifest = False,
+        name = "unused"):  # appease linter
+    name = label + str(assets) + assets_dir + str(assets_symbols) + str(compiled_resources) + str(exports_manifest)
+    name = ":" + "".join([c for c in name.elems() if c != ":"])
+
+    _expected_resources_node_info(
+        name = name[1:],
+        label = label,
+        assets = assets,
+        assets_dir = assets_dir,
+        assets_symbols = assets_symbols,
+        compiled_assets = compiled_assets,
+        compiled_resources = compiled_resources,
+        r_txt = r_txt,
+        manifest = manifest,
+        exports_manifest = exports_manifest,
+    )
+    return name
+
+def _expected_starlark_android_resources_info_impl(ctx):
+    return [
+        StarlarkAndroidResourcesInfo(
+            direct_resources_nodes = [node[ResourcesNodeInfo] for node in ctx.attr.direct_resources_nodes],
+            transitive_resources_nodes = [node[ResourcesNodeInfo] for node in ctx.attr.transitive_resources_nodes],
+            transitive_assets = ctx.attr.transitive_assets,
+            transitive_assets_symbols = ctx.attr.transitive_assets_symbols,
+            transitive_compiled_resources = ctx.attr.transitive_compiled_resources,
+            packages_to_r_txts = ctx.attr.packages_to_r_txts,
+        ),
+    ]
+
+_expected_starlark_android_resources_info = rule(
+    implementation = _expected_starlark_android_resources_info_impl,
+    attrs = dict(
+        direct_resources_nodes = attr.label_list(
+            providers = [ResourcesNodeInfo],
+        ),
+        transitive_resources_nodes = attr.label_list(
+            providers = [ResourcesNodeInfo],
+        ),
+        transitive_assets = attr.string_list(),
+        transitive_assets_symbols = attr.string_list(),
+        transitive_compiled_resources = attr.string_list(),
+        packages_to_r_txts = attr.string_list_dict(),
+    ),
+)
+
+def ExpectedStarlarkAndroidResourcesInfo(
+        direct_resources_nodes = None,
+        transitive_resources_nodes = [],
+        transitive_assets = [],
+        transitive_assets_symbols = [],
+        transitive_compiled_resources = [],
+        packages_to_r_txts = {},
+        name = "unused"):  # appease linter
+    name = (str(direct_resources_nodes) + str(transitive_resources_nodes) + str(transitive_assets) +
+            str(transitive_assets_symbols) + str(transitive_compiled_resources))
+    name = ":" + "".join([c for c in name.elems() if c not in [":", "\\"]])
+    _expected_starlark_android_resources_info(
+        name = name[1:],
+        direct_resources_nodes = direct_resources_nodes,
+        transitive_resources_nodes = transitive_resources_nodes,
+        transitive_assets = transitive_assets,
+        transitive_assets_symbols = transitive_assets_symbols,
+        transitive_compiled_resources = transitive_compiled_resources,
+        packages_to_r_txts = packages_to_r_txts,
+    )
+    return name
+
+def _build_expected_resources_node_info(string):
+    parts = string.split(":")
+    if len(parts) != 5:
+        fail("Error: malformed resources_node_info string: %s" % string)
+    return dict(
+        label = parts[0],
+        assets = parts[1].split(",") if parts[1] else [],
+        assets_dir = parts[2],
+        assets_symbols = parts[3],
+        compiled_resources = parts[4],
+    )
+
+def _expected_android_binary_native_libs_info_impl(ctx):
+    return _ExpectedAndroidBinaryNativeInfo(
+        transitive_native_libs = ctx.attr.transitive_native_libs,
+        native_libs_name = ctx.attr.native_libs_name,
+        native_libs = ctx.attr.native_libs,
+    )
+
+_expected_android_binary_native_libs_info = rule(
+    implementation = _expected_android_binary_native_libs_info_impl,
+    attrs = {
+        "transitive_native_libs": attr.string_list(),
+        "native_libs_name": attr.string(),
+        "native_libs": attr.string_list_dict(),
+    },
+)
+
+def ExpectedAndroidBinaryNativeLibsInfo(**kwargs):
+    name = "".join([str(kwargs[param]) for param in kwargs])
+    name = ":" + "".join([c for c in name.elems() if c not in [" ", "[", "]", ":", "\\", "{", "\""]])
+    _expected_android_binary_native_libs_info(name = name[1:], **kwargs)
+    return name
+
+_ExpectedAndroidBinaryNativeInfo = provider(
+    "Test provider to compare native deps info",
+    fields = ["native_libs", "native_libs_name", "transitive_native_libs"],
+)
+
+def _assert_native_libs_info(expected, actual):
+    expected = expected[_ExpectedAndroidBinaryNativeInfo]
+    if expected.native_libs_name:
+        _assert_file(
+            expected.native_libs_name,
+            actual.native_libs_name,
+            "AndroidBinaryNativeInfo.native_libs_name",
+        )
+    for config in expected.native_libs:
+        if config not in actual.native_libs:
+            fail("Error for AndroidBinaryNativeInfo.native_libs: expected key %s was not found" % config)
+        _assert_files(
+            expected.native_libs[config],
+            actual.native_libs[config].to_list(),
+            "AndroidBinaryNativeInfo.native_libs." + config,
+        )
+    _assert_files(
+        expected.transitive_native_libs,
+        actual.transitive_native_libs.to_list(),
+        "AndroidBinaryNativeInfo.transitive_native_libs",
+    )
+
+def _assert_files(expected_file_names, actual_files, error_msg_field_name):
+    """Asserts that expected file names and actual list of files is equal.
+
+    Args:
+      expected_file_names: The expected names of file basenames (no path),
+      actual_files: The actual list of files produced.
+      error_msg_field_name: The field the actual list of files is from.
+    """
+    actual_file_names = [f.basename for f in actual_files]
+    if sorted(actual_file_names) == sorted(expected_file_names):
+        return
+    fail("""Error for %s, expected and actual file names are not the same:
+expected file names: %s
+actual files: %s
+""" % (error_msg_field_name, expected_file_names, actual_files))
+
+def _assert_file_objects(expected_files, actual_files, error_msg_field_name):
+    if sorted([f.basename for f in expected_files]) == sorted([f.basename for f in actual_files]):
+        return
+    fail("""Error for %s, expected and actual file names are not the same:
+expected file names: %s
+actual files: %s
+""" % (error_msg_field_name, expected_files, actual_files))
+
+def _assert_file_depset(expected_file_paths, actual_depset, error_msg_field_name, ignore_label_prefix = ""):
+    """Asserts that expected file short_paths and actual depset of files is equal.
+
+    Args:
+      expected_file_paths: The expected file short_paths in depset order.
+      actual_depset: The actual depset produced.
+      error_msg_field_name: The field the actual depset is from.
+      ignore_label_prefix: Path prefix to ignore on actual file short_paths.
+    """
+    actual_paths = []  # = [f.short_path for f in actual_depset.to_list()]
+    for f in actual_depset.to_list():
+        path = f.short_path
+        if path.startswith(ignore_label_prefix):
+            path = path[len(ignore_label_prefix):]
+        actual_paths.append(path)
+
+    if len(expected_file_paths) != len(actual_paths):
+        fail("""Error for %s, expected %d items, got %d items
+expected: %s
+actual: %s""" % (
+            error_msg_field_name,
+            len(expected_file_paths),
+            len(actual_paths),
+            expected_file_paths,
+            actual_paths,
+        ))
+    for i in range(len(expected_file_paths)):
+        if expected_file_paths[i] != actual_paths[i]:
+            fail("""Error for %s, actual file depset ordering does not match expected ordering:
+expected ordering: %s
+actual ordering: %s
+""" % (error_msg_field_name, expected_file_paths, actual_paths))
+
+def _assert_empty(contents, error_msg_field_name):
+    """Asserts that the given is empty."""
+    if len(contents) == 0:
+        return
+    fail("Error %s is not empty: %s" % (error_msg_field_name, contents))
+
+def _assert_none(content, error_msg_field_name):
+    """Asserts that the given is None."""
+    if content == None:
+        return
+    fail("Error %s is not None: %s" % (error_msg_field_name, content))
+
+def _assert_java_info(expected, actual):
+    """Asserts that expected matches actual JavaInfo.
+
+    Args:
+      expected: A dict containing fields of a JavaInfo that are compared against
+        the actual given JavaInfo.
+      actual: A JavaInfo.
+    """
+    for key in expected.keys():
+        if not hasattr(actual, key):
+            fail("Actual JavaInfo does not have attribute %s:\n%s" % (key, actual))
+        actual_attr = getattr(actual, key)
+        expected_attr = expected[key]
+
+        # files based asserts.
+        if key in [
+            "compile_jars",
+            "runtime_output_jars",
+            "source_jars",
+            "transitive_compile_time_jars",
+            "transitive_runtime_jars",
+            "transitive_source_jars",
+        ]:
+            files = \
+                actual_attr if type(actual_attr) == "list" else actual_attr.to_list()
+            _assert_files(expected_attr, files, "JavaInfo.%s" % key)
+        else:
+            fail("Error validation of JavaInfo.%s not implemented." % key)
+
+def _assert_default_info(
+        expected,
+        actual):
+    """Asserts that the DefaultInfo contains the expected values."""
+    if not expected:
+        return
+
+    # DefaultInfo.data_runfiles Assertions
+    _assert_empty(
+        actual.data_runfiles.empty_filenames.to_list(),
+        "DefaultInfo.data_runfiles.empty_filenames",
+    )
+    _assert_files(
+        expected["runfiles"],
+        actual.data_runfiles.files.to_list(),
+        "DefaultInfo.data_runfiles.files",
+    )
+    _assert_empty(
+        actual.data_runfiles.symlinks.to_list(),
+        "DefaultInfo.data_runfiles.symlinks",
+    )
+
+    # DefaultInfo.default_runfile Assertions
+    _assert_empty(
+        actual.default_runfiles.empty_filenames.to_list(),
+        "DefaultInfo.default_runfiles.empty_filenames",
+    )
+    _assert_files(
+        expected["runfiles"],
+        actual.default_runfiles.files.to_list(),
+        "DefaultInfo.default_runfiles.files",
+    )
+    _assert_empty(
+        actual.default_runfiles.symlinks.to_list(),
+        "DefaultInfo.default_runfiles.symlinks",
+    )
+
+    # DefaultInfo.files Assertion
+    _assert_files(
+        expected["files"],
+        actual.files.to_list(),
+        "DefaultInfo.files",
+    )
+
+    # DefaultInfo.files_to_run Assertions
+    _assert_none(
+        actual.files_to_run.executable,
+        "DefaultInfo.files_to_run.executable",
+    )
+    _assert_none(
+        actual.files_to_run.runfiles_manifest,
+        "DefaultInfo.files_to_run.runfiles_manifest",
+    )
+
+def _assert_proguard_spec_provider(expected, actual):
+    """Asserts that expected matches actual ProguardSpecProvider.
+
+    Args:
+      expected: A dict containing fields of a ProguardSpecProvider that are
+        compared against the actual given ProguardSpecProvider.
+      actual: A ProguardSpecProvider.
+    """
+    for key in expected.keys():
+        if not hasattr(actual, key):
+            fail("Actual ProguardSpecProvider does not have attribute %s:\n%s" % (key, actual))
+        actual_attr = getattr(actual, key)
+        expected_attr = expected[key]
+        if key in ["specs"]:
+            _assert_files(
+                expected_attr,
+                actual_attr.to_list(),
+                "ProguardSpecProvider.%s" % key,
+            )
+        else:
+            fail("Error validation of ProguardSpecProvider.%s not implemented." % key)
+
+def _assert_string(expected, actual, error_msg):
+    if type(actual) != "string":
+        fail("Error for %s, actual value not of type string, got %s" % (error_msg, type(actual)))
+    if actual != expected:
+        fail("""Error for %s, expected and actual values are not the same:
+expected value: %s
+actual value: %s
+""" % (error_msg, expected, actual))
+
+def _assert_file(expected, actual, error_msg_field_name):
+    if actual == None and expected == None:
+        return
+
+    if actual == None and expected != None:
+        fail("Error at %s, expected %s but got None" % (error_msg_field_name, expected))
+
+    if type(actual) != "File":
+        fail("Error at %s, expected a File but got %s" % (error_msg_field_name, type(actual)))
+
+    if actual != None and expected == None:
+        fail("Error at %s, expected None but got %s" % (error_msg_field_name, actual.short_path))
+
+    ignore_label_prefix = actual.owner.package + "/"
+    actual_path = actual.short_path
+    if actual_path.startswith(ignore_label_prefix):
+        actual_path = actual_path[len(ignore_label_prefix):]
+    _assert_string(expected, actual_path, error_msg_field_name)
+
+def _assert_resources_node_info(expected, actual):
+    if type(actual.label) != "Label":
+        fail("Error for ResourcesNodeInfo.label, expected type Label, actual type is %s" % type(actual.label))
+    _assert_string(expected.label.name, actual.label.name, "ResourcesNodeInfo.label.name")
+
+    if type(actual.assets) != "depset":
+        fail("Error for ResourcesNodeInfo.assets, expected type depset, actual type is %s" % type(actual.assets))
+
+    # TODO(djwhang): Align _assert_file_objects and _assert_file_depset to work
+    # in a similar manner. For now, we will just call to_list() as this field
+    # was list prior to this change.
+    _assert_file_objects(expected.assets, actual.assets.to_list(), "ResourcesNodeInfo.assets")
+
+    _assert_string(expected.assets_dir, actual.assets_dir, "ResourcesNodeInfo.assets_dir")
+
+    _assert_file(
+        expected.assets_symbols,
+        actual.assets_symbols,
+        "ResourcesNodeInfo.assets_symbols",
+    )
+
+    _assert_file(
+        expected.compiled_assets,
+        actual.compiled_assets,
+        "ResourcesNodeInfo.compiled_assets",
+    )
+
+    _assert_file(
+        expected.compiled_resources,
+        actual.compiled_resources,
+        "ResourcesNodeInfo.compiled_resources",
+    )
+
+    _assert_file(
+        expected.r_txt,
+        actual.r_txt,
+        "ResourcesNodeInfo.r_txt",
+    )
+
+    _assert_file(
+        expected.manifest,
+        actual.manifest,
+        "ResourcesNodeInfo.manifest",
+    )
+
+    if type(actual.exports_manifest) != "bool":
+        fail("Error for ResourcesNodeInfo.exports_manifest, expected type bool, actual type is %s" % type(actual.exports_manifest))
+    if expected.exports_manifest != actual.exports_manifest:
+        fail("""Error for ResourcesNodeInfo.exports_manifest, expected and actual values are not the same:
+expected value: %s
+actual value: %s
+""" % (expected.exports_manifest, actual.exports_manifest))
+
+def _assert_resources_node_info_depset(expected_resources_node_infos, actual_depset, error_msg):
+    actual_resources_node_infos = actual_depset.to_list()
+    if len(expected_resources_node_infos) != len(actual_resources_node_infos):
+        fail(
+            "Error for StarlarkAndroidResourcesInfo.%s, expected size of list to be %d, got %d:\nExpected: %s\nActual: %s" %
+            (
+                error_msg,
+                len(expected_resources_node_infos),
+                len(actual_resources_node_infos),
+                [node.label for node in expected_resources_node_infos],
+                [node.label for node in actual_resources_node_infos],
+            ),
+        )
+    for i in range(len(actual_resources_node_infos)):
+        _assert_resources_node_info(expected_resources_node_infos[i], actual_resources_node_infos[i])
+
+def _assert_starlark_android_resources_info(expected, actual, label_under_test):
+    _assert_resources_node_info_depset(
+        expected.direct_resources_nodes,
+        actual.direct_resources_nodes,
+        "direct_resources_nodes",
+    )
+
+    _assert_resources_node_info_depset(
+        expected.transitive_resources_nodes,
+        actual.transitive_resources_nodes,
+        "transitive_resources_nodes",
+    )
+
+    # Use the package from the target under test to shrink actual paths being compared down to the
+    # name of the target.
+    ignore_label_prefix = label_under_test.package + "/"
+
+    _assert_file_depset(
+        expected.transitive_assets,
+        actual.transitive_assets,
+        "StarlarkAndroidResourcesInfo.transitive_assets",
+        ignore_label_prefix,
+    )
+    _assert_file_depset(
+        expected.transitive_assets_symbols,
+        actual.transitive_assets_symbols,
+        "StarlarkAndroidResourcesInfo.transitive_assets_symbols",
+        ignore_label_prefix,
+    )
+    _assert_file_depset(
+        expected.transitive_compiled_resources,
+        actual.transitive_compiled_resources,
+        "StarlarkAndroidResourcesInfo.transitive_compiled_resources",
+        ignore_label_prefix,
+    )
+    for pkg, value in expected.packages_to_r_txts.items():
+        if pkg in actual.packages_to_r_txts:
+            _assert_file_depset(
+                value,
+                actual.packages_to_r_txts[pkg],
+                "StarlarkAndroidResourcesInfo.packages_to_r_txts[%s]" % pkg,
+                ignore_label_prefix,
+            )
+        else:
+            fail("Error for StarlarkAndroidResourceInfo.packages_to_r_txts, expected key %s was not found" % pkg)
+
+_R_CLASS_ATTRS = dict(
+    _r_class_check = attr.label(
+        default = "//test/utils/java/com/google:RClassChecker_deploy.jar",
+        executable = True,
+        allow_files = True,
+        cfg = "exec",
+    ),
+    expected_r_class_fields = attr.string_list(),
+)
+
+def _assert_output_group_info(expected, actual):
+    for key in expected:
+        actual_attr = getattr(actual, key, None)
+        if actual_attr == None:  # both empty depset and list will fail.
+            fail("%s is not defined in OutputGroupInfo: %s" % (key, actual))
+        _assert_files(
+            expected[key],
+            actual_attr.to_list(),
+            "OutputGroupInfo." + key,
+        )
+
+def _is_suffix_sublist(full, suffixes):
+    """Returns whether suffixes is a sublist of suffixes of full."""
+    for (fi, _) in enumerate(full):
+        sublist_match = True
+        for (si, sv) in enumerate(suffixes):
+            if (fi + si >= len(full)) or not full[fi + si].endswith(sv):
+                sublist_match = False
+                break
+        if sublist_match:
+            return True
+    return False
+
+def _check_actions(inspect, actions):
+    for mnemonic, expected_argvs in inspect.items():
+        # Action mnemonic is not unique, even in the context of a target, hence
+        # it is necessary to find all actions and compare argv. If there are no
+        # matches among the actions that match the mnemonic, fail and present
+        # all the possible actions that could have matched.
+        mnemonic_matching_actions = []
+        mnemonic_match = False
+        for _, value in actions.by_file.items():
+            if mnemonic != value.mnemonic:
+                continue
+            mnemonic_match = True
+
+            if _is_suffix_sublist(value.argv, expected_argvs):
+                # When there is a match, clear the actions stored for displaying
+                # an error messaage.
+                mnemonic_matching_actions = []
+                break
+            else:
+                mnemonic_matching_actions.append(value)
+
+        if not mnemonic_match:
+            fail("%s action not found." % mnemonic)
+        if mnemonic_matching_actions:
+            # If there are mnemonic_matching_actions, then the argvs did not
+            # align. Fail but show the other actions that were created.
+            error_message = (
+                "%s with the following argv not found: %s\nSimilar actions:\n" %
+                (mnemonic, expected_argvs)
+            )
+            for i, action in enumerate(mnemonic_matching_actions):
+                error_message += (
+                    "%d. Progress Message: %s\n   Argv:             %s\n\n" %
+                    (i + 1, action, action.argv)
+                )
+            fail(error_message)
+
+_ACTIONS_ATTRS = dict(
+    inspect_actions = attr.string_list_dict(),
+)
+
+asserts = struct(
+    provider = struct(
+        attrs = _ATTRS,
+        default_info = _assert_default_info,
+        java_info = _assert_java_info,
+        proguard_spec_provider = _assert_proguard_spec_provider,
+        starlark_android_resources_info = _assert_starlark_android_resources_info,
+        output_group_info = _assert_output_group_info,
+        native_libs_info = _assert_native_libs_info,
+    ),
+    files = _assert_files,
+    r_class = struct(
+        attrs = _R_CLASS_ATTRS,
+    ),
+    actions = struct(
+        attrs = _ACTIONS_ATTRS,
+        check_actions = _check_actions,
+    ),
+)
diff --git a/test/utils/file.bzl b/test/utils/file.bzl
new file mode 100644
index 0000000..e019542
--- /dev/null
+++ b/test/utils/file.bzl
@@ -0,0 +1,44 @@
+# Copyright 2018 The Bazel Authors. All rights reserved.
+#
+# 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.
+
+"""Bazel lib that provides on-the-fly data generation helpers for testing."""
+
+def _create(
+        name = None,
+        contents = "",
+        executable = False):
+    target_name = "gen_" + name.replace(".", "_")
+    native.genrule(
+        name = target_name,
+        cmd = """
+cat > $@ <<MAKE_FILE_EOM
+%s
+MAKE_FILE_EOM
+""" % contents,
+        outs = [name],
+        executable = executable,
+    )
+    return name
+
+def _create_mock_file(path, is_directory = False):
+    return struct(
+        path = path,
+        dirname = path.rpartition("/")[0],
+        is_directory = is_directory,
+    )
+
+file = struct(
+    create = _create,
+    create_mock_file = _create_mock_file,
+)
diff --git a/test/utils/lib.bzl b/test/utils/lib.bzl
new file mode 100644
index 0000000..fef1281
--- /dev/null
+++ b/test/utils/lib.bzl
@@ -0,0 +1,51 @@
+# Copyright 2018 The Bazel Authors. All rights reserved.
+#
+# 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.
+
+"""Bazel Android testing libs."""
+
+load(
+    ":file.bzl",
+    _file = "file",
+)
+load(
+    ":unittest.bzl",
+    _analysistest = "analysistest",
+    _unittest = "unittest",
+)
+load(
+    "@bazel_skylib//lib:unittest.bzl",
+    _asserts = "asserts",
+)
+
+file = _file
+
+unittest = _unittest
+
+analysistest = _analysistest
+
+asserts = _asserts
+
+def _failure_test_impl(ctx):
+    env = analysistest.begin(ctx)
+    if ctx.attr.expected_error_msg != "":
+        asserts.expect_failure(env, ctx.attr.expected_error_msg)
+    return analysistest.end(env)
+
+failure_test = analysistest.make(
+    _failure_test_impl,
+    expect_failure = True,
+    attrs = dict(
+        expected_error_msg = attr.string(),
+    ),
+)
diff --git a/test/utils/unittest.bzl b/test/utils/unittest.bzl
new file mode 100644
index 0000000..8526902
--- /dev/null
+++ b/test/utils/unittest.bzl
@@ -0,0 +1,108 @@
+# Copyright 2018 The Bazel Authors. All rights reserved.
+#
+# 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.
+
+"""Bazel lib that provides test helpers for testing."""
+
+load(":file.bzl", _file = "file")
+load(
+    "@bazel_skylib//lib:unittest.bzl",
+    _analysistest = "analysistest",
+    _unittest = "unittest",
+)
+
+TestInfo = provider(
+    doc = "Provides a test a suggested set of attributes.",
+    fields = {
+        "name": "The name of the test.",
+        "prefix": "The prefix used to isolate artifact and target names.",
+    },
+)
+
+def _prefix(prefix, name):
+    """Prepends the given prefix to the given name."""
+    return "%s-%s" % (prefix, name)
+
+def _prefix_from_test_info(test_info, name):
+    """Prepends the prefix of a TestInfo to the given name."""
+    return _prefix(test_info.prefix, name)
+
+def _test_suite(
+        name = None,
+        test_scenarios = None):
+    """Creates a test suite containing the list of test targets.
+
+    Args:
+      name: Name of the test suite, also used as part of a prefix for naming.
+      test_scenarios: A list of methods, that setup and the test. Each scenario
+        method should accept a TestInfo provider.
+    """
+    test_targets = []
+    for scenario_name, make_scenario in test_scenarios.items():
+        test_prefix = _prefix(name, scenario_name)
+        test_info = TestInfo(
+            prefix = test_prefix,
+            name = test_prefix + "_test",
+        )
+        make_scenario(test_info)
+        test_targets.append(test_info.name)
+
+    native.test_suite(
+        name = name,
+        tests = test_targets,
+    )
+
+def _fake_java_library(name):
+    class_name = "".join(
+        [part.title() for part in name.replace("-", "_").split("_")],
+    )
+    native.java_library(
+        name = name,
+        srcs = [_file.create(
+            class_name + ".java",
+            contents = """@SuppressWarnings("DefaultPackage")
+class %s{}""" % class_name,
+        )],
+    )
+
+def _fake_jar(name):
+    if not name.endswith(".jar"):
+        fail("fake_jar method requires name to end with '.jar'")
+    _fake_java_library(name[:-4])
+    return name
+
+def _fake_executable(name):
+    return _file.create(name, contents = "echo %s" % name, executable = True)
+
+def _analysis_test_error(message, *args):
+    return [
+        AnalysisTestResultInfo(
+            success = False,
+            message = message % args,
+        ),
+    ]
+
+analysistest = _analysistest
+
+unittest = struct(
+    # Forward through unittest methods through the current unittest.
+    analysis_test_error = _analysis_test_error,
+    begin = _unittest.begin,
+    end = _unittest.end,
+    fake_executable = _fake_executable,
+    fake_jar = _fake_jar,
+    fake_java_library = _fake_java_library,
+    make = _unittest.make,
+    prefix = _prefix_from_test_info,
+    test_suite = _test_suite,
+)