# -*- coding: utf-8 -*-
###
### $Release: 0.14.1 $
### $Copyright: copyright(c) 2010-2014 kuwata-lab.com all rights reserved $
### $License: MIT License $
###

import sys, os, re
import unittest

import oktest
from oktest import ok, not_ok, NG, NOT, python3


def be_fail(message):
    def deco(func):
        try:
            func()
        except AssertionError:
            if message:
                ex = sys.exc_info()[1]
                assert message == str(ex), "%r != %r" % (message, str(ex))
        else:
            assert False, "AssertionError expected but not raised"
    return deco


def be_error(error_class, message):
    def deco(func):
        try:
            func()
        except Exception:
            exc_info = sys.exc_info()
            assert exc_info[0] is error_class, "%r is not %r" % (exc_info[0], error_class)
            if message:
                ex = exc_info[1]
                assert message == str(ex), "%r != %r" % (message, str(ex))
        else:
            assert False, "%s expected but not raised" % (exc_info[0])
    return deco


def run_script(script, use_subprocess=False):
    output = None
    try:
        fname = "_test_.py"
        command = "%s %s" % (sys.executable, fname)
        oktest_reporter = os.environ.get('OKTEST_REPORTER')
        os.environ['OKTEST_REPORTER'] = 'OldStyleReporter'
        f = open(fname, "w"); f.write(script); f.close()
        if use_subprocess:
            from subprocess import Popen, PIPE, STDOUT
            p = Popen(command, shell=True, stdin=PIPE, stdout=PIPE, stderr=STDOUT, close_fds=True)
            stdin, stdouterr = p.stdin, p.stdout
            try:
                stdin.close()
                output = stdouterr.read()
            finally:
                stdouterr.close()
            if python3:
                output = output.decode('utf-8')
        else:
            io = os.popen(command)
            try:
                output = io.read()
            finally:
                io.close()
    finally:
        os.unlink(fname)
        if oktest_reporter:
            os.environ['OKTEST_REPORTER'] = oktest_reporter
    return output


class DummyObject(object):
    pass



