#!/usr/bin/env python
#
# Public Domain 2014-present MongoDB, Inc.
# Public Domain 2008-2014 WiredTiger, Inc.
#
# This is free and unencumbered software released into the public domain.
#
# Anyone is free to copy, modify, publish, use, compile, sell, or
# distribute this software, either in source code form or as a compiled
# binary, for any purpose, commercial or non-commercial, and by any
# means.
#
# In jurisdictions that recognize copyright laws, the author or authors
# of this software dedicate any and all copyright interest in the
# software to the public domain. We make this dedication for the benefit
# of the public at large and to the detriment of our heirs and
# successors. We intend this dedication to be an overt act of
# relinquishment in perpetuity of all present and future rights to this
# software under copyright law.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.

from __future__ import print_function
import os, re, subprocess, sys
from run import wt_builddir
from wttest import WiredTigerTestCase

# suite_subprocess.py
#    Run a subprocess within the test suite
# Used as a 'mixin' class along with a WiredTigerTestCase class
class suite_subprocess:
    subproc = None

    def has_error_in_file(self, filename):
        """
        Return whether the file contains 'ERROR'.
        WT utilities issue a 'WT_ERROR' output string upon error.
        """
        with open(filename, 'r') as f:
            for line in f:
                if 'ERROR' in line:
                    return True
        return False

    def check_no_error_in_file(self, filename, match='ERROR'):
        """
        Raise an error and show output context if the file contains 'ERROR'.
        WT utilities issue a 'WT_ERROR' output string upon error.
        """
        lines = []
        hasError = False
        hasPrevious = False  # do we need to prefix an ellipsis?
        hasNext = False  # do we need to suffix an ellipsis?
        with open(filename, 'r') as f:
            for line in f:
                lines.append(line)
                hasError = hasError or match in line
                if hasError:
                    if len(lines) > 10:
                        hasNext = True
                        break
                else:
                    if len(lines) > 5:
                        lines.pop(0)
                        hasPrevious = True
        if hasError:
            print('**************** ' + match + ' in output file: ' + filename + ' ****************')
            if hasPrevious:
                print('...')
            for line in lines:
                print(line, end=' ')
            if hasNext:
                print('...')
            print('********************************')
            self.fail('ERROR found in output file: ' + filename)

    # If the string is of the form '/.../', then return just the embedded
    # pattern, otherwise, return None
    def convert_to_pattern(self, s):
        if len(s) >= 2 and s[0] == '/' and s[-1] == '/':
            return s[1:-1]
        else:
            return None

    def check_file_content(self, filename, expect):
        with open(filename, 'r') as f:
            got = f.read(len(expect) + 100)
            self.assertEqual(got, expect, filename + ': does not contain expected:\n\'' + expect + '\', but contains:\n\'' + got + '\'.')

    # Check contents of the file against a provided checklist. Expected is used as a bool to either
    # ensure checklist is included or ensure the checklist is not included in the file.
    def check_file_contains_one_of(self, filename, checklist, expected):
        """
        Check that the file contains the expected string in the first 100K bytes
        """
        maxbytes = 1024*100
        with open(filename, 'r') as f:
            got = f.read(maxbytes)
            found = False
            for expect in checklist:
                pat = self.convert_to_pattern(expect)
                if pat == None:
                    if expect in got:
                        found = True
                        if expected:
                            break
                        else:
                            self.fail("Did not expect: " + got)
                else:
                    if re.search(pat, got):
                        found = True
                        if expected:
                            break
                        else:
                            self.fail("Did not expect: " + got)
            if not found and expected:
                if len(checklist) == 1:
                    expect = '\'' + checklist[0] + '\''
                else:
                    expect = str(checklist)
                gotstr = '\'' + \
                    (got if len(got) < 1000 else (got[0:1000] + '...')) + '\''
                if len(got) >= maxbytes:
                    self.fail(filename + ': does not contain expected ' + expect + ', or output is too large, got ' + gotstr)
                else:
                    self.fail(filename + ': does not contain expected ' + expect + ', got ' + gotstr)

    def check_file_contains(self, filename, content):
        self.check_file_contains_one_of(filename, [content], True)

    def check_file_not_contains(self, filename, content):
        self.check_file_contains_one_of(filename, [content], False)

    def check_empty_file(self, filename):
        """
        Raise an error if the file is not empty
        """
        filesize = os.path.getsize(filename)
        if filesize > 0:
            with open(filename, 'r') as f:
                contents = f.read(1000)
                print('ERROR: ' + filename + ' expected to be empty, but contains:\n')
                print(contents + '...\n')
        self.assertEqual(filesize, 0, filename + ': expected to be empty')

    def check_non_empty_file(self, filename):
        """
        Raise an error if the file is empty
        """
        filesize = os.path.getsize(filename)
        if filesize == 0:
            print('ERROR: ' + filename + ' should not be empty (this command expected error output)')
        self.assertNotEqual(filesize, 0, filename + ': expected to not be empty')

    def verbose_env(self, envvar):
        return envvar + '=' + str(os.environ.get(envvar)) + '\n'

    def show_outputs(self, procargs, message, filenames):
        out = message + ': ' + \
              str(procargs) + '\n' + \
              self.verbose_env('PATH') + \
              self.verbose_env('LD_LIBRARY_PATH') + \
              self.verbose_env('DYLD_LIBRARY_PATH') + \
              self.verbose_env('PYTHONPATH') + \
              'output files follow:'
        WiredTigerTestCase.prout(out)
        for filename in filenames:
            maxbytes = 1024*100
            with open(filename, 'r') as f:
                contents = f.read(maxbytes)
                if len(contents) > 0:
                    if len(contents) >= maxbytes:
                        contents += '...\n'
                    sepline = '*' * 50 + '\n'
                    out = sepline + filename + '\n' + sepline + contents
                    WiredTigerTestCase.prout(out)

    # Run a method as a subprocess using the run.py machinery.
    # Return the process exit status and the the WiredTiger
    # home directory used by the subprocess.
    def run_subprocess_function(self, directory, funcname):
        testparts = funcname.split('.')
        if len(testparts) != 3:
            raise ValueError('bad function name "' + funcname +
                '", should be three part dotted name')
        topdir = os.path.dirname(self.buildDirectory())
        runscript = os.path.join(topdir, 'test', 'suite', 'run.py')
        procargs = [ sys.executable, runscript, '-p', '--dir', directory,
            funcname]

        # scenario_number is only set if we are running in a scenario
        try:
            scennum = self.scenario_number
            procargs.append('-s')
            procargs.append(str(scennum))
        except:
            scennum = 0

        returncode = -1
        os.makedirs(directory)

        # We cannot put the output/error files in the subdirectory, as
        # that will be cleared by the run.py script.
        with open("subprocess.err", "w") as wterr:
            with open("subprocess.out", "w") as wtout:
                returncode = subprocess.call(
                    procargs, stdout=wtout, stderr=wterr)
                if returncode != 0:
                    # This is not necessarily an error, the primary reason to
                    # run in a subprocess is that it may crash.
                    self.show_outputs(procargs,
                        "Warning: run_subprocess_function " + funcname + \
                        " returned error code " + str(returncode),
                        [ "subprocess.out", "subprocess.err" ])

        new_home_dir = os.path.join(directory,
            testparts[1] + '.' + str(scennum))
        return [ returncode, new_home_dir ]

    # Run the wt utility.
    def runWt(self, args, infilename=None,
        outfilename=None, errfilename=None, closeconn=True,
        reopensession=True, failure=False):

        # Close the connection to guarantee everything is flushed, and that
        # we can open it from another process.
        if closeconn:
            self.close_conn()

        wtoutname = outfilename or "wt.out"
        wterrname = errfilename or "wt.err"
        with open(wterrname, "w") as wterr:
            with open(wtoutname, "w") as wtout:
                # Prefer running the actual 'wt' binary rather than the
                # 'wt' script created by libtool. On OS/X with System Integrity
                # Protection enabled, running a shell script strips
                # environment variables needed to run 'wt'. There are
                # also test environments that work better with the binary.
                libs_wt = os.path.join(wt_builddir, ".libs", "wt")
                if os.path.isfile(libs_wt):
                    wtexe = libs_wt
                else:
                    wtexe = os.path.join(wt_builddir, "wt")
                procargs = [ wtexe ]
                if self._gdbSubprocess:
                    procargs = [ "gdb", "--args" ] + procargs
                elif self._lldbSubprocess:
                    procargs = [ "lldb", "--" ] + procargs
                procargs.extend(args)
                if self._gdbSubprocess:
                    infilepart = ""
                    if infilename != None:
                        infilepart = "<" + infilename + " "
                    print(str(procargs))
                    print("*********************************************")
                    print("**** Run 'wt' via: run " + \
                        " ".join(procargs[3:]) + infilepart + \
                        ">" + wtoutname + " 2>" + wterrname)
                    print("*********************************************")
                    returncode = subprocess.call(procargs)
                elif self._lldbSubprocess:
                    infilepart = ""
                    if infilename != None:
                        infilepart = "<" + infilename + " "
                    print(str(procargs))
                    print("*********************************************")
                    print("**** Run 'wt' via: run " + \
                        " ".join(procargs[3:]) + infilepart + \
                        ">" + wtoutname + " 2>" + wterrname)
                    print("*********************************************")
                    returncode = subprocess.call(procargs)
                elif infilename:
                    with open(infilename, "r") as wtin:
                        returncode = subprocess.call(
                            procargs, stdin=wtin, stdout=wtout, stderr=wterr)
                else:
                    returncode = subprocess.call(
                        procargs, stdout=wtout, stderr=wterr)
        if failure:
            if returncode == 0:
                self.show_outputs(procargs,
                    "ERROR: wt command expected failure, got success",
                    [wtoutname, wterrname])
            self.assertNotEqual(returncode, 0,
                'expected failure: "' + \
                str(procargs) + '": exited ' + str(returncode))
        else:
            if returncode != 0:
                self.show_outputs(procargs,
                    "ERROR: wt command expected success, got failure",
                    [wtoutname, wterrname])
            self.assertEqual(returncode, 0,
                'expected success: "' + \
                str(procargs) + '": exited ' + str(returncode))
        if errfilename == None:
            self.check_empty_file(wterrname)
        if outfilename == None:
            self.check_empty_file(wtoutname)

        # Reestablish the connection if needed
        if reopensession and closeconn:
            self.open_conn()
