#!/usr/bin/env python2
#
#    (C) Copyright 2015-2024 Sei Lisa. All rights reserved.
#
#    This file is part of LSL PyOptimizer.
#
#    LSL PyOptimizer is free software: you can redistribute it and/or
#    modify it under the terms of the GNU General Public License as
#    published by the Free Software Foundation, either version 3 of the
#    License, or (at your option) any later version.
#
#    LSL PyOptimizer 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 LSL PyOptimizer. If not, see <http://www.gnu.org/licenses/>.

# Unit testing program.
# Checks all files in unit_tests/*.suite/ with extensions .lsl or .run.
# When one is found, it's considered a test (if both exist, they are considered
# a single test).
#
# Extension .lsl is for source files to test. If the first line starts with
#    "// " then the rest of the line is taken as the docstring of that test
#    (visible with Eric or with option -v). The script is also fed as standard
#    input to the program.
# .run defines the command-line parameters for invocation. A test can be run
#    without a .lsl file but with a .run file. If not present, the .lsl file
#    is run with the command line 'main.py -'. The quoting rules are sh-style.
#    The executable name is ignored, but needs to be present.
# .out is for expected output to stdout. If the first line is "REGEX", then
#    the rest of the file is interpreted as a regular expression that the
#    output is matched against. Otherwise the output must exactly match.
#    If the file is not present, that's equivalent to an empty file, i.e. no
#    output is expected.
# .err is like .out but for expected output to stderr, with the same features.
# .skp is for a file that if present, will skip this test. The contents are
#    displayed as the reason for being skipped.
# .fail is for a file that, when present, marks the test as expected to fail.
#    Its contents go to the docstring if not empty, replacing the .lsl one.
#
# A test passes when the stdout output matches the .out file, and the stderr
# output matches the .err file. Both default to empty strings.
#

import unittest
import sys
import os
#import math
import main
import glob
import re
try:
    import difflib
except ImportError:
    difflib = None
if sys.version_info.major < 3:
    from StringIO import StringIO as StringStream
else:
    from io import BytesIO as StringStream
from lslopt import lslcommon,lslfuncs,lslparse,lsloutput,lslloadlib
from lslopt.lslcommon import nr
from strutil import *

class EArgError(Exception):
    pass

def parseArgs(s):
    """Parse a command line, Bourne shell-style"""
    if s is None:
        return None

    args = []

    # States
    Space = 0           # Space between args.
    SBackslash = 1      # Backslash after space. Returns to Space if followed
                        # by LF, otherwise inserts the character verbatim and
                        # goes to Normal.
    Normal = 2          # Normal argument.
    NBackslash = 3      # Backslash in Normal mode. Returns to Normal if
                        # followed by LF, otherwise inserts the character
                        # verbatim.
    DQuote = 4          # Double quote mode.
    DQBackslash = 5     # Backslash in double quote mode. Returns to DQuote if
                        # followed by LF; inserts the character verbatim if
                        # followed by '"', '`', '$' or '\' and inserts a '\'
                        # plus the character verbatim in any other case.
    SQuote = 6          # Single quote mode.

    State = Space
    p = 0
    Len = len(s)
    arg = ''

    while p < Len:
        c = s[p:p+1]
        p += 1
        if State in (Space, Normal):
            if c == '\\':
                State = NBackslash if State == Normal else SBackslash
            elif c == '"':
                State = DQuote
            elif c == "'":
                State = SQuote
            elif c in (' ', '\t'):
                if State == Normal:
                    State = Space
                    args.append(arg)
                    arg = ''
                # else remain in the 'Space' state
            elif c == '\n':
                break
            else:
                State = Normal
                arg += c
        elif State in (SBackslash, NBackslash, DQBackslash):
            if c == '\n':
                State = (DQuote if State == DQBackslash
                         else Space if State == SBackslash
                         else Normal)
            else:
                if State == DQBackslash and c not in ('"', '`', '$', '\\'):
                    arg += '\\'
                arg += c
                State = DQuote if State == DQBackslash else Normal
        elif State == DQuote:
            if c == '\\':
                State = DQBackslash
            # ` and $ are not interpreted by this parser.
            elif c == '"':
                State = Normal
            else:
                arg += c
        elif State == SQuote:
            if c == "'":
                State = Normal
            else:
                arg += c

    if State in (SQuote, DQuote, DQBackslash):
        raise EArgError(u"Unterminated string in .run file")
    if State in (SBackslash, NBackslash):
        raise EArgError(u"Backslash before EOF in .run file")

    if State == Normal:
        args.append(arg)
    return args


