#!/usr/bin/env python3

# Copyright 2020 The IREE Authors
#
# Licensed under the Apache License v2.0 with LLVM Exceptions.
# See https://llvm.org/LICENSE.txt for license information.
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""Runs all E2E TensorFlow tests and extracts their benchmarking artifacts.

Example usages:
  # Run all test suites and collect their artifacts:
  python3 ./build_tools/scripts/get_e2e_artifacts.py

  # Run the e2e_tests test suite and collect its artifacts:
  python3 ./build_tools/scripts/get_e2e_artifacts.py --test_suites=e2e_tests
"""

import fileinput
import os
import re
import subprocess
import tempfile
from typing import Dict, Set
import zipfile

import utils

from absl import app
from absl import flags

SUITE_NAME_TO_TARGET = {
    "e2e_tests": "//integrations/tensorflow/e2e:e2e_tests",
    "mobile_bert_squad_tests": "//integrations/tensorflow/e2e:mobile_bert_squad_tests",
    "layers_tests": "//integrations/tensorflow/e2e/keras/layers:layers_tests",
    "layers_dynamic_batch_tests": "//integrations/tensorflow/e2e/keras/layers:layers_dynamic_batch_tests",
    "layers_training_tests": "//integrations/tensorflow/e2e/keras/layers:layers_training_tests",
    "keyword_spotting_tests": "//integrations/tensorflow/e2e/keras:keyword_spotting_tests",
    "keyword_spotting_internal_streaming_tests": "//integrations/tensorflow/e2e/keras:keyword_spotting_internal_streaming_tests",
    "imagenet_non_hermetic_tests": "//integrations/tensorflow/e2e/keras/applications:imagenet_non_hermetic_tests",
    "slim_vision_tests": "//integrations/tensorflow/e2e/slim_vision_models:slim_vision_tests",
}
SUITES_HELP = [f"`{name}`" for name in SUITE_NAME_TO_TARGET]
SUITES_HELP = f'{", ".join(SUITES_HELP[:-1])} and {SUITES_HELP[-1]}'

FLAGS = flags.FLAGS

flags.DEFINE_bool(
    "dry_run",
    False,
    "Run without extracting files. Useful for quickly checking for artifact "
    "collisions.",
)
flags.DEFINE_string(
    "artifacts_dir",
    os.path.join(tempfile.gettempdir(), "iree", "modules"),
    "Directory to transfer the benchmarking artifacts to. Defaults to "
    "/tmp/iree/modules/",
)
flags.DEFINE_bool("run_test_suites", True, "Run any specified test suites.")
flags.DEFINE_list(
    "test_suites",
    list(SUITE_NAME_TO_TARGET.keys()),
    f"Any combination of {SUITES_HELP}.",
)

EXPECTED_COLLISIONS = ["/tf_ref/", "tf_input.mlir", "iree_input.mlir", "/saved_model/"]


def _target_to_testlogs_path(target: str) -> str:
    """Convert target into the path where Bazel stores the artifacts we want."""
    return os.path.join("bazel-testlogs", target.replace("//", "").replace(":", os.sep))


def _target_to_test_name(target: str, test_suite_path: str) -> str:
    """Get test_name from `suite_name_test_name__tf__backend_name`."""
    return target.split("__")[0].replace(f"{test_suite_path}_", "")


def get_test_paths_and_names(test_suite_path: str):
    """Get the paths Bazel stores test outputs in and the matching test names."""
    targets = utils.get_test_targets(test_suite_path)
    test_paths = [_target_to_testlogs_path(target) for target in targets]
    test_names = [_target_to_test_name(target, test_suite_path) for target in targets]
    return test_paths, test_names


def check_collision(
    filename: str,
    test_name: str,
    written_paths: Set[str],
    paths_to_tests: Dict[str, str],
):
    """Check that we aren't overwriting files unless we expect to."""
    # Note: We can't use a check that the files have identical contents because
    # tf_input.mlir can have random numbers appended to its function names.
    # See https://github.com/openxla/iree/issues/3375

    expected_collision = any([name in filename for name in EXPECTED_COLLISIONS])
    if filename in written_paths and not expected_collision:
        raise ValueError(
            f"Collision found on {filename} between {test_name}.py "
            f"and {paths_to_tests[filename]}.py"
        )
    else:
        written_paths.add(filename)
        paths_to_tests[filename] = test_name


def update_path(archive_path: str):
    """Update the --module flag with the new location of the compiled.vmfb"""
    backend_path = archive_path.split("traces")[0]  # 'ModuleName/backend_name'.
    compiled_path = os.path.join(FLAGS.artifacts_dir, backend_path, "compiled.vmfb")
    flagfile_path = os.path.join(FLAGS.artifacts_dir, archive_path)
    for line in fileinput.input(files=[flagfile_path], inplace=True):
        if line.strip().startswith("--module"):
            print(f"--module={compiled_path}\n", end="")
        else:
            print(line, end="")


def extract_artifacts(
    test_path: str,
    test_name: str,
    written_paths: Set[str],
    paths_to_tests: Dict[str, str],
):
    """Unzips all of the benchmarking artifacts for a given test and backend."""
    outputs = os.path.join(test_path, "test.outputs", "outputs.zip")
    if FLAGS.dry_run and not os.path.exists(outputs):
        # The artifacts may or may not be present on disk during a dry run. If they
        # are then we want to collision check them, but if they aren't that's fine.
        return

    archive = zipfile.ZipFile(outputs)
    # Filter out directory names.
    filenames = [name for name in archive.namelist() if name[-1] != os.sep]

    for filename in filenames:
        # Check for collisions.
        check_collision(filename, test_name, written_paths, paths_to_tests)

        # Extract and update flagfile path.
        if not FLAGS.dry_run:
            archive.extract(filename, FLAGS.artifacts_dir)
            if filename.endswith("flagfile"):
                update_path(filename)


def main(argv):
    del argv  # Unused.

    print(
        "The bazel integrations build and tests are deprecated. This script "
        "may be reworked in the future. For the time being refer to "
        "https://github.com/openxla/iree/blob/main/docs/developers/developing_iree/e2e_benchmarking.md "
        "for information on how to run TensorFlow benchmarks."
    )
    exit(1)

    # Convert test suite shorthands to full test suite targets.
    test_suites = [SUITE_NAME_TO_TARGET[suite] for suite in FLAGS.test_suites]

    if FLAGS.run_test_suites:
        # Use bazel test to execute all of the test suites in parallel.
        command = ["bazel", "test", *test_suites, "--color=yes"]
        print(f'Running: `{" ".join(command)}`')
        if not FLAGS.dry_run:
            subprocess.run(command, check=True)
        print()

    written_paths = set()
    paths_to_tests = dict()

    for test_suite in test_suites:
        # Extract all of the artifacts for this test suite.
        test_paths, test_names = get_test_paths_and_names(test_suite)
        for i, (test_path, test_name) in enumerate(zip(test_paths, test_names)):
            print(f"\rTransfering {test_suite} {i + 1}/{len(test_paths)}", end="")
            extract_artifacts(test_path, test_name, written_paths, paths_to_tests)
        print("\n")


if __name__ == "__main__":
    app.run(main)
