#!/usr/bin/python3
# encoding=UTF-8

# Copyright © 2012, 2013 Jakub Wilk
#
# This program is free software.  It is distributed under the terms of the GNU
# General Public License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# this program.  If not, you can find it on the World Wide Web at
# http://www.gnu.org/copyleft/gpl.html, or write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.

import argparse
import errno
import glob
import hashlib
import itertools
import os
import re
import subprocess as ipc
import unittest

import apt
import apt_pkg

class MalformedTestDescription(RuntimeError):
    pass

_lintian_line_re = re.compile(r'^(?P<type>[a-z]): (?P<package>[a-z0-9][a-z0-9.+-]+)(?: (?P<pkgtype>[a-z]+))?: (?P<tag>[a-zA-Z0-9._-]+)( (?P<extra>.+))?$')

def tag_from_lintian_line(line):
    match = _lintian_line_re.match(line)
    if match is None:
        raise ValueError('cannot parse lintian line: {line!r}'.format(line=line))
    return match.group('tag')

def sha1file(path):
    sha1 = hashlib.sha1()
    with open(path, 'rb') as file:
        for chunk in iter(lambda: file.read(1 << 16), b''):
            sha1.update(chunk)
    return sha1.hexdigest()

class FileItem(object):

    def __init__(self, filename, sha1):
        if '/' in filename:
            raise ValueError('invalid filename')
        self.filename = filename
        self.sha1 = sha1

    def download(self, fetcher, directory):
        uri = 'http://snapshot.debian.org/file/{sha1}'.format(sha1=self.sha1)
        dest_filename = os.path.join(directory, self.filename)
        if os.path.exists(dest_filename):
            return
        tmp_filename = dest_filename + '.tmp'
        yield apt_pkg.AcquireFile(
            fetcher,
            uri=uri,
            descr=self.filename,
            destfile=tmp_filename
        )
        if os.path.exists(dest_filename):
            # The file might have been downloaded in the mean time.
            return
        computed_sha1 = sha1file(tmp_filename)
        if computed_sha1 != self.sha1:
            raise RuntimeError('{file} SHA-1 mismatch: {comp_sha1} but expected {sha1}'.format(
                file=self.filename,
                comp_sha1=computed_sha1,
                sha1=self.sha1)
            )
        os.rename(tmp_filename, dest_filename)

    def __repr__(self):
        return '{cls}(filename={self.filename!r}, sha1={self.sha1!r})'.format(
            cls=type(self).__name__,
            self=self,
        )

class TestCase(unittest.TestCase):

    def __init__(self, parent, files, test_for=(), test_against=(), test_check=None, name=None):
        unittest.TestCase.__init__(self, 'test')
        assert test_for or test_against or test_check
        self.parent = parent
        self.files = list(files)
        self.test_for = list(test_for)
        self.test_for.sort()
        self.test_against = list(test_against)
        self.test_against.sort()
        self.test_check = test_check
        self.name = name
        self.maxDiff = None

    def get_affected_tags(self):
        for line in itertools.chain(self.test_for, self.test_against):
            yield tag_from_lintian_line(line)

    def download(self, fetcher):
        for file in self.files:
            coroutine = file.download(
                fetcher=fetcher,
                directory=self.parent.cache_directory
            )
            yield (next(coroutine), coroutine)

    def test(self):
        tags = ','.join(self.get_affected_tags())
        interesting_files = [
            os.path.join(self.parent.cache_directory, file.filename)
            for file in self.files
            if file.filename.endswith(('.dsc', '.changes', '.deb', '.udeb'))
        ]
        commandline = ['lintian4py', '--no-cfg']
        if self.test_check:
            commandline += ['--check-part', self.test_check]
        if tags:
            commandline += ['--tags', tags]
        commandline += interesting_files
        lintian = ipc.Popen(commandline,
            stdout=ipc.PIPE,
            stderr=ipc.PIPE
        )
        stdout, stderr = (s.decode('ASCII').splitlines() for s in lintian.communicate())
        if stderr:
            raise AssertionError('Non-empty stderr:\n' +
                '\n'.join('| ' + line for line in stderr)
            )
        stdout.sort()
        self.assertEqual(stdout, self.test_for)

    def __repr__(self):
        return '{cls}(parent={self.parent!r}, files={self.files!r}, test_for={self.test_for!r}, test_against={self.test_against!r}, test_check={self.test_check!r})'.format(
            cls=type(self).__name__,
            self=self,
        )

    def __str__(self):
        if self.name is not None:
            return self.name
        result = []
        result += ('+ {0!r}'.format(x) for x in self.test_for)
        result += ('- {0!r}'.format(x) for x in self.test_against)
        result += ('C {0!r}'.format(x) for x in self.test_check)
        return ', '.join(result)