#import codecs
## sh-style argument parsing
## identify line continuations
#cont_re = re.compile( '\\\\\n'
#                      '|(?:\.|[^ \t\n\'])'
#                     r"|'[^']*'")
## separates words
#args_re = re.compile(r'(?:'
#                        r'\\.'
#                         '|[^ \t\n\'"]'
#                        r'|"(?:\\.|[^"])*"'
#                        r"|'[^']*'"
#                     r')+')
## matches types of parts of a word ('...', "...", \x, x)
#part_re = re.compile(r'(?:'
#                        r'\\.'
#                         '|[^ \t\'"]'
#                     r')+'
#                     r'|"(?:\\.|[^"])*"'
#                     r"|'[^']*'")
#
#    args = args_re.findall(s)
#    for i in range(len(args)):
#        arg = args[i]
#        argout = ''
#        for match in part_re.finditer(arg):
#            part = match.group()
#            if part[0] == '"':
#                argout += codecs.escape_decode(part[1:-1])[0]
#            elif part[0] == "'":
#                argout += part[1:-1]
#            else:
#                argout += codecs.escape_decode(part)[0]
#        args[i] = argout
#    return args

def tryRead(fn, Binary = False):
    result = None
    try:
        f = open(fn, 'rb' if Binary else 'r')
        try:
            result = f.read()
        finally:
            f.close()
    except IOError as e:
        if e.errno != 2:
            raise
    return result

# In StringIO, mixing unicode and str causes problems with non-ASCII chars.
# Avoid it by overriding the write method, to always encode unicode as UTF-8.
class StrUTF8IO(StringStream):
    encoding = 'utf8'
    def write(self, s):
        StringStream.write(self, any2b(s))

def invokeMain(argv, stdin = None):
    """Invoke main.main, substituting stdin, stdout, stderr.
    Returns tuple with stdout and stderr."""
    # Revert globals to initial state
    lslcommon.LSO = False
    lslcommon.IsCalc = False
    lslcommon.Bugs.clear()
    lslcommon.Bugs.add(6495)
    lslcommon.save_stdin = sys.stdin
    lslcommon.save_stdout = sys.stdout
    lslcommon.save_stderr = sys.stderr
    stdout_output = None
    stderr_output = None
    try:
        sys.stdin = StringStream(stdin)
        sys.stdout = StrUTF8IO()
        sys.stderr = StrUTF8IO()
        sys.stdin.encoding = 'utf8'
        sys.stdout.encoding = 'utf8'
        sys.stderr.encoding = 'utf8'

        main.main(argv)

        stdout_output = sys.stdout.getvalue()
        stderr_output = sys.stderr.getvalue()
    finally:
        sys.stdin = lslcommon.save_stdin
        sys.stdout = lslcommon.save_stdout
        sys.stderr = lslcommon.save_stderr
        lslcommon.LSO = False
        lslcommon.IsCalc = False
        lslcommon.Bugs.clear()
        lslcommon.Bugs.add(6495)

    return (stdout_output, stderr_output)

#def tolEqual(actual, expected, tol):
#    """Strict equality. Like reallyEqual, but a tolerance can
#    be specified for comparing floats.
#    """
#    if type(actual) != type(expected):
#        return False
#
#    # Deal with floats (edge cases, tolerance)
#    if isinstance(actual, float):
#        # Signs must be equal
#        if math.copysign(1, actual) != math.copysign(1, expected):
#            return False
#        if math.isnan(actual):
#            # This compares the sign of NaN as well
#            return math.isnan(expected)
#        if math.isinf(actual) and math.isinf(expected):
#            return actual == expected
#        return abs(actual - expected) <= tol
#
#    # Deal with tuples and lists (item-by-item, recursively)
#    if isinstance(actual, (tuple, list)):
#        return all(tolEqual(i1, i2, tol)
#                   for i1, i2 in zip(actual, expected))
#
#    # Fall back to 'classic' equality
#    return actual == expected
#
#def reallyEqual(actual, expected):
#    """Strictest equality. The types must be equal. For floats, it checks
#    that the signs are equal, even for -0.0 and for NaNs. For the rest,
#    it falls back to ==.
#    """
#    return tolEqual(actual, expected, 0.0)
#
#def reprEqual(self, actual, expected):
#    """Returns whether the values are equal when comparing their repr's."""
#    return repr(actual) == repr(expected)

class UnitTestCase(unittest.TestCase):
    pass

