#!/usr/bin/env python

# Copyright JS Foundation and other contributors, http://js.foundation
#
# 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.


# This file is based on work under the following copyright and permission notice:
# https://github.com/test262-utils/test262-harness-py
# test262.py, _monkeyYaml.py, parseTestRecord.py

# license of test262.py:
# Copyright 2009 the Sputnik authors.  All rights reserved.
# This code is governed by the BSD license found in the LICENSE file.
# This is derived from sputnik.py, the Sputnik console test runner,
# with elements from packager.py, which is separately
# copyrighted. TODO: Refactor so there is less duplication between
# test262.py and packager.py.

# license of _packager.py:
# Copyright (c) 2012 Ecma International.  All rights reserved.
# This code is governed by the BSD license found in the LICENSE file.

# license of _monkeyYaml.py:
# Copyright 2014 by Sam Mikes.  All rights reserved.
# This code is governed by the BSD license found in the LICENSE file.

# license of parseTestRecord.py:
# Copyright 2011 by Google, Inc.  All rights reserved.
# This code is governed by the BSD license found in the LICENSE file.


from __future__ import print_function

import logging
import optparse
import os
from os import path
import platform
import re
import subprocess
import sys
import tempfile
import xml.dom.minidom
from collections import Counter

import signal
import threading
import multiprocessing

#######################################################################
# based on _monkeyYaml.py
#######################################################################

M_YAML_LIST_PATTERN = re.compile(r"^\[(.*)\]$")
M_YAML_MULTILINE_LIST = re.compile(r"^ *- (.*)$")


# The timeout of each test case
TEST262_CASE_TIMEOUT = 5


def yaml_load(string):
    return my_read_dict(string.splitlines())[1]


def my_read_dict(lines, indent=""):
    dictionary = {}
    key = None
    empty_lines = 0

    while lines:
        if not lines[0].startswith(indent):
            break

        line = lines.pop(0)
        if my_is_all_spaces(line):
            empty_lines += 1
            continue

        result = re.match(r"(.*?):(.*)", line)

        if result:
            if not dictionary:
                dictionary = {}
            key = result.group(1).strip()
            value = result.group(2).strip()
            (lines, value) = my_read_value(lines, value, indent)
            dictionary[key] = value
        else:
            if dictionary and key and key in dictionary:
                char = " " if empty_lines == 0 else "\n" * empty_lines
                dictionary[key] += char + line.strip()
            else:
                raise Exception("monkeyYaml is confused at " + line)
        empty_lines = 0

    if not dictionary:
        dictionary = None

    return lines, dictionary


def my_read_value(lines, value, indent):
    if value == ">" or value == "|":
        (lines, value) = my_multiline(lines, value == "|")
        value = value + "\n"
        return (lines, value)
    if lines and not value:
        if my_maybe_list(lines[0]):
            return my_multiline_list(lines, value)
        indent_match = re.match("(" + indent + r"\s+)", lines[0])
        if indent_match:
            if ":" in lines[0]:
                return my_read_dict(lines, indent_match.group(1))
            return my_multiline(lines, False)
    return lines, my_read_one_line(value)


def my_maybe_list(value):
    return M_YAML_MULTILINE_LIST.match(value)


def my_multiline_list(lines, value):
    # assume no explcit indentor (otherwise have to parse value)
    value = []
    indent = 0
    while lines:
        line = lines.pop(0)
        leading = my_leading_spaces(line)
        if my_is_all_spaces(line):
            pass
        elif leading < indent:
            lines.insert(0, line)
            break
        else:
            indent = indent or leading
            value += [my_read_one_line(my_remove_list_header(indent, line))]
    return (lines, value)


def my_remove_list_header(indent, line):
    line = line[indent:]
    return M_YAML_MULTILINE_LIST.match(line).group(1)


def my_read_one_line(value):
    if M_YAML_LIST_PATTERN.match(value):
        return my_flow_list(value)
    elif re.match(r"^[-0-9]*$", value):
        try:
            value = int(value)
        except ValueError:
            pass
    elif re.match(r"^[-.0-9eE]*$", value):
        try:
            value = float(value)
        except ValueError:
            pass
    elif re.match(r"^('|\").*\1$", value):
        value = value[1:-1]
    return value


def my_flow_list(value):
    result = M_YAML_LIST_PATTERN.match(value)
    values = result.group(1).split(",")
    return [my_read_one_line(v.strip()) for v in values]


