# 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.

"""
Runs tempest tests

This command is used for running the tempest tests

Test Selection
==============
Tempest run has several options:

* ``--regex/-r``: This is a selection regex like what stestr uses. It will run
  any tests that match on re.match() with the regex
* ``--smoke/-s``: Run all the tests tagged as smoke
* ``--exclude-regex``: It allows to do simple test exclusion via passing a
  rejection/exclude regexp

There are also the ``--exclude-list`` and ``--include-list`` options that
let you pass a filepath to tempest run with the file format being a line
separated regex, with '#' used to signify the start of a comment on a line.
For example::

    # Regex file
    ^regex1 # Match these tests
    .*regex2 # Match those tests

These arguments are just passed into stestr, you can refer to the stestr
selection docs for more details on how these operate:
http://stestr.readthedocs.io/en/latest/MANUAL.html#test-selection

You can also use the ``--list-tests`` option in conjunction with selection
arguments to list which tests will be run.

You can also use the ``--load-list`` option that lets you pass a filepath to
tempest run with the file format being in a non-regex format, similar to the
tests generated by the ``--list-tests`` option. You can specify target tests
by removing unnecessary tests from a list file which is generated from
``--list-tests`` option.

You can also use ``--worker-file`` option that let you pass a filepath to a
worker yaml file, allowing you to manually schedule the tests run.
For example, you can setup a tempest run with
different concurrences to be used with different regexps.
An example of worker file is showed below::

    # YAML Worker file
    - worker:
      # you can have more than one regex per worker
      - tempest.api.*
      - neutron_tempest_tests
    - worker:
      - tempest.scenario.*

This will run test matching with 'tempest.api.*' and 'neutron_tempest_tests'
against worker 1. Run tests matching with 'tempest.scenario.*' under worker 2.

You can mix manual scheduling with the standard scheduling mechanisms by
concurrency field on a worker. For example::

    # YAML Worker file
    - worker:
      # you can have more than one regex per worker
      - tempest.api.*
      - neutron_tempest_tests
      concurrency: 3
    - worker:
      - tempest.scenario.*
      concurrency: 2

This will run tests matching with 'tempest.scenario.*' against 2 workers.

This worker file is passed into stestr. For some more details on how it
operates please refer to the stestr scheduling docs:
https://stestr.readthedocs.io/en/stable/MANUAL.html#test-scheduling

Test Execution
==============
There are several options to control how the tests are executed. By default
tempest will run in parallel with a worker for each CPU present on the machine.
If you want to adjust the number of workers use the ``--concurrency`` option
and if you want to run tests serially use ``--serial/-t``

Running with Workspaces
-----------------------
Tempest run enables you to run your tempest tests from any setup tempest
workspace it relies on you having setup a tempest workspace with either the
``tempest init`` or ``tempest workspace`` commands. Then using the
``--workspace`` CLI option you can specify which one of your workspaces you
want to run tempest from. Using this option you don't have to run Tempest
directly with you current working directory being the workspace, Tempest will
take care of managing everything to be executed from there.

Running from Anywhere
---------------------
Tempest run provides you with an option to execute tempest from anywhere on
your system. You are required to provide a config file in this case with the
``--config-file`` option. When run tempest will create a .stestr
directory and a .stestr.conf file in your current working directory. This way
you can use stestr commands directly to inspect the state of the previous run.

Test Output
===========
By default tempest run's output to STDOUT will be generated using the
subunit-trace output filter. But, if you would prefer a subunit v2 stream be
output to STDOUT use the ``--subunit`` flag

Combining Runs
==============

There are certain situations in which you want to split a single run of tempest
across 2 executions of tempest run. (for example to run part of the tests
serially and others in parallel) To accomplish this but still treat the results
as a single run you can leverage the ``--combine`` option which will append
the current run's results with the previous runs.
"""

import os
import sys

from cliff import command
from oslo_log import log
from oslo_serialization import jsonutils as json
from stestr import commands

from tempest import clients
from tempest.cmd import cleanup_service
from tempest.cmd import init
from tempest.cmd import workspace
from tempest.common import credentials_factory as credentials
from tempest import config

CONF = config.CONF
SAVED_STATE_JSON = "saved_state.json"

LOG = log.getLogger(__name__)