class UnitTestRegression(UnitTestCase):
    def test_regression_misc(self):
        """Miscellaneous tests that can't be computed or are too difficult
        to compute with scripts
        """
        sys.stderr.write('\nRunning miscellaneous tests: ')
        # Test behaviour under BUG-3763
        lslcommon.Bugs.add(3763)
        self.assertEqual(lslfuncs.llXorBase64(u"ABCDABCDABCD", u"ABCD"),
                         u"AAAAAAAAABCT")
        self.assertEqual(lslfuncs.llXorBase64(u"ABCDABCDABCDABCDABCDABCDABCD",
                                              u"ABCD"),
                         u"AAAAAAAAABCTgxCDEJODAAAAABCT")
        self.assertEqual(lslfuncs.llXorBase64(u"ABCDABCDABCD", u"ABC="),
                         u"AACDEBCDEBCD")
        self.assertEqual(lslfuncs.llXorBase64(u"AQCDAQCD", u"AQC="),
                         u"AACCAQCC")
        lslcommon.Bugs.discard(3763)

        # Check that zstr returns the same type it is passed.
        self.assertEqual(type(lslfuncs.zstr(lslcommon.Key(u'x\0x'))),
                         lslcommon.Key)

    def test_regression_ll_json(self):
        from unit_tests import json
        # Patch llJsonSetValue, to allow running the test.
        json.llJsonSetValue = lambda x, y, z: u"***"
        werr('\nRunning JSON test module: ')
        save_stdout = sys.stdout
        save_stderr = sys.stderr
        actual_stdout = False
        actual_stderr = False
        try:
            sys.stdout = StrUTF8IO()
            sys.stderr = StrUTF8IO()
            errs = json.run_tests()
            actual_stdout = sys.stdout.getvalue()
            actual_stderr = sys.stderr.getvalue()
        finally:
            sys.stdout = save_stdout
            sys.stderr = save_stderr
        self.assertLessEqual(errs, 138)

        expected_stdout = str2b(tryRead('unit_tests/json.out'), 'utf8')
        expected_stderr = str2b(tryRead('unit_tests/json.err'), 'utf8')
        try:
            self.assertTrue(actual_stdout == expected_stdout)
        except AssertionError:
            werr(u'Failed'
                 u'\n************ expected stdout:\n')
            werr(expected_stdout)
            werr(u'\n************ actual stdout:\n')
            werr(actual_stdout)
            if difflib and expected_stdout and actual_stdout \
               and not expected_stdout.startswith(b'REGEX\n'):
                werr(u'\n************ diff:\n'
                     + u'\n'.join(difflib.unified_diff(
                        b2u(expected_stdout).split(u'\n'),
                        b2u(actual_stdout).split(u'\n'),
                        'expected', 'actual', lineterm=''
                )))
            werr(u'\n************ ')
            raise
        try:
            self.assertTrue(actual_stderr == expected_stderr)
        except AssertionError:
            werr(u'Failed'
                 u'\n************ expected stderr:\n')
            werr(expected_stderr)
            werr(u'\n************ actual stderr:\n')
            werr(actual_stderr)
            if difflib and expected_stderr and actual_stderr \
               and not expected_stderr.startswith(b'REGEX\n'):
                werr(u'\n************ diff:\n'
                     + u'\n'.join(difflib.unified_diff(
                        b2u(expected_stderr).split(u'\n'),
                        b2u(actual_stderr).split(u'\n'),
                        'expected', 'actual', lineterm=''
                )))
            werr(u'\n************ ')
            raise
        assert 'unit_tests.json' in sys.modules
        del sys.modules['unit_tests.json']

    def test_regression_parser(self):
        """Test the error cases. There are too many to make a test of each."""
        sys.stderr.write('\nRunning parser error tests: ')
        parser = lslparse.parser(lslloadlib.LoadLibrary())
        self.assertRaises(lslparse.EParseSyntax, parser.parse,
            'f(){integer i;i>>=i;}')
        self.assertRaises(lslparse.EParseCantChangeState, parser.parse,
            'f(){if(1)state default;else;}default{timer(){}}')
        self.assertRaises(lslparse.EParseCantChangeState, parser.parse,
            'f(){if(1);else state default;}default{timer(){}}')
        self.assertRaises(lslparse.EParseCantChangeState, parser.parse,
            'f(){if(1)if(1)state default;else;else;}default{timer(){}}')

        # Test behaviour of void functions
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'default{timer(){<llDie(),0,0>;}}')
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'default{timer(){[<llDie(),0,0>];}}')
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'default{timer(){key a=llDie();}}')
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'default{timer(){key a;a=llDie();}}')
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'default{timer(){do;while(llDie());}}')
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'default{timer(){for(;llDie(););}}')
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'default{timer(){while(llDie());}}')
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'default{timer(){if(llDie());}}')
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'default{timer(){if(llDie());else;}}')
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'default{timer(){[llDie()];}}', ('optimize',))
        parser.parse('default{timer(){[llDie()];}}')
        parser.parse('default{timer(){llDie();}}')
        parser.parse('default{timer(){(llDie());}}')
        parser.parse('default{timer(){for(llDie();1;llDie());}}',
            ('optimize',))
        # 'return <void expr>' works in the same situations as state changes
        self.assertRaises(lslparse.EParseReturnShouldBeEmpty, parser.parse,
            'default{timer(){return llDie();}}')
        self.assertRaises(lslparse.EParseReturnShouldBeEmpty, parser.parse,
            'default{timer(){if(1)return llDie();else;}}')
        self.assertRaises(lslparse.EParseReturnShouldBeEmpty, parser.parse,
            'default{timer(){if(1);else return llDie();}}')
        self.assertRaises(lslparse.EParseReturnShouldBeEmpty, parser.parse,
            'default{timer(){return 1;}}')
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'default{timer(){if(1)return 1;}}')

        # Constants with 'c' in symbol table (introduced to solve issue #30)
        # Some errors are not the expected ones. Most (all?) should be syntax.
        parser.parse('integer a=LOOP;default{timer(){llOwnerSay(NAK+EOF);}}')
        parser.parse('integer a=LOOP;default{timer(){llOwnerSay(NAK+EOF);}}',
            ('prettify',))
        parser.parse('default{timer(){LOOP;}}')
        parser.parse('default{timer(){LOOP;}}', ('prettify',))
        self.assertRaises(lslparse.EParseSyntax, parser.parse,
            'default{touch(integer LOOP){}}', ('prettify',))
        self.assertRaises(lslparse.EParseSyntax, parser.parse,
            'default{timer(){ZERO_VECTOR.x;}}', ('prettify',))
        self.assertRaises(lslparse.EParseSyntax, parser.parse,
            'default{timer(){LOOP.x;}}', ('prettify',))
        self.assertRaises(lslparse.EParseSyntax, parser.parse,
            'default{timer(){LOOP=1;}}', ('prettify',))
        self.assertRaises(lslparse.EParseSyntax, parser.parse,
            'default{timer(){LOOP++;}}', ('prettify',))
        # should raise EParseSyntax instead
        self.assertRaises(lslparse.EParseUndefined, parser.parse,
            'default{timer(){++LOOP;}}', ('prettify',))
        # should raise EParseSyntax instead
        self.assertRaises(lslparse.EParseAlreadyDefined, parser.parse,
            'integer LOOP=0;', ('prettify',))
        # should raise EParseSyntax instead
        self.assertRaises(lslparse.EParseAlreadyDefined, parser.parse,
            'integer LOOP(){}', ('prettify',))
        self.assertRaises(lslparse.EParseSyntax, parser.parse,
            'default{timer(){integer LOOP=1;}}', ('prettify',))
        self.assertRaises(lslparse.EParseSyntax, parser.parse,
            'default{timer(){integer LOOP;}}', ('prettify',))
        # should raise EParseSyntax instead
        self.assertRaises(lslparse.EParseAlreadyDefined, parser.parse,
            'default{timer(){}}state LOOP{timer(){}}', ('prettify',))
        # should raise EParseSyntax instead
        self.assertRaises(lslparse.EParseUndefined, parser.parse,
            'default{timer(){state LOOP;}}', ('prettify',))
        self.assertRaises(lslparse.EParseSyntax, parser.parse,
            'default{timer(){@LOOP;}}', ('prettify',))
        self.assertRaises(lslparse.EParseSyntax, parser.parse,
            'default{timer(){integer LOOP;}}', ('prettify',))


