#!/usr/bin/env python
# Copyright 2020 The Tint Authors.
#
# 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.

# Test runner for executing a test of tests with Tint. The runner will
# find all .wgsl files in the given folder and attempt to convert them
# to each of the backend formats. If the file contains a '.fail.' in the
# name then the runner will expect the file to fail conversion.

import base64
import copy
import difflib
import optparse
import os
import platform
import re
import subprocess
import sys
import tempfile


"""
A single test case to be executed. Stores the path to the test file
and the result of executing the test.
"""


class TestCase:
    def __init__(self, input_path, parse_only):
        self.input_path = input_path
        self.parse_only = parse_only
        self.results = {}

    def IsExpectedFail(self):
        fail_re = re.compile('^.+[\.]fail[\.]wgsl')
        return fail_re.match(self.GetInputPath())

    def IsParseOnly(self):
        return self.parse_only

    def GetInputPath(self):
        return self.input_path

    def GetResult(self, fmt):
        return self.results[fmt]

    def GetReason(self):
        with open(self.GetInputPath()) as test:
            first_line = test.readline()
        if (first_line.startswith("# v-")):
            reason = first_line[2:8]
        else:
            reason = ''
        return reason


"""
The test runner, will execute a series of test cases and record the
results.
"""


class TestRunner:
    def RunTest(self, tc):
        """Runs a single test."""
        print("Testing {}".format(tc.GetInputPath()))

        cmd = [self.options.test_prog_path]
        if tc.IsParseOnly():
            cmd += ['--parse-only']

        languages = ["wgsl", "spvasm", "msl", "hlsl"]
        try:
            for lang in languages:
                lang_cmd = copy.copy(cmd)
                lang_cmd += ['--format', lang]
                lang_cmd += [tc.GetInputPath()]
                err = subprocess.check_output(lang_cmd,
                                              stderr=subprocess.STDOUT)

        except Exception as e:
            failure_reason = "{}".format("".join(map(chr,
                                                     bytearray(e.output))))
            if tc.IsExpectedFail():
                right_reason = tc.GetReason()
                if (right_reason in failure_reason):
                    return False, ""
                else:
                    return False, right_reason

            if not tc.IsExpectedFail():
                print(failure_reason)
                print(e)
                return False, ""

        return True, ""

    def RunTests(self):
        """Runs a set of test cases"""
        for tc in self.test_cases:
            result, reason = self.RunTest(tc)
            """evaluate final result based on result, tc.IsExpectedFail() and reason"""
            if not result:
                # result == false, expected true, reason:don't care
                if not tc.IsExpectedFail():
                    print("Expected: " + tc.GetInputPath() +
                          " to pass but failed.")
                    self.failures.append(tc.GetInputPath())
                # result == false, expected false, reason: wrong
                else:
                    if reason.startswith("v-"):
                        print("Failed for a wrong reason: " +
                              tc.GetInputPath() +
                              " expected with error code: " + reason)
                        self.failures_wrong_reason.append(tc.GetInputPath())
            # result == true, expected false, reason:don't care
            elif tc.IsExpectedFail():
                print("Expected: " + tc.GetInputPath() +
                      " to fail but passed.")
                self.failures.append(tc.GetInputPath())

    def GetUnexpectedFailures(self):
        for failure in self.failures + self.failures_wrong_reason:
            if failure not in self.known_failures:
                self.unexpected_failures.append(failure)
        return

    def SummarizeResults(self):
        """Prints a summarization of the test results to STDOUT"""
        if len(self.unexpected_failures):
            self.unexpected_failures.sort()
            print('\nSummary of unexpected failures:')
            for unexpected_fail in self.unexpected_failures:
                print(unexpected_fail)

        for f in self.known_failures:
            if f not in self.failures_wrong_reason + self.failures:
                self.unexpected_successes.append(f)

        if len(self.unexpected_successes):
            print('\nSummary of unexpected successes:')
            for s in self.unexpected_successes:
                print(s)

        print('')
        print('Test cases executed: {}'.format(len(self.test_cases)))
        print('  Successes:  {}'.format(
            (len(self.test_cases) - len(self.failures) -
             len(self.failures_wrong_reason))))
        print('  Failures:   {}'.format(
            len(self.failures) + len(self.failures_wrong_reason)))
        print('  Unexpected Failures:  {}'.format(len(
            self.unexpected_failures)))
        print('  Unexpected Successes:  {}'.format(
            len(self.unexpected_successes)))
        print('')

    def Run(self):
        """Executes the test runner."""
        base_path = os.path.abspath(
            os.path.join(os.path.dirname(__file__), '..'))

        usage = 'usage: %prog [options] (file)'
        parser = optparse.OptionParser(usage=usage)
        parser.add_option('--build-dir',
                          default=os.path.join(base_path, 'out', 'Debug'),
                          help='path to build directory')
        parser.add_option('--test-dir',
                          default=os.path.join(os.path.dirname(__file__), '..',
                                               'third_party', 'gpuweb-cts',
                                               'src', 'webgpu', 'shader',
                                               'validation', 'wgsl'),
                          help='path to directory containing test files')
        parser.add_option(
            '--known-failures-file',
            default=os.path.join(base_path, 'tools', 'known_tint_failures'),
            help='path to directory containing the known failures file')
        parser.add_option(
            '--test-prog-path',
            default=None,
            help='path to program to test (default build-dir/tint)')
        parser.add_option('--parse-only',
                          action="store_true",
                          default=False,
                          help='only parse test cases; do not compile')

        self.options, self.args = parser.parse_args()

        if self.options.test_prog_path == None:
            test_prog = os.path.abspath(
                os.path.join(self.options.build_dir, 'tint'))
            if not os.path.isfile(test_prog):
                print("Cannot find test program {}".format(test_prog))
                return 1

            self.options.test_prog_path = test_prog

        if not os.path.isfile(self.options.test_prog_path):
            print("Cannot find test program '{}'".format(
                self.options.test_prog_path))
            return 1

        input_file_re = re.compile('^.+[\.]wgsl')
        self.test_cases = []

        if self.args:
            for filename in self.args:
                input_path = os.path.join(self.options.test_dir, filename)
                if not os.path.isfile(input_path):
                    print("Cannot find test file '{}'".format(filename))
                    return 1

                self.test_cases.append(
                    TestCase(input_path, self.options.parse_only))

        else:
            for file_dir, _, filename_list in os.walk(self.options.test_dir):
                for input_filename in filename_list:
                    if input_file_re.match(input_filename):
                        input_path = os.path.join(file_dir, input_filename)
                        if os.path.isfile(input_path):
                            self.test_cases.append(
                                TestCase(input_path, self.options.parse_only))
            known_failure_file = self.options.known_failures_file
            self.known_failures = []
            with open(known_failure_file, 'r') as f:
                for failure_filename in f.read().splitlines():
                    self.known_failures.append(
                        os.path.join(self.options.test_dir, failure_filename))

        self.failures = []
        self.failures_wrong_reason = []
        self.unexpected_failures = []
        self.unexpected_successes = []

        self.RunTests()
        self.GetUnexpectedFailures()
        self.SummarizeResults()

        return not len(self.unexpected_failures + self.unexpected_successes)


def main():
    runner = TestRunner()
    return runner.Run()


if __name__ == '__main__':
    sys.exit(main())