class TempestRun(command.Command):

    def _set_env(self, config_file=None):
        if config_file:
            if os.path.exists(os.path.abspath(config_file)):
                CONF.set_config_path(os.path.abspath(config_file))
            else:
                raise FileNotFoundError(
                    "Config file: %s doesn't exist" % config_file)

        # NOTE(mtreinish): This is needed so that stestr doesn't gobble up any
        # stacktraces on failure.
        if 'TESTR_PDB' in os.environ:
            return
        else:
            os.environ["TESTR_PDB"] = ""
        # NOTE(dims): most of our .stestr.conf try to test for PYTHON
        # environment variable and fall back to "python", under python3
        # if it does not exist. we should set it to the python3 executable
        # to deal with this situation better for now.
        if 'PYTHON' not in os.environ:
            os.environ['PYTHON'] = sys.executable

    def _create_stestr_conf(self):
        top_level_path = os.path.dirname(os.path.dirname(__file__))
        discover_path = os.path.join(top_level_path, 'test_discover')
        file_contents = init.STESTR_CONF % (discover_path, top_level_path)
        with open('.stestr.conf', 'w+') as stestr_conf_file:
            stestr_conf_file.write(file_contents)

    def take_action(self, parsed_args):
        if parsed_args.config_file:
            self._set_env(parsed_args.config_file)
        else:
            self._set_env()
        # Workspace execution mode
        if parsed_args.workspace:
            workspace_mgr = workspace.WorkspaceManager(
                parsed_args.workspace_path)
            path = workspace_mgr.get_workspace(parsed_args.workspace)
            if not path:
                sys.exit(
                    "The %r workspace isn't registered in "
                    "%r. Use 'tempest init' to "
                    "register the workspace." %
                    (parsed_args.workspace, workspace_mgr.path))
            os.chdir(path)
            if not os.path.isfile('.stestr.conf'):
                self._create_stestr_conf()
        # local execution with config file mode
        elif parsed_args.config_file and not os.path.isfile('.stestr.conf'):
            self._create_stestr_conf()
        elif not os.path.isfile('.stestr.conf'):
            print("No .stestr.conf file was found for local execution")
            sys.exit(2)
        if parsed_args.state:
            self._init_state()

        regex = self._build_regex(parsed_args)

        # temporary method for parsing deprecated and new stestr options
        # and showing warning messages in order to make the transition
        # smoother for all tempest consumers
        # TODO(kopecmartin) remove this after stestr>=3.1.0 is used
        # in all supported OpenStack releases
        def parse_dep(old_o, old_v, new_o, new_v):
            ret = ''
            if old_v:
                LOG.warning("'%s' option is deprecated, use '%s' instead "
                            "which is functionally equivalent. Right now "
                            "Tempest still supports this option for "
                            "backward compatibility, however, it will be "
                            "removed soon.",
                            old_o, new_o)
                ret = old_v
            if old_v and new_v:
                # both options are specified
                LOG.warning("'%s' and '%s' are specified at the same time, "
                            "'%s' takes precedence over '%s'",
                            new_o, old_o, new_o, old_o)
            if new_v:
                ret = new_v
            return ret
        ex_regex = parse_dep('--black-regex', parsed_args.black_regex,
                             '--exclude-regex', parsed_args.exclude_regex)
        ex_list = parse_dep('--blacklist-file', parsed_args.blacklist_file,
                            '--exclude-list', parsed_args.exclude_list)
        in_list = parse_dep('--whitelist-file', parsed_args.whitelist_file,
                            '--include-list', parsed_args.include_list)

        return_code = 0
        if parsed_args.list_tests:
            try:
                return_code = commands.list_command(
                    filters=regex, include_list=in_list,
                    exclude_list=ex_list, exclude_regex=ex_regex)
            except TypeError:
                # exclude_list, include_list and exclude_regex are defined only
                # in stestr >= 3.1.0, this except block catches the case when
                # tempest is executed with an older stestr
                return_code = commands.list_command(
                    filters=regex, whitelist_file=in_list,
                    blacklist_file=ex_list, black_regex=ex_regex)

        else:
            serial = not parsed_args.parallel
            params = {
                'filters': regex, 'subunit_out': parsed_args.subunit,
                'serial': serial, 'concurrency': parsed_args.concurrency,
                'worker_path': parsed_args.worker_file,
                'load_list': parsed_args.load_list,
                'combine': parsed_args.combine
            }
            try:
                return_code = commands.run_command(
                    **params, exclude_list=ex_list,
                    include_list=in_list, exclude_regex=ex_regex)
            except TypeError:
                # exclude_list, include_list and exclude_regex are defined only
                # in stestr >= 3.1.0, this except block catches the case when
                # tempest is executed with an older stestr
                return_code = commands.run_command(
                    **params, blacklist_file=ex_list,
                    whitelist_file=in_list, black_regex=ex_regex)
            if parsed_args.slowest:
                commands.slowest_command()
            if return_code > 0:
                sys.exit(return_code)
        return return_code

    def get_description(self):
        return 'Run tempest'

    def _init_state(self):
        print("Initializing saved state.")
        data = {}
        self.global_services = cleanup_service.get_global_cleanup_services()
        self.admin_mgr = clients.Manager(
            credentials.get_configured_admin_credentials())
        admin_mgr = self.admin_mgr
        kwargs = {'data': data,
                  'is_dry_run': False,
                  'saved_state_json': data,
                  'is_preserve': False,
                  'is_save_state': True}
        for service in self.global_services:
            svc = service(admin_mgr, **kwargs)
            svc.run()

        with open(SAVED_STATE_JSON, 'w+') as f:
            f.write(json.dumps(data, sort_keys=True,
                               indent=2, separators=(',', ': ')))

    def get_parser(self, prog_name):
        parser = super(TempestRun, self).get_parser(prog_name)
        parser = self._add_args(parser)
        return parser

    def _add_args(self, parser):
        # workspace args
        parser.add_argument('--workspace', default=None,
                            help='Name of tempest workspace to use for running'
                                 ' tests. You can see a list of workspaces '
                                 'with tempest workspace list')
        parser.add_argument('--workspace-path', default=None,
                            dest='workspace_path',
                            help="The path to the workspace file, the default "
                                 "is ~/.tempest/workspace.yaml")
        # Configuration flags
        parser.add_argument('--config-file', default=None, dest='config_file',
                            help='Configuration file to run tempest with')
        # test selection args
        regex = parser.add_mutually_exclusive_group()
        regex.add_argument('--smoke', '-s', action='store_true',
                           help="Run the smoke tests only")
        regex.add_argument('--regex', '-r', default='',
                           help='A normal stestr selection regex used to '
                                'specify a subset of tests to run')
        parser.add_argument('--black-regex', dest='black_regex',
                            help='DEPRECATED: This option is deprecated and '
                                 'will be removed soon, use --exclude-regex '
                                 'which is functionally equivalent. If this '
                                 'is specified at the same time as '
                                 '--exclude-regex, this flag will be ignored '
                                 'and --exclude-regex will be used')
        parser.add_argument('--exclude-regex', dest='exclude_regex',
                            help='A regex to exclude tests that match it')
        parser.add_argument('--whitelist-file', '--whitelist_file',
                            help='DEPRECATED: This option is deprecated and '
                                 'will be removed soon, use --include-list '
                                 'which is functionally equivalent. If this '
                                 'is specified at the same time as '
                                 '--include-list, this flag will be ignored '
                                 'and --include-list will be used')
        parser.add_argument('--include-list', '--include_list',
                            help="Path to an include file which contains the "
                                 "regex for tests to be included in tempest "
                                 "run, this file contains a separate regex on "
                                 "each newline.")
        parser.add_argument('--blacklist-file', '--blacklist_file',
                            help='DEPRECATED: This option is deprecated and '
                                 'will be removed soon, use --exclude-list '
                                 'which is functionally equivalent. If this '
                                 'is specified at the same time as '
                                 '--exclude-list, this flag will be ignored '
                                 'and --exclude-list will be used')
        parser.add_argument('--exclude-list', '--exclude_list',
                            help='Path to an exclude file which contains the '
                                 'regex for tests to be excluded in tempest '
                                 'run, this file contains a separate regex on '
                                 'each newline.')
        parser.add_argument('--load-list', '--load_list',
                            help='Path to a non-regex whitelist file, '
                                 'this file contains a separate test '
                                 'on each newline. This command '
                                 'supports files created by the tempest '
                                 'run ``--list-tests`` command')
        parser.add_argument('--worker-file', '--worker_file',
                            help='Optional path to a worker file. This file '
                            'contains each worker configuration to be '
                            'used to schedule the tests run')
        # list only args
        parser.add_argument('--list-tests', '-l', action='store_true',
                            help='List tests',
                            default=False)
        # execution args
        parser.add_argument('--concurrency', '-w',
                            type=int, default=0,
                            help="The number of workers to use, defaults to "
                                 "the number of cpus")
        parallel = parser.add_mutually_exclusive_group()
        parallel.add_argument('--parallel', dest='parallel',
                              action='store_true',
                              help='Run tests in parallel (this is the'
                                   ' default)')
        parallel.add_argument('--serial', '-t', dest='parallel',
                              action='store_false',
                              help='Run tests serially')
        parser.add_argument('--save-state', dest='state',
                            action='store_true',
                            help="To save the state of the cloud before "
                                 "running tempest.")
        # output args
        parser.add_argument("--subunit", action='store_true',
                            help='Enable subunit v2 output')
        parser.add_argument("--combine", action='store_true',
                            help='Combine the output of this run with the '
                                 "previous run's as a combined stream in the "
                                 "stestr repository after it finish")
        parser.add_argument('--slowest', action='store_true',
                            help='Show the longest running tests in the '
                                 'stestr repository after it finishes')

        parser.set_defaults(parallel=True)
        return parser

    def _build_regex(self, parsed_args):
        regex = None
        if parsed_args.smoke:
            regex = ['smoke']
        elif parsed_args.regex:
            regex = parsed_args.regex.split()
        return regex