def my_multiline(lines, preserve_newlines=False):
    # assume no explcit indentor (otherwise have to parse value)
    value = ""
    indent = my_leading_spaces(lines[0])
    was_empty = None

    while lines:
        line = lines.pop(0)
        is_empty = my_is_all_spaces(line)

        if is_empty:
            if preserve_newlines:
                value += "\n"
        elif my_leading_spaces(line) < indent:
            lines.insert(0, line)
            break
        else:
            if preserve_newlines:
                if was_empty != None:
                    value += "\n"
            else:
                if was_empty:
                    value += "\n"
                elif was_empty is False:
                    value += " "
            value += line[(indent):]

        was_empty = is_empty

    return (lines, value)


def my_is_all_spaces(line):
    return len(line.strip()) == 0


def my_leading_spaces(line):
    return len(line) - len(line.lstrip(' '))


#######################################################################
# based on parseTestRecord.py
#######################################################################

# Matches trailing whitespace and any following blank lines.
_BLANK_LINES = r"([ \t]*[\r\n]{1,2})*"

# Matches the YAML frontmatter block.
# It must be non-greedy because test262-es2015/built-ins/Object/assign/Override.js contains a comment like yaml pattern
_YAML_PATTERN = re.compile(r"/\*---(.*?)---\*/" + _BLANK_LINES, re.DOTALL)

# Matches all known variants for the license block.
# https://github.com/tc39/test262/blob/705d78299cf786c84fa4df473eff98374de7135a/tools/lint/lib/checks/license.py
_LICENSE_PATTERN = re.compile(
    r'// Copyright( \([C]\))? (\w+) .+\. {1,2}All rights reserved\.[\r\n]{1,2}' +
    r'(' +
    r'// This code is governed by the( BSD)? license found in the LICENSE file\.' +
    r'|' +
    r'// See LICENSE for details.' +
    r'|' +
    r'// Use of this source code is governed by a BSD-style license that can be[\r\n]{1,2}' +
    r'// found in the LICENSE file\.' +
    r'|' +
    r'// See LICENSE or https://github\.com/tc39/test262/blob/master/LICENSE' +
    r')' + _BLANK_LINES, re.IGNORECASE)


def yaml_attr_parser(test_record, attrs, name, onerror=print):
    parsed = yaml_load(attrs)
    if parsed is None:
        onerror("Failed to parse yaml in name %s" % name)
        return

    for key in parsed:
        value = parsed[key]
        if key == "info":
            key = "commentary"
        test_record[key] = value

    if 'flags' in test_record:
        for flag in test_record['flags']:
            test_record[flag] = ""


def find_license(src):
    match = _LICENSE_PATTERN.search(src)
    if not match:
        return None

    return match.group(0)


def find_attrs(src):
    match = _YAML_PATTERN.search(src)
    if not match:
        return (None, None)

    return (match.group(0), match.group(1).strip())


def parse_test_record(src, name, onerror=print):
    # Find the license block.
    header = find_license(src)

    # Find the YAML frontmatter.
    (frontmatter, attrs) = find_attrs(src)

    # YAML frontmatter is required for all tests.
    if frontmatter is None:
        onerror("Missing frontmatter: %s" % name)

    # The license shuold be placed before the frontmatter and there shouldn't be
    # any extra content between the license and the frontmatter.
    if header is not None and frontmatter is not None:
        header_idx = src.index(header)
        frontmatter_idx = src.index(frontmatter)
        if header_idx > frontmatter_idx:
            onerror("Unexpected license after frontmatter: %s" % name)

        # Search for any extra test content, but ignore whitespace only or comment lines.
        extra = src[header_idx + len(header): frontmatter_idx]
        if extra and any(line.strip() and not line.lstrip().startswith("//") for line in extra.split("\n")):
            onerror(
                "Unexpected test content between license and frontmatter: %s" % name)

    # Remove the license and YAML parts from the actual test content.
    test = src
    if frontmatter is not None:
        test = test.replace(frontmatter, '')
    if header is not None:
        test = test.replace(header, '')

    test_record = {}
    test_record['header'] = header.strip() if header else ''
    test_record['test'] = test

    if attrs:
        yaml_attr_parser(test_record, attrs, name, onerror)

    # Report if the license block is missing in non-generated tests.
    if header is None and "generated" not in test_record and "hashbang" not in name:
        onerror("No license found in: %s" % name)

    return test_record


#######################################################################
# based on test262.py
#######################################################################

