#
# Copyright (c) 2009 Nokia Corporation and/or its subsidiary(-ies).
# All rights reserved.
# This component and the accompanying materials are made available
# under the terms of "Eclipse Public License v1.0"
# which accompanies this distribution, and is available
# at the URL "http://www.eclipse.org/legal/epl-v10.html".
#
# Initial Contributors:
# Nokia Corporation - initial contribution.
#
# Contributors:
#
# Description: 
#

import sys, os, re, unittest, shutil, zipfile, filecmp, subprocess

class BaseTestCase(unittest.TestCase):
    def set_modification_reference_time(self, path):
        """
        Set modification reference time for a subsequent call to assert_modified()
        or assert_not_modified().
        @param path: The path to use, can be a file or a directory.
        """
        if not hasattr(self, '_mod_refs'):
            self._mod_refs = {}
        
        if os.path.isdir(path):
            self._mod_refs[path] = self._get_dir_modtime_dict(path)
        elif os.path.isfile(path):
            self._mod_refs[path] = os.stat(path).st_mtime
        else:
            self.fail("'%s' does not exist" % path)
    
    def assert_modified(self, path):
        """
        Assert that a given file or directory has been modified since the last
        call to set_modification_reference_time() with the same path.
        """
        # This fails on linux for some reason, probably because the modification
        # timestamps are not accurate enough
        if sys.platform == "win32":
            self._assert_modification(path, assert_not_modified=False)
    
    def assert_not_modified(self, path):
        """
        Assert that a given file or directory has NOT been modified since the last
        call to set_modification_reference_time() with the same path.
        """
        self._assert_modification(path, assert_not_modified=True)
    
    def remove_if_exists(self, path_or_paths):
        """Remove files or directories if they exist.
        @param path_or_paths: The path to remove. Can also be a list of paths."""
        if isinstance(path_or_paths, list):
            paths = path_or_paths
        else:
            paths = [path_or_paths]
        
        for path in paths:
            if os.path.isdir(path):
                shutil.rmtree(path)
            elif os.path.isfile(path):
                os.remove(path)
    
    def create_dir(self, path):
        """Create the given directory if it doesn't exist."""
        if not os.path.exists(path):
            os.makedirs(path)
    
    def recreate_dir(self, path):
        """Remove the given directory if it exists, and recreate it."""
        if os.path.exists(path):
            shutil.rmtree(path)
        os.makedirs(path)
    
    def create_dir_for_file_path(self, path):
        """Create the directories for the given file"""
        dir = os.path.dirname(path)
        if dir != '' and not os.path.exists(dir):
            os.makedirs(dir)
    
    def assert_exists_and_contains_something(self, path):
        """
        Assert that the given path is a file or a directory and contains some data.
        """
        if os.path.isdir(path):
            if len(os.listdir(path)) == 0:
                self.fail("Path '%s' exists (is a directory) but does not contain anything)" % path)
        elif os.path.isfile(path):
            if os.stat(path).st_size == 0:
                self.fail("Path '%s' exists (is a file) but does not contain anything)" % path)
        else:
            self.fail("Path '%s' does not exist" % path)
    
    def assert_dir_contents_equal(self, dir1, dir2, ignore=[], custom_comparison_functions={}, current_root_dir=''):
        """
        Assert recursively that the contents of two directories are equal.
        @param ignore: List containing names that should be ignored in the comparison (e.g. '.svn').
            The entries can either be relative, e.g. 'file.txt', which would ignore 'file.txt'
            in any directory, or they can be absolute, e.g. '/some/dir/file.txt', which would
            ignore 'file.txt' only under 'some/dir/', relative to the comparison root.
        @param custom_comparison_functions: Dictionary containing custom comparison functions
            for files. Each entry in the dict should contain the following contents:
                Key: The relative path of the file under the directories, e.g.
                    'some/path/file.txt'
                Value: The function used to compare the file contents. The function should
                    take as parameters the raw binary data of the files, and should return
                    True if the contents are equal.
        @param current_root_dir: For internal use.
        """
        msg = "Directory contents are not equal ('%s' vs. '%s')\n" % (dir1, dir2)
        
        ignore_list = []
        for entry in ignore:
            if entry.startswith('/'):
                dirname, entryname = entry.rsplit('/', 1)
                dirname = dirname.lstrip('/')
                #print "dirname = %r" % dirname
                #print "entryname = %r" % entryname
                #print "current_root_dir = %r" % current_root_dir
                if dirname == current_root_dir.rstrip('/'):
                    ignore_list.append(entryname)
            else:
                ignore_list.append(entry)

        # Compare files with the custom comparison functions if necessary
        for path, func in custom_comparison_functions.iteritems():
            dirname  = os.path.dirname(path).replace('\\', '/')
            filename = os.path.basename(path)
            
            filepath1 = os.path.join(dir1, filename)
            filepath2 = os.path.join(dir2, filename)
            
            # Compare if the file is in the current path and they both exist
            if dirname == current_root_dir and \
                os.path.isfile(filepath1) and \
                os.path.isfile(filepath2):
                comp_result = func(
                    self.read_data_from_file(filepath1),
                    self.read_data_from_file(filepath2))
                if not comp_result:
                    # The files are not equal -> fail
                    self.fail(msg + "File '%s' differs" % filename)
                else:
                    # The files are equal -> ignore from dircmp comparison
                    ignore_list.append(filename)
        
        dcmp = filecmp.dircmp(dir1, dir2, ignore=ignore_list)
        self.assertEquals(0, len(dcmp.left_only), msg + "Files only on left: %s" % dcmp.left_only)
        self.assertEquals(0, len(dcmp.right_only), msg + "Files only on right: %s" % dcmp.right_only)
        self.assertEquals(0, len(dcmp.diff_files), msg + "Differing files: %s" % dcmp.diff_files)
        self.assertEquals(0, len(dcmp.funny_files), msg + "Funny files: %s" % dcmp.funny_files)
        # Recurse into sub-directories
        for d in dcmp.common_dirs:
            if current_root_dir:    cr = current_root_dir + '/' + d
            else:                   cr = d
            self.assert_dir_contents_equal(
                os.path.join(dir1, d), os.path.join(dir2, d),
                ignore, custom_comparison_functions, cr)
    
    def assert_file_contents_equal(self, file1, file2, ignore_patterns=[], ignore_endline_style=False):
        """
        Assert the the given two files exist and their contents are equal.
        @param ignore_patterns: List of regular expressions for portions of the
            file content to ignore in the comparison. The ignored parts are
            deleted from the files before actual comparison.
        @param ignore_endline_style: If True, the endline style (CRLF or LF) is
            ignored during the comparison.
        """
        self.assertTrue(os.path.exists(file1), "File '%s' does not exist!" % file1)
        self.assertTrue(os.path.exists(file2), "File '%s' does not exist!" % file2)
        
        data1 = self.read_data_from_file(file1)
        data2 = self.read_data_from_file(file2)
        
        def remove_ignored(data, pattern_list):
            for i, pattern in enumerate(pattern_list):
                data = re.sub(pattern, '{{{ignore_%d}}}' % i, data)
            return data
        data1 = remove_ignored(data1, ignore_patterns)
        data2 = remove_ignored(data2, ignore_patterns)
        
        if ignore_endline_style:
            data1 = data1.replace('\r\n', '\n')
            data2 = data2.replace('\r\n', '\n')
        
        import difflib
        difseq = difflib.ndiff(data1, data2)
        # take only the first ten rows of the difference
        difference = ''.join(list(difseq)[:10])
        if data1 != data2:
            if len(ignore_patterns) > 0:
                self.write_data_to_file(file1 + '.diff', ''.join(difseq))
                self.write_data_to_file(file1 + '.comparetemp', data1)
                self.write_data_to_file(file2 + '.comparetemp', data2)
                self.fail("Data of the files '%s' and '%s' are not equal\nSee *.comparetemp files for the actual data that was compared.\nDifference %s" % (file1, file2, difference))
            else:
                self.fail("Data of the files '%s' and '%s' are not equal\nDifference %s" % (file1, file2, difference))
    
    def assert_file_content_equals(self, filepath, expected_data, ignore_endline_style=False):
        """
        Assert that the content of the given file is equals to the given expected data.
        @param ignore_endline_style: If True, the endline style (CRLF or LF) is
            ignored during the comparison.
        """
        self.assertTrue(os.path.exists(filepath), "'%s' does not exist!" % filepath)
        self.assertTrue(os.path.isfile(filepath), "'%s' is not a file!" % filepath)
        
        f = open(filepath, "rb")
        try:        filedata = f.read()
        finally:    f.close()
        
        if ignore_endline_style:
            filedata = filedata.replace('\r\n', '\n')
            expected_data = expected_data.replace('\r\n', '\n')
        
        if filedata != expected_data:
            msg = ("The content of the file '%s' is not what was expected!\n" % filepath) +\
                  ("Expected: %r\nActual: %r" % (expected_data, filedata))
            self.fail(msg)
    
    def assert_file_contains(self, filepath, data, encoding=None, regexes=[]):
        """
        Assert that the given file contains the given text somewhere in its contents.
        @param filepath: Path to the file to check.
        @param data: The data the file is expected to contain.
        @param encoding: Encoding used to decode the contents of the file.
            If None, noe decoding is done.
        @param regexes: A list of regular expressions that are used to search for
            a match in the file data. If any of them doesn't find a match,
            the assertion fails.
        """
        self.assertTrue(os.path.exists(filepath), "'%s' does not exist!" % filepath)
        self.assertTrue(os.path.isfile(filepath), "'%s' is not a file!" % filepath)
        
        f = open(filepath, "rb")
        try:        filedata = f.read()
        finally:    f.close()
        
        if encoding is not None:
            filedata = filedata.decode(encoding)
        
        if not isinstance(data, list):
            data = [data]
        
        for entry in data:
            if not filedata.find(entry) != -1:
                self.fail("The file '%s' does not contain the data '%s'" % (filepath, entry))
        
        for regex in regexes:
            if re.search(regex, filedata, re.MULTILINE) is None:
                self.fail("The regex %r does not match anything in file '%s'" % (regex, filepath))

    def assert_file_does_not_contain(self, filepath, data, encoding=None):
        """
        Assert that the given file doesn't contain the given text somewhere in its contents.
        @param filepath: Path to the file to check.
        @param data: The data the file is expected to not contain.
        @param encoding: Encoding used to decode the contents of the file.
            If None, noe decoding is done.
        """
        self.assertTrue(os.path.exists(filepath), "'%s' does not exist!" % filepath)
        self.assertTrue(os.path.isfile(filepath), "'%s' is not a file!" % filepath)
        
        f = open(filepath, "rb")
        try:        filedata = f.read()
        finally:    f.close()
        
        if encoding is not None:
            filedata = filedata.decode(encoding)
        
        if not isinstance(data, list):
            data = [data]
        
        for entry in data:
            if not filedata.find(entry) == -1:
                self.fail("The file '%s' contains the data '%s'" % (filepath, entry))
    
    def read_data_from_file(self, path):
        """Read the raw binary data from the given file."""
        f = open(path, "rb")
        try:        return f.read()
        finally:    f.close()
    
    def read_data_from_zip_file(self, path, entry):
        """Read the raw binary data from the given ZIP file with the given ZIP entry."""
        zf = zipfile.ZipFile(path, "r")
        try:        return zf.read(entry)
        finally:    zf.close()
    
    def write_data_to_file(self, path, data):
        """Write raw binary data into the given file."""
        f = open(path, "wb")
        try:        f.write(data)
        finally:    f.close()
    
    def run_command(self, command, expected_return_code=0):
        """
        Run the given command, asserting that it returns the expected value.
        @param command: The command to run.
        @param expected_return_code: The expected return code. Can be None if the return
            code doesn't matter.
        @return: The command output.
        """
        # Using shell=True on windows uses
        #    cmd.exe /c <command>
        # to run the actual command, and if cmd.exe sees that the first
        # character in the command is ", it strips that and a trailing ".
        # For this reason we add quotes to the command to prevent e.g.
        #   "C:\some\command.cmd" --some-arg "xyz"
        # from becoming
        #   C:\some\command.cmd" --some-arg "xyz
        if sys.platform == 'win32' and command.startswith('"'):
            command = '"' + command + '"'
        
        p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True)
        out, err = p.communicate()
        if expected_return_code is not None:
            self.assertTrue(p.returncode == expected_return_code,
                            "Could not execute command (%s)\n"\
                            "Return code is not what was expected (expected %d, got %d)\n"\
                            "Output: \n%s" % (command, expected_return_code, p.returncode, out))
        return out
    
    def assert_problem_lists_equal(self, actual, expected, outdir):
        """
        Assert that two lists of api.Problem objects are equal.
        @param actual: The list of actual problems.
        @param expected: The list of expected problems.
        @param outdir: The directory where output is written if the assertion
            fails.
        """
        act = sorted([repr(p) for p in actual])
        exp = sorted([repr(p) for p in expected])
        if act != exp:
            if not os.path.exists(outdir):
                os.makedirs(outdir)
            self.write_data_to_file(os.path.join(outdir, 'actual.txt'), os.linesep.join(act))
            self.write_data_to_file(os.path.join(outdir, 'expected.txt'), os.linesep.join(exp))
            self.fail("Problem lists not equal, see the files in '%s'" % outdir)
    
    def assert_problem_list_equals_expected(self, actual, expected_file, outdir):
        """
        Assert that the given list of api.Problem objects is equal to the
        list specified in the given file.
        @param actual: The list of actual problems.
        @param expected_file: Path to the file containing the list of
            expected problems. The file should contain the repr() output
            for an api.Problem object on each line.
        @param outdir: The directory where output is written if the assertion
            fails.
        """
        act = sorted([repr(p) for p in actual])
        
        if not os.path.isfile(expected_file):
            raise RuntimeError("Expected problem file '%s' does not exist or is not a file!" % expected_file)
        f = open(expected_file, 'r')
        try:        exp = sorted([line.rstrip('\r\n') for line in f])
        finally:    f.close()
        
        # Delete lines starting with #
        for i in reversed(range(len(exp))):
            if exp[i].startswith('#'):
                del exp[i]
        
        if act != exp:
            if not os.path.exists(outdir):
                os.makedirs(outdir)
            self.write_data_to_file(os.path.join(outdir, 'actual.txt'), os.linesep.join(act))
            self.write_data_to_file(os.path.join(outdir, 'expected.txt'), os.linesep.join(exp))
            self.fail("Problem list is not what was expected, see the files in '%s'" % outdir)
    
    
    # =====================================================
    # Private helper methods
    # =====================================================
    
    def _get_dir_modtime_dict(self, dir_path):
        """
        Return a dictionary of all files and directories and their last
        modification times in a given directory.
        """
        refdict = {}
        for root, dirs, files in os.walk(dir_path):
            for f in files:
                path = os.path.join(root, f)
                refdict[path] = os.stat(path).st_mtime
            for d in dirs:
                path = os.path.join(root, d)
                refdict[path] = os.stat(path).st_mtime
        return refdict
    
    def _assert_modification(self, path, assert_not_modified=True):
        if os.path.isdir(path):
            if assert_not_modified:
                self._assert_dir_not_modified(path)
            else:
                self.assert_dir_modified(path)
        elif os.path.isfile(path):
            if assert_not_modified:
                self._assert_file_not_modified(path)
            else:
                self._assert_file_modified(path)
        else:
            self.fail("'%s' does not exist" % path)
    
    def _assert_dir_not_modified(self, dir_path):
        refdict = self._mod_refs[dir_path]
        curdict = self._get_dir_modtime_dict(dir_path)
        
        # If the keys of the dicts are not the same, the contents of the
        # dir have been modified (added or removed files/subdirs)
        self.assertEquals(curdict.keys(), refdict.keys())
        
        # Compare manually so that assertion error output shows the specific file/dir
        for path in curdict.iterkeys():
            self.assertEquals(curdict[path], refdict[path], "File or dir '%s' modified" % path)
    
    def assert_dir_modified(self, dir_path):
        refdict = self._mod_refs[dir_path]
        curdict = self._get_dir_modtime_dict(dir_path)
        
        self.assertNotEqual(curdict, refdict, "Directory '%s' has not been modified when it was expected to be" % dir_path)
    
    def _assert_file_not_modified(self, file_path):
        time1 = self._mod_refs[file_path]
        time2 = os.stat(file_path).st_mtime
        self.assertEquals(time1, time2,
            ("File '%s' was modified when it should not have been "+\
            "(mod time %f vs. %f)") % (file_path, time1, time2))
    
    def _assert_file_modified(self, file_path):
        time1 = self._mod_refs[file_path]
        time2 = os.stat(file_path).st_mtime
        self.assertNotEqual(time1, time2,
            ("File '%s' was modified not when it should have been "+\
            "(mod time %f vs. %f)") % (file_path, time1, time2))