class TestSuite(unittest.TestSuite):

    def __init__(self, directory=None, tdesc_filenames=()):
        unittest.TestSuite.__init__(self)
        if directory is None:
            directory = os.path.dirname(__file__)
        self.directory = directory
        if not tdesc_filenames:
            tdesc_filenames = glob.glob(os.path.join(self.directory, '*.t'))
        self.tdesc_filenames = tdesc_filenames
        self.cache_directory = os.path.join(directory, 'cache')
        try:
            os.mkdir(self.cache_directory)
        except OSError as ex:
            if ex.errno != errno.EEXIST:
                raise
        self.addTests(self.read_tests())

    def download(self):
        progress = apt.progress.text.AcquireProgress()
        fetcher = apt_pkg.Acquire(progress)
        items = list(itertools.chain(
            *(test.download(fetcher) for test in self)
        ))
        if not items:
            return
        rc = fetcher.run()
        if rc != fetcher.RESULT_CONTINUE:
            raise RuntimeError('fetching files failed')
        for item, coroutine in items:
            try:
                next(coroutine)
            except StopIteration:
                pass
            else:
                raise RuntimeError

    def run(self, result):
        self.download()
        unittest.TestSuite.run(self, result)

    def read_tests(self):
        for tdesc_filename in self.tdesc_filenames:
            with open(tdesc_filename) as tdesc_file:
                for para in apt_pkg.TagFile(tdesc_file):
                    yield self.read_test(para)

    def read_test(self, para):
        name = para.get('name')
        try:
            files = para['files'].splitlines()
        except KeyError:
            raise MalformedTestDescription('missing Files field')
        files = list(
            FileItem(*reversed(f.split(None, 1)))
            for f in files
        )
        test_for = [s.lstrip() for s in para.get('test-for', '').splitlines()]
        test_against = [s.lstrip() for s in para.get('test-against', '').splitlines()]
        test_check = para.get('test-check', '').strip() or None
        if not (test_for or test_against or test_check):
            raise MalformedTestDescription('missing Test-For or Test-Check or Test-Against field')
        return TestCase(self,
            files=files,
            test_for=test_for,
            test_against=test_against,
            test_check=test_check,
            name=name
        )

def setup_environment():
    os.environ['LINTIAN_INTERNAL_TESTSUITE'] = '1'
    try:
        del os.environ['LINTIAN_PROFILE']
    except LookupError:
        pass

class TestProgram(unittest.TestProgram):
    def parseArgs(self, argv):
        parser = argparse.ArgumentParser()
        parser.add_argument('-q', '--quiet', action='store_const', const=0, dest='verbosity', default=1, help='minimal output')
        parser.add_argument('-v', '--verbose', action='store_const', const=2, dest='verbosity', help='verbose output')
        parser.add_argument('-f', '--failfast', action='store_true', help='stop on first failure')
        parser.add_argument('-c', '--catch', dest='catchbreak', action='store_true', help='catch Control+C and display results')
        parser.add_argument('-b', '--buffer', action='store_true', help='buffer stdout and stderr during test runs')
        parser.add_argument('tdesc', metavar='<test-file>', nargs='*', help='path to test file (*.t)')
        options = parser.parse_args()
        for k, v in vars(options).items():
            setattr(self, k, v)
        self.test = TestSuite(tdesc_filenames=options.tdesc)

def main():
    setup_environment()
    TestProgram()

if __name__ == '__main__':
    main()

# Local Variables:
# indent-tabs-mode: nil
# End:
# vim: syntax=python sw=4 sts=4 sr et