class Test262Error(Exception):
    def __init__(self, message):
        Exception.__init__(self)
        self.message = message


def report_error(error_string):
    raise Test262Error(error_string)


def build_options():
    result = optparse.OptionParser()
    result.add_option("--command", default=None,
                      help="The command-line to run")
    result.add_option("--tests", default=path.abspath('.'),
                      help="Path to the tests")
    result.add_option("--exclude-list", default=None,
                      help="Path to the excludelist.xml file")
    result.add_option("--cat", default=False, action="store_true",
                      help="Print packaged test code that would be run")
    result.add_option("--summary", default=False, action="store_true",
                      help="Print summary after running tests")
    result.add_option("--full-summary", default=False, action="store_true",
                      help="Print summary and test output after running tests")
    result.add_option("--strict_only", default=False, action="store_true",
                      help="Test only strict mode")
    result.add_option("--non_strict_only", default=False, action="store_true",
                      help="Test only non-strict mode")
    result.add_option("--unmarked_default", default="both",
                      help="default mode for tests of unspecified strictness")
    result.add_option("-j", "--job-count", default=None, action="store", type=int,
                      help="Number of parallel test jobs to run. In case of '0' cpu count is used.")
    result.add_option("--logname", help="Filename to save stdout to")
    result.add_option("--loglevel", default="warning",
                      help="sets log level to debug, info, warning, error, or critical")
    result.add_option("--print-handle", default="print",
                      help="Command to print from console")
    result.add_option("--list-includes", default=False, action="store_true",
                      help="List includes required by tests")
    result.add_option("--module-flag", default="-m",
                      help="List includes required by tests")
    return result


def validate_options(options):
    if not options.command:
        report_error("A --command must be specified.")
    if not path.exists(options.tests):
        report_error("Couldn't find test path '%s'" % options.tests)


def is_windows():
    actual_platform = platform.system()
    return (actual_platform == 'Windows') or (actual_platform == 'Microsoft')


class TempFile(object):

    def __init__(self, suffix="", prefix="tmp", text=False):
        self.suffix = suffix
        self.prefix = prefix
        self.text = text
        self.file_desc = None
        self.name = None
        self.is_closed = False
        self.open_file()

    def open_file(self):
        (self.file_desc, self.name) = tempfile.mkstemp(
            suffix=self.suffix,
            prefix=self.prefix,
            text=self.text)

    def write(self, string):
        os.write(self.file_desc, string)

    def read(self):
        file_desc = file(self.name)
        result = file_desc.read()
        file_desc.close()
        return result

    def close(self):
        if not self.is_closed:
            self.is_closed = True
            os.close(self.file_desc)

    def dispose(self):
        try:
            self.close()
            os.unlink(self.name)
        except OSError as exception:
            logging.error("Error disposing temp file: %s", str(exception))


class TestResult(object):

    def __init__(self, exit_code, stdout, stderr, case):
        self.exit_code = exit_code
        self.stdout = stdout
        self.stderr = stderr
        self.case = case

    def report_outcome(self, long_format):
        name = self.case.get_name()
        mode = self.case.get_mode()
        if self.has_unexpected_outcome():
            if self.case.is_negative():
                print("=== %s passed in %s, but was expected to fail ===" % (name, mode))
                print("--- expected error: %s ---\n" % self.case.get_negative_type())
            else:
                if long_format:
                    print("=== %s failed in %s ===" % (name, mode))
                else:
                    print("%s in %s: " % (name, mode))
            self.write_output(sys.stdout)
            if long_format:
                print("===")
        elif self.case.is_negative():
            print("%s failed in %s as expected" % (name, mode))
        else:
            print("%s passed in %s" % (name, mode))

    def write_output(self, target):
        out = self.stdout.strip()
        if out:
            target.write("--- output --- \n %s" % out)
        error = self.stderr.strip()
        if error:
            target.write("--- errors ---  \n %s" % error)

        target.write("\n--- exit code: %d ---\n" % self.exit_code)

    def has_failed(self):
        return self.exit_code != 0

    def async_has_failed(self):
        return 'Test262:AsyncTestComplete' not in self.stdout

    def has_unexpected_outcome(self):
        if self.case.is_async_test():
            return self.async_has_failed() or self.has_failed()
        elif self.case.is_negative():
            return not (self.has_failed() and self.case.negative_match(self.get_error_output()))

        return self.has_failed()

    def get_error_output(self):
        if self.stderr:
            return self.stderr
        return self.stdout


