# Copyright (c) 2013-2020, SIB - Swiss Institute of Bioinformatics and
#                          Biozentrum - University of Basel
# 
# 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.


"""
unittest.TestCase class providing common functionality for testing actions.
"""
import unittest
import os
import subprocess
import ost

# set verbosity level here, is propagated to all others
ost.PushVerbosityLevel(2)

class ActionTestCase(unittest.TestCase):
    """
    Class to help developing actions. Comes with a lot of convenience wrappers
    around what should be tested and serves as a recorder for test calls...
    just for in two years when you come back to rewrite the whole action...

    While inheriting this class, :attr:`pm_action` needs to be defined.
    Otherwise the whole idea does not work.

    .. attribute:: pm_bin

      This is the path of the ``pm`` binary. Automatically set by calling
      :meth:`~ActionTestCase.__init__` inside the initialisation of your class.

      :type: :class:`str`

    .. attribute:: pm_action

      The action to be tested. Needs to be set by your initialisation routine,
      **after** calling :meth:`~ActionTestCase.__init__` from here. Skip the
      "pm-" in front of the action name.

      :type: :class:`str`
    """
    def __init__(self, *args, **kwargs):
        """
        Convenience stuff for action testing.
        """
        # Determining the pm binary to be called. Going hard-coded is a bad
        # thing. But this is a unit test and we know where we are as all unit
        # tests are run in "tests/MODULENAME" within build-folder.
        bld_dir = os.path.abspath(os.path.dirname(os.path.dirname(os.getcwd())))
        self.pm_bin = os.path.join(bld_dir, 'stage', 'bin', 'pm')
        self.pm_action = None
        unittest.TestCase.__init__(self, *args, **kwargs)

    def RunAction(self, arguments, verbose=False):
        """
        Call an action, return the exit status (``$?`` shell variable). May be
        set to ``verbose`` to print the actions terminal output. The action to
        be executed needs to be stored in :attr:`pm_action` first.

        If in verbose mode, output to :file:`stdout` of the action will be
        printed first followed by :file:`stderr`.

        :param arguments: A list of arguments for the call.
        :type arguments: :class:`list`

        :param verbose: If ``True``, report output of the action.
        :type verbose: :class:`bool`

        :returns: The exit code of the action (:class:`int`).
        """
        if not self.pm_action:
            raise RuntimeError("A 'pm_action' attribute has to be defined by "+
                               "each subclass of 'test_actions.ActionTestCase'")

        # run the action with arguments, wait for the job to finish and capture
        # output
        cmd = [self.pm_bin, self.pm_action] + arguments
        job = subprocess.Popen(cmd,
                               stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        sout, serr = job.communicate()
        if verbose:
            ost.LogScript("stdout of '%s'" % ' '.join(cmd))
            ost.LogScript("------")
            lines = sout.splitlines()
            for out_l in lines:
                ost.LogScript(out_l)
            ost.LogScript("------")
            ost.LogError("stderr of '%s'" % ' '.join(cmd))
            ost.LogError("------")
            lines = serr.splitlines()
            for err_l in lines:
                ost.LogError(err_l)
            ost.LogError("------")
        return job.returncode

    def RunExitStatusTest(self, exit_code, arguments, verbose=False):
        """
        Run the action with given arguments and check the exit code.

        :param exit_code: The expected return code, ``$?`` in a shell.
        :type exit_code: :class:`int`

        :param arguments: A list of arguments for the call.
        :type arguments: :class:`list`

        :param verbose: If ``True``, report output of the action.
        :type verbose: :class:`bool`
        """
        exit_code_run = self.RunAction(arguments, verbose=verbose)
        self.assertEqual(exit_code, exit_code_run,
                         msg="Exit code of '%s' " % ' '.join([self.pm_bin,
                                                              self.pm_action]+
                                                             arguments)+
                         "is supposed to be '%d' " % exit_code+
                         "but returned as '%d'." % exit_code_run)

    def testPMExists(self):
        """
        This is an internal test, executed when the source code of the test
        class is run as unit test. Verifies that :attr:`pm_bin` is an existing
        file (also complains if a directory is found instead).
        """
        self.assertEqual(os.path.isfile(self.pm_bin), True,
                         msg="Could not find 'pm' bin at '%s', " % self.pm_bin+
                         "cannot proceed unit tests.")

if __name__ == "__main__":
    import sys
    from ost import testutils
    sys.dont_write_bytecode = True
    testutils.RunTests()

#  LocalWords:  attr meth ActionTestCase init str stdout stderr param bool