class UnitTestCoverage(UnitTestCase):
    def test_coverage_misc(self):
        """Miscellaneous tests that can't be computed or are too difficult
        to compute with scripts
        """
        sys.stderr.write('\nRunning misc coverage tests: ')
        # Doesn't accept bytes
        self.assertRaises(lslfuncs.ELSLInvalidType, lslfuncs.zstr, b"blah")
        # Can't typecast float to vector
        self.assertRaises(lslfuncs.ELSLTypeMismatch, lslfuncs.typecast,
                          lslfuncs.F32(1.2), lslcommon.Vector)
        # Can't typecast integer to vector
        self.assertRaises(lslfuncs.ELSLTypeMismatch, lslfuncs.typecast,
                          1, lslcommon.Vector)
        # Can't typecast vector to key
        self.assertRaises(lslfuncs.ELSLTypeMismatch, lslfuncs.typecast,
                          lslcommon.Vector((1.,2.,3.)), lslcommon.Key)
        # Can't typecast quaternion to key
        self.assertRaises(lslfuncs.ELSLTypeMismatch, lslfuncs.typecast,
                          lslcommon.Quaternion((1.,2.,3.,4.)), lslcommon.Key)
        # Can't typecast list to vector
        self.assertRaises(lslfuncs.ELSLTypeMismatch, lslfuncs.typecast,
                          [1, 1., lslcommon.Key(u'blah'),
                           lslcommon.Quaternion((1.,0.,0.,0.))],
                          lslcommon.Vector)
        # Can't typecast key to integer
        self.assertRaises(lslfuncs.ELSLTypeMismatch, lslfuncs.typecast,
                          lslcommon.Key(u"1"), int)
        # Can't negate string
        self.assertRaises(lslfuncs.ELSLTypeMismatch, lslfuncs.neg, u"3")
        # Can't add two keys
        self.assertRaises(lslfuncs.ELSLTypeMismatch, lslfuncs.add,
                          lslcommon.Key(u"1"), lslcommon.Key(u"2"))
        # Can't subtract two strings
        self.assertRaises(lslfuncs.ELSLTypeMismatch, lslfuncs.sub,
                          u"1", u"2")
        # Can't multiply two strings
        self.assertRaises(lslfuncs.ELSLTypeMismatch, lslfuncs.mul,
                          u"1", u"2")
        # Can't multiply quaternion and float in any order
        self.assertRaises(lslfuncs.ELSLTypeMismatch, lslfuncs.mul,
                          lslcommon.Quaternion((1.,2.,3.,4.)), 1.)
        self.assertRaises(lslfuncs.ELSLTypeMismatch, lslfuncs.mul,
                          1., lslcommon.Quaternion((1.,2.,3.,4.)))
        # Can't multiply quaternion by vector (but the opposite order is OK)
        self.assertRaises(lslfuncs.ELSLTypeMismatch, lslfuncs.mul,
                          lslcommon.Quaternion((1.,2.,3.,4.)),
                          lslcommon.Vector((1.,2.,3.)))
        # Can't divide quaternion by vector either
        self.assertRaises(lslfuncs.ELSLTypeMismatch, lslfuncs.div,
                          lslcommon.Quaternion((1.,2.,3.,4.)),
                          lslcommon.Vector((1.,2.,3.)))
        # Can't mod floats
        self.assertRaises(lslfuncs.ELSLTypeMismatch, lslfuncs.mod, 3., 3)
        # Can't compare string and integer
        self.assertRaises(lslfuncs.ELSLTypeMismatch, lslfuncs.compare, u'3', 4)
        self.assertRaises(lslfuncs.ELSLTypeMismatch, lslfuncs.less, u'3', 4)

        # Bytes is not a valid type to multiply by (in any order)
        self.assertRaises(lslfuncs.ELSLInvalidType, lslfuncs.mul, b"a", 3)
        self.assertRaises(lslfuncs.ELSLInvalidType, lslfuncs.mul,
                          lslcommon.Vector((3.,4.,5.)), b"a")
        self.assertRaises(lslfuncs.ELSLInvalidType, lslfuncs.typecast,
                          b"", unicode)

        # v2f/q2f coverage (force conversion from ints to floats)
        self.assertEqual(repr(lslfuncs.v2f(lslcommon.Vector((1,0,0)))),
                         'Vector((1.0, 0.0, 0.0))')
        self.assertEqual(repr(lslfuncs.q2f(lslcommon.Quaternion((1,0,0,0)))),
                         'Quaternion((1.0, 0.0, 0.0, 0.0))')
        # Key repr coverage
        self.assertEqual(repr(lslcommon.Key(u'')), "Key(u'')"
            if str != unicode else "Key('')")

        # string + key coverage
        self.assertEqual(lslfuncs.add(u'a', lslcommon.Key(u'b')), u'ab')
        self.assertEqual(type(lslfuncs.add(u'a', lslcommon.Key(u'b'))), unicode)

        # The SEF table prevents this assertion from being reachable via script.
        self.assertRaises(lslfuncs.ELSLCantCompute, lslfuncs.llXorBase64Strings,
                          u"AABA", u"AABA")
        self.assertRaises(lslfuncs.ELSLCantCompute, lslfuncs.llModPow,
                          3, 5, 7)
        # Check invalid type in llGetListEntryType
        self.assertRaises(lslfuncs.ELSLInvalidType, lslfuncs.llGetListEntryType,
            [b'a'], 0)

        # Check that Value2LSL raises an exception if the type is unknown.
        outmod = lsloutput.outscript()
        # Script with a single node of type Expression, containing a constant
        # of type Bytes. That's rejected by the output module.
        msg = None
        script = [nr(nt='EXPR', t='string', ch=[
            nr(nt='CONST', t='string', value=b'ab')
        ])]
        save_IsCalc = lslcommon.IsCalc
        lslcommon.IsCalc = True
        try:
            try:
                outmod.output((script, ()))
            except AssertionError as e:
                msg = str(e)
        finally:
            lslcommon.IsCalc = save_IsCalc
        self.assertEqual(msg, u"Value of unknown type in Value2LSL: 'ab'"
            if python2 else u"Value of unknown type in Value2LSL: b'ab'")
        del msg
        # Extended assignment in output
        script = [nr(nt='EXPR', t='integer', ch=[
            nr(nt='^=', t='integer', ch=[
                nr(nt='IDENT', t='integer', name='a', scope=0),
                nr(nt='CONST', t='integer', value=3)
            ])])]
        save_IsCalc = lslcommon.IsCalc
        lslcommon.IsCalc = True
        try:
            out = outmod.output((script, [{'a':{'Kind':'v','Loc':1,'Scope':0,
                                                'Type':'integer'}
                                         }]
                               ))
        finally:
            lslcommon.IsCalc = save_IsCalc

        self.assertEqual(out, 'a = a ^ (3)')
        del out, script, outmod, save_IsCalc

    def test_coverage_parser(self):
        """Cover the error cases. There are too many to make a test of each."""
        parser = lslparse.parser(lslloadlib.LoadLibrary(
            builtins = 'unit_tests/builtins-coverage-2.txt',
            fndata = 'unit_tests/builtins-coverage-2.txt'))
        self.assertRaises(lslparse.EParseNoConversion, parser.parse,
            'f(){list L;(integer)L[0];}', ('lazylists',))
        parser = lslparse.parser(lslloadlib.LoadLibrary())
        sys.stderr.write('\nRunning parser exception coverage tests: ')
        # Parse_unary_postfix_expression
        self.assertRaises(lslparse.EParseUEOF, parser.parse, u'f(){key x=')
        self.assertRaises(lslparse.EParseUndefined, parser.parse,
            'f(){g();}')
        self.assertRaises(lslparse.EParseUndefined, parser.parse,
            'integer g;f(){g();}')
        self.assertRaises(lslparse.EParseUndefined, parser.parse,
            'f(){f=0;}')
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'f(){integer V; V[1] = 0;}', ('lazylists',))
        self.assertRaises(lslparse.EParseFunctionMismatch, parser.parse,
            'f(){list V; V[1,1] = 0;}', ('lazylists',))
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'f(){list V; V[""] = 0;}', ('lazylists',))
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'f(){list V; V[1] = llDie();}', ('lazylists',))
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'f(){string s;s++;}')
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'f(){string s;++s;}')
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'f(){string s;s=llDie();}')
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'f(){string s;s+=(key)"";}')
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'f(){string s;s-=s;}')
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'f(){string s;s*=2;}')
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'f(){vector v;v%=1.0;}')
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'f(){string s;s>>=s;}', ('extendedassignment',))
        # Parse_unary_expression
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'f(){-"";}')
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'f(){!"";}')
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'f(){~"";}')
        self.assertRaises(lslparse.EParseUndefined, parser.parse,
            'f(){++f;}')
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'f(){(key)1;}')
        self.assertRaises(lslparse.EParseFunctionMismatch, parser.parse,
            'f(){list L;(integer)L[""];}', ('lazylists',))
        # Parse_factor
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'f(){""*2;}')
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'f(){<1,1,1>%2;}')
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'f(){<1,1,1>/<1,1,1>;}')
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'f(){<1,1,1>/"";}')
        # Parse_term
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'f(){llDie()+1;}')
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'f(){""-1;}')
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'f(){[]+llDie();}')
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'f(){(key)""+(key)"";}')
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'f(){""+(key)"";}')
        # Parse_shift
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'f(){"">>1;}')
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'f(){1<<"";}')
        # Parse_inequality
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'f(){""<"";}')
        # Parse_comparison
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'f(){llDie()==3;}')
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'f(){""==3;}')
        # Parse_bitbool_factor
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'f(){""&3;}')
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'f(){3&"";}')
        # Parse_bitxor_term
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'f(){""^3;}')
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'f(){3^"";}')
        # Parse_bitbool_term
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'f(){""|3;}')
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'f(){3|"";}')
        # Parse_expression
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'f(){3||"";}')
        self.assertRaises(lslparse.EParseTypeMismatch, parser.parse,
            'f(){""&&3;}')
        # Parse_optional_expression_list
        self.assertRaises(lslparse.EParseFunctionMismatch, parser.parse,
            'f(){llSay(0);}')
        self.assertRaises(lslparse.EParseAlreadyDefined, parser.parse,
            'f(){@x;@x;}')
        self.assertRaises(lslparse.EParseAlreadyDefined, parser.parse,
            'f(){integer x;integer x;}')
        self.assertRaises(lslparse.EParseAlreadyDefined, parser.parse,
            'f(integer x, integer x){}')
        self.assertRaises(lslparse.EParseAlreadyDefined, parser.parse,
            'default{timer(){}timer(){}}')
        self.assertRaises(lslparse.EParseSyntax, parser.parse,
            'default{timer(){state state;}}')
        self.assertRaises(lslparse.EParseUndefined, parser.parse,
            'default{timer(){state undefined;}}')
        self.assertRaises(lslparse.EParseSyntax, parser.parse,
            'default{timer(){switch(1){case 1;}}}', ('enableswitch',))
        self.assertRaises(lslparse.EParseSyntax, parser.parse,
            'default{timer(){switch(1){default;}}}', ('enableswitch',))
        self.assertRaises(lslparse.EParseInvalidBrkContArg, parser.parse,
            'default{timer(){while(1){break 0;}}}', ('breakcont',))
        self.assertRaises(lslparse.EParseInvalidBrkContArg, parser.parse,
            'default{timer(){while(1){break 2;}}}', ('breakcont',))
        self.assertRaises(lslparse.EParseInvalidBrkContArg, parser.parse,
            'default{timer(){while(1){continue 0;}}}', ('breakcont',))
        self.assertRaises(lslparse.EParseInvalidBrkContArg, parser.parse,
            'default{timer(){while(1){continue 2;}}}', ('breakcont',))
        self.assertRaises(lslparse.EParseSyntax, parser.parse,
            'integer T=-TRUE;default{timer(){}}')
        self.assertRaises(lslparse.EParseSyntax, parser.parse,
            'list L=[[]];default{timer(){}}')
        self.assertRaises(lslparse.EParseSyntax, parser.parse,
            'default{timer(integer i){}}')
        self.assertRaises(lslparse.EParseSyntax, parser.parse,
            'i = 0;',)
        self.assertRaises(lslparse.EParseSyntax, parser.parse,
            'default{timer(){}}state{timer(){}}')
        self.assertRaises(lslparse.EParseUndefined, parser.parse,
            'default{timer(){jump undefined;}}')
        # BuildTempGlobalsTable coverage
        self.assertRaises(lslparse.EParseSyntax, parser.parse,
            ';')
        self.assertRaises(lslparse.EParseSyntax, parser.parse,
            'f(;')
        self.assertRaises(lslparse.EParseSyntax, parser.parse,
            'f();')
        self.assertRaises(lslparse.EParseSyntax, parser.parse,
            'integer f=')
        self.assertRaises(lslparse.EParseUEOF, parser.parse,
            'integer /*')
        self.assertRaises(lslparse.EParseSyntax, parser.parse,
            'default{timer(){}}state e;')