class TestCase(object):

    def __init__(self, suite, name, full_path, strict_mode, command_template, module_flag):
        self.suite = suite
        self.name = name
        self.full_path = full_path
        self.strict_mode = strict_mode
        with open(self.full_path) as file_desc:
            self.contents = file_desc.read()
        test_record = parse_test_record(self.contents, name)
        self.test = test_record["test"]
        del test_record["test"]
        del test_record["header"]
        test_record.pop("commentary", None)    # do not throw if missing
        self.test_record = test_record
        self.command_template = command_template
        self.module_flag = module_flag

        self.validate()

    def negative_match(self, stderr):
        neg = re.compile(self.get_negative_type())
        return re.search(neg, stderr)

    def get_negative(self):
        if not self.is_negative():
            return None
        return self.test_record["negative"]

    def get_negative_type(self):
        negative = self.get_negative()
        if isinstance(negative, dict) and "type" in negative:
            return negative["type"]
        return negative

    def get_negative_phase(self):
        negative = self.get_negative()
        return negative and "phase" in negative and negative["phase"]

    def get_name(self):
        return path.join(*self.name)

    def get_mode(self):
        if self.strict_mode:
            return "strict mode"
        return "non-strict mode"

    def get_path(self):
        return self.name

    def is_negative(self):
        return 'negative' in self.test_record

    def is_only_strict(self):
        return 'onlyStrict' in self.test_record

    def is_no_strict(self):
        return 'noStrict' in self.test_record or self.is_raw()

    def is_raw(self):
        return 'raw' in self.test_record

    def is_async_test(self):
        return 'async' in self.test_record or '$DONE' in self.test

    def is_module(self):
        return 'module' in self.test_record

    def get_include_list(self):
        if self.test_record.get('includes'):
            return self.test_record['includes']
        return []

    def get_additional_includes(self):
        return '\n'.join([self.suite.get_include(include) for include in self.get_include_list()])

    def get_source(self):
        if self.is_raw():
            return self.test

        source = self.suite.get_include("sta.js") + \
            self.suite.get_include("assert.js")

        if self.is_async_test():
            source = source + \
                self.suite.get_include("timer.js") + \
                self.suite.get_include("doneprintHandle.js").replace(
                    'print', self.suite.print_handle)

        source = source + \
            self.get_additional_includes() + \
            self.test + '\n'

        if self.get_negative_phase() == "early":
            source = ("throw 'Expected an early error, but code was executed.';\n" +
                      source)

        if self.strict_mode:
            source = '"use strict";\nvar strict_mode = true;\n' + source
        else:
            # add comment line so line numbers match in both strict and non-strict version
            source = '//"no strict";\nvar strict_mode = false;\n' + source

        return source

    @staticmethod
    def instantiate_template(template, params):
        def get_parameter(match):
            key = match.group(1)
            return params.get(key, match.group(0))

        return re.sub(r"\{\{(\w+)\}\}", get_parameter, template)

    @staticmethod
    def execute(command):
        if is_windows():
            args = '%s' % command
        else:
            args = command.split(" ")
        stdout = TempFile(prefix="test262-out-")
        stderr = TempFile(prefix="test262-err-")
        try:
            logging.info("exec: %s", str(args))
            process = subprocess.Popen(
                args,
                shell=False,
                stdout=stdout.file_desc,
                stderr=stderr.file_desc
            )
            timer = threading.Timer(TEST262_CASE_TIMEOUT, process.kill)
            timer.start()
            code = process.wait()
            timer.cancel()
            out = stdout.read()
            err = stderr.read()
        finally:
            stdout.dispose()
            stderr.dispose()
        return (code, out, err)

    def run_test_in(self, tmp):
        tmp.write(self.get_source())
        tmp.close()

        if self.is_module():
            arg = self.module_flag + ' ' + tmp.name
        else:
            arg = tmp.name

        command = TestCase.instantiate_template(self.command_template, {
            'path': arg
        })

        (code, out, err) = TestCase.execute(command)
        return TestResult(code, out, err, self)

    def run(self):
        tmp = TempFile(suffix=".js", prefix="test262-", text=True)
        try:
            result = self.run_test_in(tmp)
        finally:
            tmp.dispose()
        return result

    def print_source(self):
        print(self.get_source())

    def validate(self):
        flags = self.test_record.get("flags")
        phase = self.get_negative_phase()

        if phase not in [None, False, "parse", "early", "runtime", "resolution"]:
            raise TypeError("Invalid value for negative phase: " + phase)

        if not flags:
            return

        if 'raw' in flags:
            if 'noStrict' in flags:
                raise TypeError("The `raw` flag implies the `noStrict` flag")
            elif 'onlyStrict' in flags:
                raise TypeError(
                    "The `raw` flag is incompatible with the `onlyStrict` flag")
            elif self.get_include_list():
                raise TypeError(
                    "The `raw` flag is incompatible with the `includes` tag")