class Assertions_TC(unittest.TestCase):


    def test_eq(self):
        ok (1+1) == 2
        NG (1+1) == 1
        NOT (1+1) == 1
        @be_fail("2 == 1 : failed.")
        def fn(): ok (1+1) == 1
        @be_fail("not 2 == 2 : failed.")
        def fn(): NG (1+1) == 2
        @be_fail("not 2 == 2 : failed.")
        def fn(): NOT (1+1) == 2


    def test_ne(self):
        ok (1+1) != 1
        NG (1+1) != 2
        NOT (1+1) != 2
        @be_fail("2 != 2 : failed.")
        def fn(): ok (1+1) != 2
        @be_fail("not 2 != 1 : failed.")
        def fn(): NG (1+1) != 1
        @be_fail("not 2 != 1 : failed.")
        def fn(): NOT (1+1) != 1


    def test_lt(self):
        ok (1) < 2
        NG (2) < 2
        @be_fail("2 < 2 : failed.")
        def fn(): ok (2) < 2
        @be_fail("not 1 < 2 : failed.")
        def fn(): NG (1) < 2

    def test_le(self):
        ok (2) <= 2
        NG (2) <= 1
        @be_fail("2 <= 1 : failed.")
        def fn(): ok (2) <= 1
        @be_fail("not 2 <= 2 : failed.")
        def fn(): NG (2) <= 2

    def test_gt(self):
        ok (2) > 1
        NG (2) > 2
        @be_fail("2 > 2 : failed.")
        def fn(): ok (2) > 2
        @be_fail("not 2 > 1 : failed.")
        def fn(): NG (2) > 1

    def test_ge(self):
        ok (2) >= 2
        NG (1) >= 2
        @be_fail("1 >= 2 : failed.")
        def fn(): ok (1) >= 2
        @be_fail("not 2 >= 2 : failed.")
        def fn(): NG (2) >= 2


    def test_in_delta(self):
        ok (3.14159).in_delta(3.1415, 0.0001)
        @be_fail(None)
        def fn(): ok (3.14159).in_delta(3.1415, 0.00001)
        #msg = "3.1415899999999999 < 3.1415100000000002 : failed."
        #msg = "3.14159 < 3.1415100000000002 : failed."
        msg = "%r < %r : failed." % (3.14159, 3.1415 + 0.00001)
        @be_fail(msg)
        def fn(): ok (3.14159).in_delta(3.1415, 0.00001)


    def test_in_delta(self):
        ok ("123@mail.com").matches(r'\w+@\w+(\.\w+)')
        ok ("123@mail.com").matches(re.compile(r'^\w+@\w+(\.\w+)$'))
        @be_fail(r"re.search('\\d+', 'abc') : failed.")
        def fn(): ok ("abc").matches(r'\d+')
        @be_fail(r"not re.search('\\w+', 'foo') : failed.")
        def fn(): ok ("foo").not_match(re.compile(r'\w+'))


    def test_is_(self):
        val1 = [10]
        val2 = [10]
        ok (val1).is_(val1)
        @be_fail("[10] is [10] : failed.")
        def fn(): ok (val1).is_(val2),

    def test_is_not(self):
        val1 = [10]
        val2 = [10]
        ok (val1).is_not(val2)
        @be_fail("[10] is not [10] : failed.")
        def fn(): ok (val1).is_not(val1)


    def test_in_(self):
        L = [0,10,20,30]
        ok (10).in_(L)
        @be_fail("11 in [0, 10, 20, 30] : failed.")
        def fn(): ok (11).in_(L)

    def test_not_in(self):
        L = [0,10,20,30]
        ok (11).not_in(L)
        @be_fail("10 not in [0, 10, 20, 30] : failed.")
        def fn(): ok (10).not_in(L)


    def test_contains(self):
        L = [10, 20, 30]
        ok (L).contains(10)
        ok (L).contains(20)
        ok (L).contains(30)
        @be_fail("11 in [10, 20, 30] : failed.")
        def fn(): ok (L).contains(11)

    def test_not_contain(self):
        L = [10, 20, 30]
        ok (L).not_contain(11)
        @be_fail("10 not in [10, 20, 30] : failed.")
        def fn(): ok (L).not_contain(10)


    global Val
    class Val(object):
        def __init__(self, val):
            self.val = val
        def __repr__(self):
            return "<Val val=%s>" % self.val

    def test_is_a(self):
        ok (Val(123)).is_a(Val)
        @be_fail("isinstance(123, Val) : failed.")
        def fn(): ok (123).is_a(Val)

    def test_is_not_a(self):
        ok (123).is_not_a(Val)
        @be_fail("not isinstance(<Val val=123>, Val) : failed.")
        def fn(): ok (Val(123)).is_not_a(Val)


    def test_has_attr(self):
        ok ("s").has_attr("__class__")
        @be_fail("hasattr('s', 'xxxxx') : failed.")
        def fn(): ok ("s").has_attr("xxxxx")
        #
        ok ("s").hasattr("__class__")
        @be_fail("hasattr('s', 'xxxxx') : failed.")
        def fn(): ok ("s").hasattr("xxxxx")


    def test_attr(self):
        import datetime
        d = datetime.date(2000, 12, 31)
        ok (d).attr("year", 2000)
        ok (d).attr("year", 2000).attr("month", 12).attr("day", 31)
        NG (d).attr("year", 2001)
        @be_fail("attr('year'): 2000 == 2001 : failed.")
        def fn(): ok (d).attr("year", 2001)
        @be_fail("not attr('year'): 2000 == 2000 : failed.")
        def fn(): NG (d).attr("year", 2000)
        @be_fail("hasattr(datetime.date(2000, 12, 31), 'hour') : failed.")
        def fn(): ok (d).attr("hour", 12)
        @be_fail("hasattr(datetime.date(2000, 12, 31), 'hour') : failed.")
        def fn(): NG (d).attr("hour", 12)
        ##
        obj = DummyObject()
        obj.val = "aaa\nbbb\nccc\n"
        expected = r"""
attr('val'): 'aaa\nbbb\nccc\n' == 'aaa\nbbbb\nccc\n' : failed.
--- expected
+++ actual
@@ -1,3 +1,3 @@
 aaa
-bbbb
+bbb
 ccc
"""[1:]
        try:
            ok (obj).attr('val', "aaa\nbbbb\nccc\n")
        except AssertionError:
            ex = sys.exc_info()[1]
            actual = str(ex)
            actual = actual.replace("--- expected ", "--- expected")
            actual = actual.replace("+++ actual ",   "+++ actual")
            self.assertEqual(expected, actual)
        else:
            raise AssertionError("AsertionError expected but not raised")

    def test_matches(self):
        # passed
        try:
            ok ("SOS").matches(r'^[A-Z]+$')
        except:
            raise
        #
        try:
            ok ("SOS").matches(re.compile(r'^[A-Z]+$'))
        except:
            raise
        #
        try:
            ok ("SOS").matches(r'^[A-Z]+$', re.M)
        except:
            raise
        # failed
        try:
            ok ("SOS").matches(r'[0-9]+')
        except AssertionError:
            ex = sys.exc_info()[1]
            self.assertEqual("re.search('[0-9]+', 'SOS') : failed.", str(ex))
        else:
            raise AssertionError("AssertionError expected but not raised")
        #
        try:
            ok ("SOS").matches(re.compile(r'[0-9]+'))
        except AssertionError:
            ex = sys.exc_info()[1]
            self.assertEqual("re.search('[0-9]+', 'SOS') : failed.", str(ex))
        else:
            raise AssertionError("AssertionError expected but not raised")

    def test_not_match(self):
        # passed
        try:
            ok ("SOS").not_match(r'^[0-9]+$')
        except:
            raise
        #
        try:
            ok ("SOS").not_match(re.compile(r'^[0-9]+$'))
        except:
            raise
        #
        try:
            ok ("SOS").not_match(r'^[0-9]+$', re.M)
        except:
            raise
        # failed
        try:
            ok ("SOS").not_match(r'[A-Z]+')
        except AssertionError:
            ex = sys.exc_info()[1]
            self.assertEqual("not re.search('[A-Z]+', 'SOS') : failed.", str(ex))
        else:
            raise AssertionError("AssertionError expected but not raised")
        #
        try:
            ok ("SOS").not_match(re.compile(r'[A-Z]+'))
        except AssertionError:
            ex = sys.exc_info()[1]
            self.assertEqual("not re.search('[A-Z]+', 'SOS') : failed.", str(ex))
        else:
            raise AssertionError("AssertionError expected but not raised")

    def test_raises(self):
        def f(): raise ValueError('errmsg1')
        ok (f).raises(ValueError)              # ValueError
        def f(): raise ValueError('errmsg1')
        ok (f).raises(ValueError, 'errmsg1')   # ValueError + errmsg
        def f(): raise ValueError('ERROR123')
        ok (f).raises(ValueError, re.compile(r'^[A-Z]+\d+$'))   # ValueError + rexp
        def f(): raise ValueError('errmsg1')
        ok (f).raises(Exception, 'errmsg1')    # Exception + errmsg
        def f(): raise ValueError('errmsg1')
        ok (f).raises(Exception)               # f.exception
        assert hasattr(f, 'exception')
        assert isinstance(f.exception, ValueError)
        assert str(f.exception) == 'errmsg1'
        #
        @be_fail("Exception should be raised : failed.")
        def fn():
            def f(): pass
            ok (f).raises(Exception)
        @be_fail("ValueError('errmsg1',) is kind of NameError : failed.")
        def fn():
            def f(): raise ValueError('errmsg1')
            ok (f).raises(NameError)
        @be_fail("'errmsg1' == 'errmsg2' : failed.")
        def fn():
            def f(): raise ValueError('errmsg1')
            ok (f).raises(ValueError, 'errmsg2')
        @be_fail("error message 'ERROR' is not matched to pattern.")
        def fn():
            def f(): raise ValueError('ERROR')
            ok (f).raises(ValueError, re.compile(r'^[A-Z]+\d+$'))


    def test_raises2(self):     # pass through AssertionError
        def f():
            assert 1 == 2, '1==2'            # assertion failed
            raise ValueError('errmsg1')
        try:
            ok (f).raises(ValueError)        # ok().raises() doens't catch assertion error
        except AssertionError:
            assert True, "OK"
            ex = sys.exc_info()[1]
            assert str(ex) == '1==2'
        except Exception:
            assert Failse, "AssertionError expected"
        else:
            assert Failse, "AssertionError expected"


    def test_not_raise2(self):  # pass through AssertionError
        def f():
            assert 1 == 0                  # assertion failed
        try:
            ok (f).not_raise(Exception)    # Exception
        except AssertionError:
            ex = sys.exc_info()[1]
            assert str(ex) == ""
        except Exception:
            assert Failse, "AssertionError expected"
        else:
            assert Failse, "AssertionError expected"


    def test_length(self):
        ok ("foo").length(3)
        ok ([]).length(0)
        ok ((1,2,3)).is_a(tuple).length(3)
        @be_fail("len('foo') == 4 : failed.")
        def fn(): ok ("foo").length(4)
        @be_fail("len([]) == 1 : failed.")
        def fn(): ok ([]).length(1)
        @be_fail("len((1, 2, 3)) == 4 : failed.")
        def fn(): ok ((1,2,3)).is_a(tuple).length(4)


    def test_is_file(self):
        fname = '__foobar.txt'
        dname = '__foobar.d'
        try:
            f = open(fname, 'w')
            f.write('foobar')
            f.close()
            os.mkdir(dname)
            #
            ok (fname).is_file()
            ok (dname).not_file()
            ok (dname).is_not_file()
            #
            @be_fail("os.path.isfile(%r) : failed." % dname)
            def fn(): ok (dname).is_file()
            @be_fail("os.path.isfile('not-exist') : failed.")
            def fn(): ok ('not-exist').is_file()
            @be_fail("not os.path.isfile(%r) : failed." % fname)
            def fn(): ok (fname).not_file()
            @be_fail("not os.path.isfile(%r) : failed." % fname)
            def fn(): ok (fname).is_not_file()
        finally:
            os.unlink(fname)
            os.rmdir(dname)


    def test_is_dir(self):
        fname = '__foobar.txt'
        dname = '__foobar.d'
        try:
            f = open(fname, 'w')
            f.write('foobar')
            f.close()
            os.mkdir(dname)
            #
            ok (dname).is_dir()
            ok (fname).not_dir()
            ok (fname).is_not_dir()
            #
            @be_fail("os.path.isdir(%r) : failed." % fname)
            def fn(): ok (fname).is_dir()
            @be_fail("os.path.isdir('not-exist') : failed.")
            def fn(): ok ('not-exist').is_dir()
            @be_fail("not os.path.isdir(%r) : failed." % dname)
            def fn(): ok (dname).not_dir()
            @be_fail("not os.path.isdir(%r) : failed." % dname)
            def fn(): ok (dname).is_not_dir()
        finally:
            os.unlink(fname)
            os.rmdir(dname)


    def test_exists(self):
        fname = '__foobar.txt'
        dname = '__foobar.d'
        try:
            f = open(fname, 'w')
            f.write('foobar')
            f.close()
            os.mkdir(dname)
            #
            ok (dname).exists()
            ok (fname).exists()
            ok ("not-found").not_exist()
            #
            @be_fail("os.path.exists('not-exist') : failed.")
            def fn(): ok ('not-exist').exists()
            @be_fail("not os.path.exists(%r) : failed." % fname)
            def fn(): ok (fname).not_exist()
            @be_fail("not os.path.exists(%r) : failed." % dname)
            def fn(): ok (dname).not_exist()
        finally:
            os.unlink(fname)
            os.rmdir(dname)


    def test_is_truthy(self):
        ok (True).is_truthy()
        ok (1).is_truthy()
        ok ([1]).is_truthy()
        @be_fail("bool(False) == True : failed.")
        def fn(): ok (False).is_truthy()
        @be_fail("bool(None) == True : failed.")
        def fn(): ok (None).is_truthy()
        @be_fail("bool('') == True : failed.")
        def fn(): ok ('').is_truthy()
        @be_fail("bool([]) == True : failed.")
        def fn(): ok ([]).is_truthy()
        @be_fail("bool({}) == True : failed.")
        def fn(): ok ({}).is_truthy()


    def test_is_falsy(self):
        ok (False).is_falsy()
        ok (None).is_falsy()
        ok (0).is_falsy()
        ok ('').is_falsy()
        ok ([]).is_falsy()
        ok ({}).is_falsy()
        @be_fail("bool(True) == False : failed.")
        def fn(): ok (True).is_falsy()
        @be_fail("bool(1) == False : failed.")
        def fn(): ok (1).is_falsy()
        @be_fail("bool('x') == False : failed.")
        def fn(): ok ('x').is_falsy()
        @be_fail("bool([1]) == False : failed.")
        def fn(): ok ([1]).is_falsy()
        @be_fail("bool({'x': 1}) == False : failed.")
        def fn(): ok ({'x':1}).is_falsy()


    ## ------------------------------------------------------------


    def test_not_ok(self):
        try:
            f = open('foobar.txt', 'w'); f.write('foobar'); f.close()
            os.mkdir('foobar.d')
            #
            not_ok ("xxxxxx.txt").is_file()
            not_ok ("foobar.d").is_file()
            not_ok ("foobar.txt").is_not_file()
            #
            not_ok ("xxxxxx.d").is_dir()
            not_ok ("foobar.txt").is_dir()
            not_ok ("foobar.d").is_not_dir()
            #
            not_ok ("foobar").matches("\d+")
            not_ok ("123").not_match("\d+")
        finally:
            os.unlink('foobar.txt')
            os.rmdir('foobar.d')


    def test_NG(self):
        try:
            f = open('foobar.txt', 'w'); f.write('foobar'); f.close()
            os.mkdir('foobar.d')
            #
            NG ("xxxxxx.txt").is_file()
            NG ("foobar.d").is_file()
            NG ("foobar.txt").is_not_file()
            #
            NG ("xxxxxx.d").is_dir()
            NG ("foobar.txt").is_dir()
            NG ("foobar.d").is_not_dir()
            #
            NG ("foobar").matches("\d+")
            NG ("123").not_match("\d+")
        finally:
            os.unlink('foobar.txt')
            os.rmdir('foobar.d')


    def test_fail(self):
        try:
            oktest.fail("description")
            self.faild("oktest.fail() should raise AssertionError but not")
        except:
            ex = sys.exc_info()[1]
            #self.assertEqual(AssertionError, type(ex))     # not work on Python 2.4
            self.assertTrue(isinstance(ex, AssertionError))
            self.assertEqual("description", str(ex))


    def test_should(self):
        ok ("foobar").should.startswith("foob").endswith("bar")
        @be_fail("'foobar'.startswith('aaa') : failed.")
        def fn(): ok ("foobar").should.startswith("aaa")
        #
        @be_error(AttributeError, "'str' object has no attribute 'start_with'")
        def fn(): ok ("foobar").should.start_with("foob")   # AttributeError
        @be_error(ValueError, "module.path: not a callable.")
        def fn(): ok (sys).should.path()
        @be_error(ValueError, "'Sasaki'.upper(): expected to return True or False but it returned 'SASAKI'.")
        def fn(): ok ("Sasaki").should.upper()


    def test_should_not(self):
        ok ("foobar").should_not.startswith("aaa")
        @be_fail("not 'foobar'.startswith('foo') : failed.")
        def fn(): ok ("foobar").should_not.startswith("foo")


    def test_assertion(self):
        @oktest.assertion
        def startswith_(self, arg):
            boolean = self.target.startswith(arg)
            if boolean != self.boolean:
                self.failed("%r.startswith(%r) : failed." % (self.target, arg))
        ok ("foobar").startswith_("foob")
        NG ("foobar").startswith_("a")
        @be_fail("'foobar'.startswith('afoo') : failed.")
        def fn(): ok ("foobar").startswith_("afoo")
        @be_fail("not 'foobar'.startswith('foo') : failed.")
        def fn(): NG ("foobar").startswith_("foo")


    def test_chained(self):
        ok ("Sasaki".upper()).is_a(str).matches(r'^[A-Z]+$') == "SASAKI"
        try:
            bkup = oktest.DIFF
            oktest.DIFF = False
            @be_fail("'SASAKI' == 'Sasaki' : failed.")
            def fn(): ok ("Sasaki".upper()).is_a(str).matches(r'^[A-Z]+$') == "Sasaki"
        finally:
            oktest.DIFF = bkup


    def test_unittest_compatibility(self):
        ## unittest compatibility
        desc = "unittest compatibility"
        script = r"""
from oktest import *
import sys
sys.stderr = sys.stdout
import unittest
class FooTest(unittest.TestCase):
  def test1(self):
    ok (1+1) == 2
  def test2(self):
    ok (1+1) == 3
unittest.main()
"""[1:]
        expected = r"""
.F
======================================================================
FAIL: test2 (__main__.FooTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "_test_.py", line 9, in test2
    ok (1+1) == 3
AssertionError: 2 == 3 : failed.

----------------------------------------------------------------------
Ran 2 tests in 0.000s

FAILED (failures=1)
"""[1:]
        #do_test_with(desc, script, expected)
        #output = run_script(script)
        output = run_script(script, True)
        output = re.sub(r'tests in 0\.(\d\d\d)s', 'tests in 0.000s', output)
        tupl = sys.version_info[:2]
        if (3,0) <= tupl <= (3,1):
            sys.stderr.write("\033[0;31m*** skip because not supported\033[0m\n")
            return
        self.assertEqual(expected, output)


    def test_checking_tested_or_not(self):
        ## checking tested or not
        desc = "checking tested or not"
        script = r"""
from oktest import *
import sys
sys.stderr = sys.stdout
import unittest
class FooTest(object):
  def test_1(self):
    ok (1+1) == 2
  def test_2(self):
    ok (1+1)
  def test_3(self):
    not_ok (1+1)
run()
"""[1:]
        expected = r"""
* FooTest.test_1 ... [ok]
* FooTest.test_2 ... *** warning: oktest: ok() is called but not tested. (file '_test_.py', line 9)
[ok]
* FooTest.test_3 ... *** warning: oktest: not_ok() is called but not tested. (file '_test_.py', line 11)
[ok]
"""[1:]
        #do_test_with(desc, script, expected)
        output = run_script(script)
        self.assertEqual(expected, output)


if __name__ == '__main__':
    unittest.main()