class UnitTestExpr(UnitTestCase):
    pass

class UnitTestLSO(UnitTestCase):
    pass

class UnitTestPreproc(UnitTestCase):
    pass

def generateScriptTests():
    """Find all files in unit_tests/*.d/*.{lsl,run} and generate tests for
    them.
    """

    path = os.path.dirname(__file__)
    if path:
        os.chdir(path)

    testsuites = ('Regression', 'Coverage', 'Expr', 'LSO', 'Preproc')
    for testsuite in testsuites:
        files = glob.glob(os.path.join('unit_tests',
                                       testsuite.lower() + '.suite', '*.lsl')
           )  + glob.glob(os.path.join('unit_tests',
                                       testsuite.lower() + '.suite', '*.run')
           )
        files = list(set([os.path.splitext(x)[0] for x in files]))
        files.sort()
        for fbase in files:
            # Create a closure with the test data
            def makeTestFunction(fbase, suite):
                def TestFunction(self):
                    # We NEED to read in binary, because there's invalid UTF-8
                    # as part of the tests, and Python 3 would choke otherwise.
                    # This means we have to deal with CRLF line endings here.
                    stdin = (tryRead(fbase + '.lsl', Binary=True) or b''
                        ).replace(b'\r\n', b'\n')

                    expected_stdout = str2b(tryRead(fbase + '.out') or '',
                        'utf8')
                    expected_stderr = str2b(tryRead(fbase + '.err') or '',
                        'utf8')
                    runargs = (parseArgs(tryRead(fbase + '.run'))
                               or (['main.py', '-y', '-'] if suite != 'Expr'
                                   else ['main.py',
                                         # Defaults for Expr:
                                         '-O', 'clear,optimize,constfold'
                                               ',addstrings,foldtabs,expr',
                                         '-y',
                                         '-']))
                    werr(u"\nRunning test %s: " % any2u(fbase))
                    actual_stdout, actual_stderr = invokeMain(runargs, stdin)
                    actual_stdout = actual_stdout.replace(b'\r\n', b'\n')
                    actual_stderr = actual_stderr.replace(b'\r\n', b'\n')

                    try:
                        if expected_stderr.startswith(b'REGEX\n'):
                            self.assertIsNotNone(re.search(
                                b2u(expected_stderr[6:], 'utf8'),
                                b2u(actual_stderr, 'utf8'))
                            )
                        else:
                            self.assertTrue(expected_stderr == actual_stderr)
                    except AssertionError:
                        werr(u'Failed'
                             u'\n************ expected stderr:\n')
                        werr(expected_stderr)
                        werr(u'\n************ actual stderr:\n')
                        werr(actual_stderr)
                        if difflib and expected_stderr and actual_stderr \
                           and not expected_stderr.startswith(b'REGEX\n'):
                            werr(u'\n************ diff:\n'
                                 + u'\n'.join(difflib.unified_diff(
                                    b2u(expected_stderr).split(u'\n'),
                                    b2u(actual_stderr).split(u'\n'),
                                    'expected', 'actual', lineterm=''
                            )))
                        werr(u'\n************ ')
                        raise
                    try:
                        if expected_stdout.startswith(b'REGEX\n'):
                            self.assertIsNotNone(re.search(
                                b2u(expected_stdout[6:], 'utf8'),
                                b2u(actual_stdout, 'utf8')))
                        else:
                            self.assertTrue(expected_stdout == actual_stdout)
                    except AssertionError:
                        werr(u'Failed'
                             u'\n************ expected stdout:\n')
                        werr(expected_stdout)
                        werr(u'\n************ actual stdout:\n')
                        werr(actual_stdout)
                        if difflib and expected_stdout and actual_stdout \
                           and not expected_stdout.startswith(b'REGEX\n'):
                            werr(u'\n************ diff:\n'
                                 + u'\n'.join(difflib.unified_diff(
                                    b2u(expected_stdout).split('\n'),
                                    b2u(actual_stdout).split('\n'),
                                    'expected', 'actual', lineterm=''
                            )))
                        sys.stderr.write(u'\n************ ')
                        raise
                return TestFunction
            TestFunction = makeTestFunction(fbase, testsuite)
            # __doc__ is used by Eric
            line = b''
            try:
                f = open(fbase + '.lsl', 'rb')
                try:
                    line = f.readline()
                    if line.endswith(b'\r\n'):
                        line = line[:-2]
                    elif line[-1:] in (b'\r', b'\n'):
                        line = line[:-1]
                finally:
                    f.close()
            except IOError as e:
                if e.errno != 2:
                    raise
            TestFunction.__doc__ = (b2u(line[3:]) if line.startswith(b'// ')
                                    else None)

            TestFunction.__name__ = ('test_' + testsuite + '__'
                + os.path.basename(fbase).replace('-','_'))
            fail = tryRead(fbase + '.fail')
            if fail is not None:
                if fail:
                    TestFunction.__doc__ = b2u(fail)
                TestFunction = unittest.expectedFailure(TestFunction)
            else:
                skip = tryRead(fbase + '.skp')
                if skip is not None:
                    TestFunction = unittest.skip(skip)(TestFunction)
            setattr(globals()['UnitTest' + testsuite],
                    TestFunction.__name__, TestFunction)


generateScriptTests()
if __name__ == '__main__':
    unittest.main(argv = sys.argv)