def pool_init():
    """Ignore CTRL+C in the worker process."""
    signal.signal(signal.SIGINT, signal.SIG_IGN)


def test_case_run_process(case):
    return case.run()


class ProgressIndicator(object):

    def __init__(self, count):
        self.count = count
        self.succeeded = 0
        self.failed = 0
        self.failed_tests = []

    def has_run(self, result):
        result.report_outcome(True)
        if result.has_unexpected_outcome():
            self.failed += 1
            self.failed_tests.append(result)
        else:
            self.succeeded += 1


def make_plural(num):
    if num == 1:
        return (num, "")
    return (num, "s")


def percent_format(partial, total):
    return "%i test%s (%.1f%%)" % (make_plural(partial) +
                                   ((100.0 * partial)/total,))


class TestSuite(object):

    def __init__(self, options):
        self.test_root = path.join(options.tests, 'test')
        self.lib_root = path.join(options.tests, 'harness')
        self.strict_only = options.strict_only
        self.non_strict_only = options.non_strict_only
        self.unmarked_default = options.unmarked_default
        self.print_handle = options.print_handle
        self.include_cache = {}
        self.exclude_list_path = options.exclude_list
        self.module_flag = options.module_flag
        self.logf = None

    def _load_excludes(self):
        if self.exclude_list_path and os.path.exists(self.exclude_list_path):
            xml_document = xml.dom.minidom.parse(self.exclude_list_path)
            xml_tests = xml_document.getElementsByTagName("test")
            return {x.getAttribute("id") for x in xml_tests}

        return set()

    def validate(self):
        if not path.exists(self.test_root):
            report_error("No test repository found")
        if not path.exists(self.lib_root):
            report_error("No test library found")

    @staticmethod
    def is_hidden(test_path):
        return test_path.startswith('.') or test_path == 'CVS'

    @staticmethod
    def is_test_case(test_path):
        return test_path.endswith('.js') and not test_path.endswith('_FIXTURE.js')

    @staticmethod
    def should_run(rel_path, tests):
        if not tests:
            return True
        for test in tests:
            if test in rel_path:
                return True
        return False

    def get_include(self, name):
        if not name in self.include_cache:
            static = path.join(self.lib_root, name)
            if path.exists(static):
                with open(static) as file_desc:
                    contents = file_desc.read()
                    contents = re.sub(r'\r\n', '\n', contents)
                    self.include_cache[name] = contents + "\n"
            else:
                report_error("Can't find: " + static)
        return self.include_cache[name]

    def enumerate_tests(self, tests, command_template):
        exclude_list = self._load_excludes()

        logging.info("Listing tests in %s", self.test_root)
        cases = []
        for root, dirs, files in os.walk(self.test_root):
            for hidden_dir in [x for x in dirs if self.is_hidden(x)]:
                dirs.remove(hidden_dir)
            dirs.sort()
            for test_path in filter(TestSuite.is_test_case, sorted(files)):
                full_path = path.join(root, test_path)
                if full_path.startswith(self.test_root):
                    rel_path = full_path[len(self.test_root)+1:]
                else:
                    logging.warning("Unexpected path %s", full_path)
                    rel_path = full_path
                if self.should_run(rel_path, tests):
                    basename = path.basename(full_path)[:-3]
                    name = rel_path.split(path.sep)[:-1] + [basename]
                    if rel_path in exclude_list:
                        print('Excluded: ' + rel_path)
                    else:
                        if not self.non_strict_only:
                            strict_case = TestCase(self, name, full_path, True, command_template, self.module_flag)
                            if not strict_case.is_no_strict():
                                if strict_case.is_only_strict() or self.unmarked_default in ['both', 'strict']:
                                    cases.append(strict_case)
                        if not self.strict_only:
                            non_strict_case = TestCase(self, name, full_path, False, command_template, self.module_flag)
                            if not non_strict_case.is_only_strict():
                                if non_strict_case.is_no_strict() or self.unmarked_default in ['both', 'non_strict']:
                                    cases.append(non_strict_case)
        logging.info("Done listing tests")
        return cases

    def print_summary(self, progress, logfile):

        def write(string):
            if logfile:
                self.logf.write(string + "\n")
            print(string)

        print("")
        write("=== Summary ===")
        count = progress.count
        succeeded = progress.succeeded
        failed = progress.failed
        write(" - Ran %i test%s" % make_plural(count))
        if progress.failed == 0:
            write(" - All tests succeeded")
        else:
            write(" - Passed " + percent_format(succeeded, count))
            write(" - Failed " + percent_format(failed, count))
            positive = [c for c in progress.failed_tests if not c.case.is_negative()]
            negative = [c for c in progress.failed_tests if c.case.is_negative()]
            if positive:
                print("")
                write("Failed Tests")
                for result in positive:
                    write("  %s in %s" % (result.case.get_name(), result.case.get_mode()))
            if negative:
                print("")
                write("Expected to fail but passed ---")
                for result in negative:
                    write("  %s in %s" % (result.case.get_name(), result.case.get_mode()))

    def print_failure_output(self, progress, logfile):
        for result in progress.failed_tests:
            if logfile:
                self.write_log(result)
            print("")
            result.report_outcome(False)

    def run(self, command_template, tests, print_summary, full_summary, logname, job_count=1):
        if not "{{path}}" in command_template:
            command_template += " {{path}}"
        cases = self.enumerate_tests(tests, command_template)
        if not cases:
            report_error("No tests to run")
        progress = ProgressIndicator(len(cases))
        if logname:
            self.logf = open(logname, "w")

        if job_count == 1:
            for case in cases:
                result = case.run()
                if logname:
                    self.write_log(result)
                progress.has_run(result)
        else:
            if job_count == 0:
                job_count = None # uses multiprocessing.cpu_count()

            pool = multiprocessing.Pool(processes=job_count, initializer=pool_init)
            try:
                for result in pool.imap(test_case_run_process, cases):
                    if logname:
                        self.write_log(result)
                    progress.has_run(result)
            except KeyboardInterrupt:
                pool.terminate()
                pool.join()

        if print_summary:
            self.print_summary(progress, logname)
            if full_summary:
                self.print_failure_output(progress, logname)
            else:
                print("")
                print("Use --full-summary to see output from failed tests")
        print("")
        return progress.failed

    def write_log(self, result):
        name = result.case.get_name()
        mode = result.case.get_mode()
        if result.has_unexpected_outcome():
            if result.case.is_negative():
                self.logf.write(
                    "=== %s passed in %s, but was expected to fail === \n" % (name, mode))
                self.logf.write("--- expected error: %s ---\n" % result.case.GetNegativeType())
                result.write_output(self.logf)
            else:
                self.logf.write("=== %s failed in %s === \n" % (name, mode))
                result.write_output(self.logf)
            self.logf.write("===\n")
        elif result.case.is_negative():
            self.logf.write("%s failed in %s as expected \n" % (name, mode))
        else:
            self.logf.write("%s passed in %s \n" % (name, mode))

    def print_source(self, tests):
        cases = self.enumerate_tests(tests, "")
        if cases:
            cases[0].print_source()

    def list_includes(self, tests):
        cases = self.enumerate_tests(tests, "")
        includes_dict = Counter()
        for case in cases:
            includes = case.get_include_list()
            includes_dict.update(includes)

        print(includes_dict)


def main():
    code = 0
    parser = build_options()
    (options, args) = parser.parse_args()
    validate_options(options)

    test_suite = TestSuite(options)

    test_suite.validate()
    if options.loglevel == 'debug':
        logging.basicConfig(level=logging.DEBUG)
    elif options.loglevel == 'info':
        logging.basicConfig(level=logging.INFO)
    elif options.loglevel == 'warning':
        logging.basicConfig(level=logging.WARNING)
    elif options.loglevel == 'error':
        logging.basicConfig(level=logging.ERROR)
    elif options.loglevel == 'critical':
        logging.basicConfig(level=logging.CRITICAL)

    if options.cat:
        test_suite.print_source(args)
    elif options.list_includes:
        test_suite.list_includes(args)
    else:
        code = test_suite.run(options.command, args,
                              options.summary or options.full_summary,
                              options.full_summary,
                              options.logname,
                              options.job_count)
    return code


if __name__ == '__main__':
    try:
        sys.exit(main())
    except Test262Error as exception:
        print("Error: %s" % exception.message)
        sys.exit(1)
