""" Unit tests version 2.6.1.0 for adodbapi"""

"""
    adodbapi - A python DB API 2.0 interface to Microsoft ADO

    Copyright (C) 2002  Henrik Ekelund

    This library is free software; you can redistribute it and/or
    modify it under the terms of the GNU Lesser General Public
    License as published by the Free Software Foundation; either
    version 2.1 of the License, or (at your option) any later version.

    This library 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
    Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public
    License along with this library; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

    Updates by Vernon Cole
"""

import copy
import datetime
import decimal
import random
import string
import sys
import time
import unittest

import adodbapitestconfig as config  # run the configuration module. # will set sys.path to find correct version of adodbapi
import tryconnection  # in our code below, all our switches are from config.whatever
import win32com.client

import adodbapi
import adodbapi.apibase as api

try:
    import adodbapi.ado_consts as ado_consts
except ImportError:  # we are doing a shortcut import as a module -- so
    try:
        import ado_consts
    except ImportError:
        from adodbapi import ado_consts


def randomstring(length):
    return "".join([random.choice(string.ascii_letters) for n in range(32)])


class CommonDBTests(unittest.TestCase):
    "Self contained super-simple tests in easy syntax, should work on everything between mySQL and Oracle"

    def setUp(self):
        self.engine = "unknown"

    def getEngine(self):
        return self.engine

    def getConnection(self):
        raise NotImplementedError  # "This method must be overriden by a subclass"

    def getCursor(self):
        return self.getConnection().cursor()

    def testConnection(self):
        crsr = self.getCursor()
        assert crsr.__class__.__name__ == "Cursor"

    def testErrorHandlerInherits(self):
        conn = self.getConnection()
        mycallable = lambda connection, cursor, errorclass, errorvalue: 1
        conn.errorhandler = mycallable
        crsr = conn.cursor()
        assert (
            crsr.errorhandler == mycallable
        ), "Error handler on crsr should be same as on connection"

    def testDefaultErrorHandlerConnection(self):
        conn = self.getConnection()
        del conn.messages[:]
        try:
            conn.close()
            conn.commit()  # Should not be able to use connection after it is closed
        except:
            assert len(conn.messages) == 1
            assert len(conn.messages[0]) == 2
            assert conn.messages[0][0] == api.ProgrammingError

    def testOwnErrorHandlerConnection(self):
        mycallable = (
            lambda connection, cursor, errorclass, errorvalue: 1
        )  # does not raise anything
        conn = self.getConnection()
        conn.errorhandler = mycallable
        conn.close()
        conn.commit()  # Should not be able to use connection after it is closed
        assert len(conn.messages) == 0

        conn.errorhandler = None  # This should bring back the standard error handler
        try:
            conn.close()
            conn.commit()  # Should not be able to use connection after it is closed
        except:
            pass
        # The Standard errorhandler appends error to messages attribute
        assert (
            len(conn.messages) > 0
        ), "Setting errorhandler to none  should bring back the standard error handler"

    def testDefaultErrorHandlerCursor(self):
        crsr = self.getConnection().cursor()
        del crsr.messages[:]
        try:
            crsr.execute("SELECT abbtytddrf FROM dasdasd")
        except:
            assert len(crsr.messages) == 1
            assert len(crsr.messages[0]) == 2
            assert crsr.messages[0][0] == api.DatabaseError

    def testOwnErrorHandlerCursor(self):
        mycallable = (
            lambda connection, cursor, errorclass, errorvalue: 1
        )  # does not raise anything
        crsr = self.getConnection().cursor()
        crsr.errorhandler = mycallable
        crsr.execute("SELECT abbtytddrf FROM dasdasd")
        assert len(crsr.messages) == 0

        crsr.errorhandler = None  # This should bring back the standard error handler
        try:
            crsr.execute("SELECT abbtytddrf FROM dasdasd")
        except:
            pass
        # The Standard errorhandler appends error to messages attribute
        assert (
            len(crsr.messages) > 0
        ), "Setting errorhandler to none  should bring back the standard error handler"

    def testUserDefinedConversions(self):
        try:
            duplicatingConverter = lambda aStringField: aStringField * 2
            assert duplicatingConverter("gabba") == "gabbagabba"

            self.helpForceDropOnTblTemp()
            conn = self.getConnection()
            # the variantConversions attribute should not exist on a normal connection object
            self.assertRaises(AttributeError, lambda x: conn.variantConversions[x], [2])
            # create a variantConversions attribute on the connection
            conn.variantConversions = copy.copy(api.variantConversions)
            crsr = conn.cursor()
            tabdef = (
                "CREATE TABLE xx_%s (fldData VARCHAR(100) NOT NULL, fld2 VARCHAR(20))"
                % config.tmp
            )
            crsr.execute(tabdef)
            crsr.execute(
                "INSERT INTO xx_%s(fldData,fld2) VALUES('gabba','booga')" % config.tmp
            )
            crsr.execute(
                "INSERT INTO xx_%s(fldData,fld2) VALUES('hey','yo')" % config.tmp
            )
            # change converter for ALL adoStringTypes columns
            conn.variantConversions[api.adoStringTypes] = duplicatingConverter
            crsr.execute("SELECT fldData,fld2 FROM xx_%s ORDER BY fldData" % config.tmp)

            rows = crsr.fetchall()
            row = rows[0]
            self.assertEqual(row[0], "gabbagabba")
            row = rows[1]
            self.assertEqual(row[0], "heyhey")
            self.assertEqual(row[1], "yoyo")

            upcaseConverter = lambda aStringField: aStringField.upper()
            assert upcaseConverter("upThis") == "UPTHIS"

            # now use a single column converter
            rows.converters[1] = upcaseConverter  # convert second column
            self.assertEqual(row[0], "heyhey")  # first will be unchanged
            self.assertEqual(row[1], "YO")  # second will convert to upper case

        finally:
            try:
                del conn.variantConversions  # Restore the default
            except:
                pass
            self.helpRollbackTblTemp()

    def testUserDefinedConversionForExactNumericTypes(self):
        # variantConversions is a dictionary of conversion functions
        # held internally in adodbapi.apibase
        #
        # !!! this test intentionally alters the value of what should be constant in the module
        # !!! no new code should use this example, to is only a test to see that the
        # !!! deprecated way of doing this still works.  (use connection.variantConversions)
        #
        if sys.version_info < (3, 0):  ### Py3 need different test
            oldconverter = adodbapi.variantConversions[
                ado_consts.adNumeric
            ]  # keep old function to restore later
            # By default decimal and "numbers" are returned as decimals.
            # Instead, make numbers return as  floats
            try:
                adodbapi.variantConversions[ado_consts.adNumeric] = adodbapi.cvtFloat
                self.helpTestDataType(
                    "decimal(18,2)", "NUMBER", 3.45, compareAlmostEqual=1
                )
                self.helpTestDataType(
                    "numeric(18,2)", "NUMBER", 3.45, compareAlmostEqual=1
                )
                # now return strings
                adodbapi.variantConversions[ado_consts.adNumeric] = adodbapi.cvtString
                self.helpTestDataType("numeric(18,2)", "NUMBER", "3.45")
                # now a completly weird user defined convertion
                adodbapi.variantConversions[ado_consts.adNumeric] = (
                    lambda x: "!!This function returns a funny unicode string %s!!" % x
                )
                self.helpTestDataType(
                    "numeric(18,2)",
                    "NUMBER",
                    "3.45",
                    allowedReturnValues=[
                        "!!This function returns a funny unicode string 3.45!!"
                    ],
                )
            finally:
                # now reset the converter to its original function
                adodbapi.variantConversions[ado_consts.adNumeric] = (
                    oldconverter  # Restore the original convertion function
                )

    def helpTestDataType(
        self,
        sqlDataTypeString,
        DBAPIDataTypeString,
        pyData,
        pyDataInputAlternatives=None,
        compareAlmostEqual=None,
        allowedReturnValues=None,
    ):
        self.helpForceDropOnTblTemp()
        conn = self.getConnection()
        crsr = conn.cursor()
        tabdef = (
            """
            CREATE TABLE xx_%s (
                fldId integer NOT NULL,
                fldData """
            % config.tmp
            + sqlDataTypeString
            + ")\n"
        )

        crsr.execute(tabdef)

        # Test Null values mapped to None
        crsr.execute("INSERT INTO xx_%s (fldId) VALUES (1)" % config.tmp)

        crsr.execute("SELECT fldId,fldData FROM xx_%s" % config.tmp)
        rs = crsr.fetchone()
        self.assertEqual(rs[1], None)  # Null should be mapped to None
        assert rs[0] == 1

        # Test description related
        descTuple = crsr.description[1]
        assert descTuple[0] in ["fldData", "flddata"], 'was "%s" expected "%s"' % (
            descTuple[0],
            "fldData",
        )

        if DBAPIDataTypeString == "STRING":
            assert descTuple[1] == api.STRING, 'was "%s" expected "%s"' % (
                descTuple[1],
                api.STRING.values,
            )
        elif DBAPIDataTypeString == "NUMBER":
            assert descTuple[1] == api.NUMBER, 'was "%s" expected "%s"' % (
                descTuple[1],
                api.NUMBER.values,
            )
        elif DBAPIDataTypeString == "BINARY":
            assert descTuple[1] == api.BINARY, 'was "%s" expected "%s"' % (
                descTuple[1],
                api.BINARY.values,
            )
        elif DBAPIDataTypeString == "DATETIME":
            assert descTuple[1] == api.DATETIME, 'was "%s" expected "%s"' % (
                descTuple[1],
                api.DATETIME.values,
            )
        elif DBAPIDataTypeString == "ROWID":
            assert descTuple[1] == api.ROWID, 'was "%s" expected "%s"' % (
                descTuple[1],
                api.ROWID.values,
            )
        elif DBAPIDataTypeString == "UUID":
            assert descTuple[1] == api.OTHER, 'was "%s" expected "%s"' % (
                descTuple[1],
                api.OTHER.values,
            )
        else:
            raise NotImplementedError  # "DBAPIDataTypeString not provided"

        # Test data binding
        inputs = [pyData]
        if pyDataInputAlternatives:
            inputs.extend(pyDataInputAlternatives)
        inputs = set(inputs)  # removes redundant string==unicode tests
        fldId = 1
        for inParam in inputs:
            fldId += 1
            try:
                crsr.execute(
                    "INSERT INTO xx_%s (fldId,fldData) VALUES (?,?)" % config.tmp,
                    (fldId, inParam),
                )
            except:
                conn.printADOerrors()
                raise
            crsr.execute(
                "SELECT fldData FROM xx_%s WHERE ?=fldID" % config.tmp, [fldId]
            )
            rs = crsr.fetchone()
            if allowedReturnValues:
                allowedTypes = tuple([type(aRV) for aRV in allowedReturnValues])
                assert isinstance(
                    rs[0], allowedTypes
                ), 'result type "%s" must be one of %s' % (type(rs[0]), allowedTypes)
            else:
                assert isinstance(
                    rs[0], type(pyData)
                ), 'result type "%s" must be instance of %s' % (
                    type(rs[0]),
                    type(pyData),
                )

            if compareAlmostEqual and DBAPIDataTypeString == "DATETIME":
                iso1 = adodbapi.dateconverter.DateObjectToIsoFormatString(rs[0])
                iso2 = adodbapi.dateconverter.DateObjectToIsoFormatString(pyData)
                self.assertEqual(iso1, iso2)
            elif compareAlmostEqual:
                s = float(pyData)
                v = float(rs[0])
                assert (
                    abs(v - s) / s < 0.00001
                ), "Values not almost equal recvd=%s, expected=%f" % (rs[0], s)
            else:
                if allowedReturnValues:
                    ok = False
                    self.assertTrue(
                        rs[0] in allowedReturnValues,
                        f'Value "{rs[0]!r}" not in {allowedReturnValues}',
                    )
                else:
                    self.assertEqual(
                        rs[0],
                        pyData,
                        'Values are not equal recvd="%s", expected="%s"'
                        % (rs[0], pyData),
                    )

    def testDataTypeFloat(self):
        self.helpTestDataType("real", "NUMBER", 3.45, compareAlmostEqual=True)
        self.helpTestDataType("float", "NUMBER", 1.79e37, compareAlmostEqual=True)

    def testDataTypeDecmal(self):
        self.helpTestDataType(
            "decimal(18,2)",
            "NUMBER",
            3.45,
            allowedReturnValues=["3.45", "3,45", decimal.Decimal("3.45")],
        )
        self.helpTestDataType(
            "numeric(18,2)",
            "NUMBER",
            3.45,
            allowedReturnValues=["3.45", "3,45", decimal.Decimal("3.45")],
        )
        self.helpTestDataType(
            "decimal(20,2)",
            "NUMBER",
            444444444444444444,
            allowedReturnValues=[
                "444444444444444444.00",
                "444444444444444444,00",
                decimal.Decimal("444444444444444444"),
            ],
        )
        if self.getEngine() == "MSSQL":
            self.helpTestDataType(
                "uniqueidentifier",
                "UUID",
                "{71A4F49E-39F3-42B1-A41E-48FF154996E6}",
                allowedReturnValues=["{71A4F49E-39F3-42B1-A41E-48FF154996E6}"],
            )

    def testDataTypeMoney(self):  # v2.1 Cole -- use decimal for money
        if self.getEngine() == "MySQL":
            self.helpTestDataType(
                "DECIMAL(20,4)", "NUMBER", decimal.Decimal("-922337203685477.5808")
            )
        elif self.getEngine() == "PostgreSQL":
            self.helpTestDataType(
                "money",
                "NUMBER",
                decimal.Decimal("-922337203685477.5808"),
                compareAlmostEqual=True,
                allowedReturnValues=[
                    -922337203685477.5808,
                    decimal.Decimal("-922337203685477.5808"),
                ],
            )
        else:
            self.helpTestDataType("smallmoney", "NUMBER", decimal.Decimal("214748.02"))
            self.helpTestDataType(
                "money", "NUMBER", decimal.Decimal("-922337203685477.5808")
            )

    def testDataTypeInt(self):
        if self.getEngine() != "PostgreSQL":
            self.helpTestDataType("tinyint", "NUMBER", 115)
        self.helpTestDataType("smallint", "NUMBER", -32768)
        if self.getEngine() not in ["ACCESS", "PostgreSQL"]:
            self.helpTestDataType(
                "bit", "NUMBER", 1
            )  # Does not work correctly with access
        if self.getEngine() in ["MSSQL", "PostgreSQL"]:
            self.helpTestDataType(
                "bigint",
                "NUMBER",
                3000000000,
                allowedReturnValues=[3000000000, int(3000000000)],
            )
        self.helpTestDataType("int", "NUMBER", 2147483647)

    def testDataTypeChar(self):
        for sqlDataType in ("char(6)", "nchar(6)"):
            self.helpTestDataType(
                sqlDataType,
                "STRING",
                "spam  ",
                allowedReturnValues=["spam", "spam", "spam  ", "spam  "],
            )

    def testDataTypeVarChar(self):
        if self.getEngine() == "MySQL":
            stringKinds = ["varchar(10)", "text"]
        elif self.getEngine() == "PostgreSQL":
            stringKinds = ["varchar(10)", "text", "character varying"]
        else:
            stringKinds = [
                "varchar(10)",
                "nvarchar(10)",
                "text",
                "ntext",
            ]  # ,"varchar(max)"]

        for sqlDataType in stringKinds:
            self.helpTestDataType(sqlDataType, "STRING", "spam", ["spam"])

    def testDataTypeDate(self):
        if self.getEngine() == "PostgreSQL":
            dt = "timestamp"
        else:
            dt = "datetime"
        self.helpTestDataType(
            dt, "DATETIME", adodbapi.Date(2002, 10, 28), compareAlmostEqual=True
        )
        if self.getEngine() not in ["MySQL", "PostgreSQL"]:
            self.helpTestDataType(
                "smalldatetime",
                "DATETIME",
                adodbapi.Date(2002, 10, 28),
                compareAlmostEqual=True,
            )
        if tag != "pythontime" and self.getEngine() not in [
            "MySQL",
            "PostgreSQL",
        ]:  # fails when using pythonTime
            self.helpTestDataType(
                dt,
                "DATETIME",
                adodbapi.Timestamp(2002, 10, 28, 12, 15, 1),
                compareAlmostEqual=True,
            )

    def testDataTypeBinary(self):
        binfld = b"\x07\x00\xE2\x40*"
        arv = [binfld, adodbapi.Binary(binfld), bytes(binfld)]
        if self.getEngine() == "PostgreSQL":
            self.helpTestDataType(
                "bytea", "BINARY", adodbapi.Binary(binfld), allowedReturnValues=arv
            )
        else:
            self.helpTestDataType(
                "binary(5)", "BINARY", adodbapi.Binary(binfld), allowedReturnValues=arv
            )
            self.helpTestDataType(
                "varbinary(100)",
                "BINARY",
                adodbapi.Binary(binfld),
                allowedReturnValues=arv,
            )
            if self.getEngine() != "MySQL":
                self.helpTestDataType(
                    "image", "BINARY", adodbapi.Binary(binfld), allowedReturnValues=arv
                )

    def helpRollbackTblTemp(self):
        self.helpForceDropOnTblTemp()

    def helpForceDropOnTblTemp(self):
        conn = self.getConnection()
        with conn.cursor() as crsr:
            try:
                crsr.execute("DROP TABLE xx_%s" % config.tmp)
                if not conn.autocommit:
                    conn.commit()
            except:
                pass

    def helpCreateAndPopulateTableTemp(self, crsr):
        tabdef = (
            """
            CREATE TABLE xx_%s (
                fldData INTEGER
            )
            """
            % config.tmp
        )
        try:  # EAFP
            crsr.execute(tabdef)
        except api.DatabaseError:  # was not dropped before
            self.helpForceDropOnTblTemp()  # so drop it now
            crsr.execute(tabdef)
        for i in range(9):  # note: this poor SQL code, but a valid test
            crsr.execute("INSERT INTO xx_%s (fldData) VALUES (%i)" % (config.tmp, i))
            # NOTE: building the test table without using parameter substitution

    def testFetchAll(self):
        crsr = self.getCursor()
        self.helpCreateAndPopulateTableTemp(crsr)
        crsr.execute("SELECT fldData FROM xx_%s" % config.tmp)
        rs = crsr.fetchall()
        assert len(rs) == 9
        # test slice of rows
        i = 3
        for row in rs[3:-2]:  # should have rowid 3..6
            assert row[0] == i
            i += 1
        self.helpRollbackTblTemp()

    def testPreparedStatement(self):
        crsr = self.getCursor()
        self.helpCreateAndPopulateTableTemp(crsr)
        crsr.prepare("SELECT fldData FROM xx_%s" % config.tmp)
        crsr.execute(crsr.command)  # remember the one that was prepared
        rs = crsr.fetchall()
        assert len(rs) == 9
        assert rs[2][0] == 2
        self.helpRollbackTblTemp()

    def testWrongPreparedStatement(self):
        crsr = self.getCursor()
        self.helpCreateAndPopulateTableTemp(crsr)
        crsr.prepare("SELECT * FROM nowhere")
        crsr.execute(
            "SELECT fldData FROM xx_%s" % config.tmp
        )  # should execute this one, not the prepared one
        rs = crsr.fetchall()
        assert len(rs) == 9
        assert rs[2][0] == 2
        self.helpRollbackTblTemp()

    def testIterator(self):
        crsr = self.getCursor()
        self.helpCreateAndPopulateTableTemp(crsr)
        crsr.execute("SELECT fldData FROM xx_%s" % config.tmp)
        for i, row in enumerate(
            crsr
        ):  # using cursor as an iterator, rather than fetchxxx
            assert row[0] == i
        self.helpRollbackTblTemp()

    def testExecuteMany(self):
        crsr = self.getCursor()
        self.helpCreateAndPopulateTableTemp(crsr)
        seq_of_values = [(111,), (222,)]
        crsr.executemany(
            "INSERT INTO xx_%s (fldData) VALUES (?)" % config.tmp, seq_of_values
        )
        if crsr.rowcount == -1:
            print(
                self.getEngine()
                + " Provider does not support rowcount (on .executemany())"
            )
        else:
            self.assertEqual(crsr.rowcount, 2)
        crsr.execute("SELECT fldData FROM xx_%s" % config.tmp)
        rs = crsr.fetchall()
        assert len(rs) == 11
        self.helpRollbackTblTemp()

    def testRowCount(self):
        crsr = self.getCursor()
        self.helpCreateAndPopulateTableTemp(crsr)
        crsr.execute("SELECT fldData FROM xx_%s" % config.tmp)
        if crsr.rowcount == -1:
            # print("provider does not support rowcount on select")
            pass
        else:
            self.assertEqual(crsr.rowcount, 9)
        self.helpRollbackTblTemp()

    def testRowCountNoRecordset(self):
        crsr = self.getCursor()
        self.helpCreateAndPopulateTableTemp(crsr)
        crsr.execute("DELETE FROM xx_%s WHERE fldData >= 5" % config.tmp)
        if crsr.rowcount == -1:
            print(self.getEngine() + " Provider does not support rowcount (on DELETE)")
        else:
            self.assertEqual(crsr.rowcount, 4)
        self.helpRollbackTblTemp()

    def testFetchMany(self):
        crsr = self.getCursor()
        self.helpCreateAndPopulateTableTemp(crsr)
        crsr.execute("SELECT fldData FROM xx_%s" % config.tmp)
        rs = crsr.fetchmany(3)
        assert len(rs) == 3
        rs = crsr.fetchmany(5)
        assert len(rs) == 5
        rs = crsr.fetchmany(5)
        assert len(rs) == 1  # Asked for five, but there is only one left
        self.helpRollbackTblTemp()

    def testFetchManyWithArraySize(self):
        crsr = self.getCursor()
        self.helpCreateAndPopulateTableTemp(crsr)
        crsr.execute("SELECT fldData FROM xx_%s" % config.tmp)
        rs = crsr.fetchmany()
        assert len(rs) == 1  # arraysize Defaults to one
        crsr.arraysize = 4
        rs = crsr.fetchmany()
        assert len(rs) == 4
        rs = crsr.fetchmany()
        assert len(rs) == 4
        rs = crsr.fetchmany()
        assert len(rs) == 0
        self.helpRollbackTblTemp()

    def testErrorConnect(self):
        conn = self.getConnection()
        conn.close()
        self.assertRaises(api.DatabaseError, self.db, "not a valid connect string", {})

    def testRowIterator(self):
        self.helpForceDropOnTblTemp()
        conn = self.getConnection()
        crsr = conn.cursor()
        tabdef = (
            """
            CREATE TABLE xx_%s (
                fldId integer NOT NULL,
                fldTwo integer,
                fldThree integer,
                fldFour integer)
                """
            % config.tmp
        )
        crsr.execute(tabdef)

        inputs = [(2, 3, 4), (102, 103, 104)]
        fldId = 1
        for inParam in inputs:
            fldId += 1
            try:
                crsr.execute(
                    "INSERT INTO xx_%s (fldId,fldTwo,fldThree,fldFour) VALUES (?,?,?,?)"
                    % config.tmp,
                    (fldId, inParam[0], inParam[1], inParam[2]),
                )
            except:
                conn.printADOerrors()
                raise
            crsr.execute(
                "SELECT fldTwo,fldThree,fldFour FROM xx_%s WHERE ?=fldID" % config.tmp,
                [fldId],
            )
            rec = crsr.fetchone()
            # check that stepping through an emulated row works
            for j in range(len(inParam)):
                assert (
                    rec[j] == inParam[j]
                ), 'returned value:"%s" != test value:"%s"' % (rec[j], inParam[j])
            # check that we can get a complete tuple from a row
            assert (
                tuple(rec) == inParam
            ), f'returned value:"{rec!r}" != test value:"{inParam!r}"'
            # test that slices of rows work
            slice1 = tuple(rec[:-1])
            slice2 = tuple(inParam[0:2])
            assert (
                slice1 == slice2
            ), f'returned value:"{slice1!r}" != test value:"{slice2!r}"'
            # now test named column retrieval
            assert rec["fldTwo"] == inParam[0]
            assert rec.fldThree == inParam[1]
            assert rec.fldFour == inParam[2]
        # test array operation
        # note that the fields vv        vv     vv    are out of order
        crsr.execute("select fldThree,fldFour,fldTwo from xx_%s" % config.tmp)
        recs = crsr.fetchall()
        assert recs[1][0] == 103
        assert recs[0][1] == 4
        assert recs[1]["fldFour"] == 104
        assert recs[0, 0] == 3
        assert recs[0, "fldTwo"] == 2
        assert recs[1, 2] == 102
        for i in range(1):
            for j in range(2):
                assert recs[i][j] == recs[i, j]

    def testFormatParamstyle(self):
        self.helpForceDropOnTblTemp()
        conn = self.getConnection()
        conn.paramstyle = "format"  # test nonstandard use of paramstyle
        crsr = conn.cursor()
        tabdef = (
            """
            CREATE TABLE xx_%s (
                fldId integer NOT NULL,
                fldData varchar(10),
                fldConst varchar(30))
                """
            % config.tmp
        )
        crsr.execute(tabdef)

        inputs = ["one", "two", "three"]
        fldId = 2
        for inParam in inputs:
            fldId += 1
            sql = (
                "INSERT INTO xx_"
                + config.tmp
                + " (fldId,fldConst,fldData) VALUES (%s,'thi%s :may cause? trouble', %s)"
            )
            try:
                crsr.execute(sql, (fldId, inParam))
            except:
                conn.printADOerrors()
                raise
            crsr.execute(
                "SELECT fldData, fldConst FROM xx_" + config.tmp + " WHERE %s=fldID",
                [fldId],
            )
            rec = crsr.fetchone()
            self.assertEqual(
                rec[0],
                inParam,
                'returned value:"%s" != test value:"%s"' % (rec[0], inParam),
            )
            self.assertEqual(rec[1], "thi%s :may cause? trouble")

        # now try an operation with a "%s" as part of a literal
        sel = (
            "insert into xx_" + config.tmp + " (fldId,fldData) VALUES (%s,'four%sfive')"
        )
        params = (20,)
        crsr.execute(sel, params)

        # test the .query implementation
        assert "(?," in crsr.query, 'expected:"%s" in "%s"' % ("(?,", crsr.query)
        # test the .command attribute
        assert crsr.command == sel, 'expected:"%s" but found "%s"' % (sel, crsr.command)

        # test the .parameters attribute
        self.assertEqual(crsr.parameters, params)
        # now make sure the data made it
        crsr.execute("SELECT fldData FROM xx_%s WHERE fldID=20" % config.tmp)
        rec = crsr.fetchone()
        self.assertEqual(rec[0], "four%sfive")

    def testNamedParamstyle(self):
        self.helpForceDropOnTblTemp()
        conn = self.getConnection()
        crsr = conn.cursor()
        crsr.paramstyle = "named"  # test nonstandard use of paramstyle
        tabdef = (
            """
            CREATE TABLE xx_%s (
                fldId integer NOT NULL,
                fldData varchar(10))
                """
            % config.tmp
        )
        crsr.execute(tabdef)

        inputs = ["four", "five", "six"]
        fldId = 10
        for inParam in inputs:
            fldId += 1
            try:
                crsr.execute(
                    "INSERT INTO xx_%s (fldId,fldData) VALUES (:Id,:f_Val)"
                    % config.tmp,
                    {"f_Val": inParam, "Id": fldId},
                )
            except:
                conn.printADOerrors()
                raise
            crsr.execute(
                "SELECT fldData FROM xx_%s WHERE fldID=:Id" % config.tmp, {"Id": fldId}
            )
            rec = crsr.fetchone()
            self.assertEqual(
                rec[0],
                inParam,
                'returned value:"%s" != test value:"%s"' % (rec[0], inParam),
            )
        # now a test with a ":" as part of a literal
        crsr.execute(
            "insert into xx_%s (fldId,fldData) VALUES (:xyz,'six:five')" % config.tmp,
            {"xyz": 30},
        )
        crsr.execute("SELECT fldData FROM xx_%s WHERE fldID=30" % config.tmp)
        rec = crsr.fetchone()
        self.assertEqual(rec[0], "six:five")

    def testPyformatParamstyle(self):
        self.helpForceDropOnTblTemp()
        conn = self.getConnection()
        crsr = conn.cursor()
        crsr.paramstyle = "pyformat"  # test nonstandard use of paramstyle
        tabdef = (
            """
            CREATE TABLE xx_%s (
                fldId integer NOT NULL,
                fldData varchar(10))
                """
            % config.tmp
        )
        crsr.execute(tabdef)

        inputs = ["four", "five", "six"]
        fldId = 10
        for inParam in inputs:
            fldId += 1
            try:
                crsr.execute(
                    "INSERT INTO xx_%s (fldId,fldData) VALUES (%%(Id)s,%%(f_Val)s)"
                    % config.tmp,
                    {"f_Val": inParam, "Id": fldId},
                )
            except:
                conn.printADOerrors()
                raise
            crsr.execute(
                "SELECT fldData FROM xx_%s WHERE fldID=%%(Id)s" % config.tmp,
                {"Id": fldId},
            )
            rec = crsr.fetchone()
            self.assertEqual(
                rec[0],
                inParam,
                'returned value:"%s" != test value:"%s"' % (rec[0], inParam),
            )
        # now a test with a "%" as part of a literal
        crsr.execute(
            "insert into xx_%s (fldId,fldData) VALUES (%%(xyz)s,'six%%five')"
            % config.tmp,
            {"xyz": 30},
        )
        crsr.execute("SELECT fldData FROM xx_%s WHERE fldID=30" % config.tmp)
        rec = crsr.fetchone()
        self.assertEqual(rec[0], "six%five")

    def testAutomaticParamstyle(self):
        self.helpForceDropOnTblTemp()
        conn = self.getConnection()
        conn.paramstyle = "dynamic"  # test nonstandard use of paramstyle
        crsr = conn.cursor()
        tabdef = (
            """
            CREATE TABLE xx_%s (
                fldId integer NOT NULL,
                fldData varchar(10),
                fldConst varchar(30))
                """
            % config.tmp
        )
        crsr.execute(tabdef)
        inputs = ["one", "two", "three"]
        fldId = 2
        for inParam in inputs:
            fldId += 1
            try:
                crsr.execute(
                    "INSERT INTO xx_"
                    + config.tmp
                    + " (fldId,fldConst,fldData) VALUES (?,'thi%s :may cause? troub:1e', ?)",
                    (fldId, inParam),
                )
            except:
                conn.printADOerrors()
                raise
            trouble = "thi%s :may cause? troub:1e"
            crsr.execute(
                "SELECT fldData, fldConst FROM xx_" + config.tmp + " WHERE ?=fldID",
                [fldId],
            )
            rec = crsr.fetchone()
            self.assertEqual(
                rec[0],
                inParam,
                'returned value:"%s" != test value:"%s"' % (rec[0], inParam),
            )
            self.assertEqual(rec[1], trouble)
        #     inputs = [u'four',u'five',u'six']
        fldId = 10
        for inParam in inputs:
            fldId += 1
            try:
                crsr.execute(
                    "INSERT INTO xx_%s (fldId,fldData) VALUES (:Id,:f_Val)"
                    % config.tmp,
                    {"f_Val": inParam, "Id": fldId},
                )
            except:
                conn.printADOerrors()
                raise
            crsr.execute(
                "SELECT fldData FROM xx_%s WHERE :Id=fldID" % config.tmp, {"Id": fldId}
            )
            rec = crsr.fetchone()
            self.assertEqual(
                rec[0],
                inParam,
                'returned value:"%s" != test value:"%s"' % (rec[0], inParam),
            )
        # now a test with a ":" as part of a literal -- and use a prepared query
        ppdcmd = (
            "insert into xx_%s (fldId,fldData) VALUES (:xyz,'six:five')" % config.tmp
        )
        crsr.prepare(ppdcmd)
        crsr.execute(ppdcmd, {"xyz": 30})
        crsr.execute("SELECT fldData FROM xx_%s WHERE fldID=30" % config.tmp)
        rec = crsr.fetchone()
        self.assertEqual(rec[0], "six:five")

    def testRollBack(self):
        conn = self.getConnection()
        crsr = conn.cursor()
        assert not crsr.connection.autocommit, "Unexpected beginning condition"
        self.helpCreateAndPopulateTableTemp(crsr)
        crsr.connection.commit()  # commit the first bunch

        crsr.execute("INSERT INTO xx_%s (fldData) VALUES(100)" % config.tmp)

        selectSql = "SELECT fldData FROM xx_%s WHERE fldData=100" % config.tmp
        crsr.execute(selectSql)
        rs = crsr.fetchall()
        assert len(rs) == 1
        self.conn.rollback()
        crsr.execute(selectSql)
        assert (
            crsr.fetchone() is None
        ), "cursor.fetchone should return None if a query retrieves no rows"
        crsr.execute("SELECT fldData from xx_%s" % config.tmp)
        rs = crsr.fetchall()
        assert len(rs) == 9, "the original records should still be present"
        self.helpRollbackTblTemp()

    def testCommit(self):
        try:
            con2 = self.getAnotherConnection()
        except NotImplementedError:
            return  # should be "SKIP" for ACCESS
        assert not con2.autocommit, "default should be manual commit"
        crsr = con2.cursor()
        self.helpCreateAndPopulateTableTemp(crsr)

        crsr.execute("INSERT INTO xx_%s (fldData) VALUES(100)" % config.tmp)
        con2.commit()

        selectSql = "SELECT fldData FROM xx_%s WHERE fldData=100" % config.tmp
        crsr.execute(selectSql)
        rs = crsr.fetchall()
        assert len(rs) == 1
        crsr.close()
        con2.close()
        conn = self.getConnection()
        crsr = self.getCursor()
        with conn.cursor() as crsr:
            crsr.execute(selectSql)
            rs = crsr.fetchall()
            assert len(rs) == 1
            assert rs[0][0] == 100
        self.helpRollbackTblTemp()

    def testAutoRollback(self):
        try:
            con2 = self.getAnotherConnection()
        except NotImplementedError:
            return  # should be "SKIP" for ACCESS
        assert not con2.autocommit, "unexpected beginning condition"
        crsr = con2.cursor()
        self.helpCreateAndPopulateTableTemp(crsr)
        crsr.execute("INSERT INTO xx_%s (fldData) VALUES(100)" % config.tmp)
        selectSql = "SELECT fldData FROM xx_%s WHERE fldData=100" % config.tmp
        crsr.execute(selectSql)
        rs = crsr.fetchall()
        assert len(rs) == 1
        crsr.close()
        con2.close()
        crsr = self.getCursor()
        try:
            crsr.execute(
                selectSql
            )  # closing the connection should have forced rollback
            row = crsr.fetchone()
        except api.DatabaseError:
            row = None  # if the entire table disappeared the rollback was perfect and the test passed
        assert (
            row is None
        ), f"cursor.fetchone should return None if a query retrieves no rows. Got {row!r}"
        self.helpRollbackTblTemp()

    def testAutoCommit(self):
        try:
            ac_conn = self.getAnotherConnection({"autocommit": True})
        except NotImplementedError:
            return  # should be "SKIP" for ACCESS
        crsr = ac_conn.cursor()
        self.helpCreateAndPopulateTableTemp(crsr)
        crsr.execute("INSERT INTO xx_%s (fldData) VALUES(100)" % config.tmp)
        crsr.close()
        with self.getCursor() as crsr:
            selectSql = "SELECT fldData from xx_%s" % config.tmp
            crsr.execute(
                selectSql
            )  # closing the connection should _not_ have forced rollback
            rs = crsr.fetchall()
            assert len(rs) == 10, "all records should still be present"
        ac_conn.close()
        self.helpRollbackTblTemp()

    def testSwitchedAutoCommit(self):
        try:
            ac_conn = self.getAnotherConnection()
        except NotImplementedError:
            return  # should be "SKIP" for ACCESS
        ac_conn.autocommit = True
        crsr = ac_conn.cursor()
        self.helpCreateAndPopulateTableTemp(crsr)
        crsr.execute("INSERT INTO xx_%s (fldData) VALUES(100)" % config.tmp)
        crsr.close()
        conn = self.getConnection()
        ac_conn.close()
        with self.getCursor() as crsr:
            selectSql = "SELECT fldData from xx_%s" % config.tmp
            crsr.execute(
                selectSql
            )  # closing the connection should _not_ have forced rollback
            rs = crsr.fetchall()
            assert len(rs) == 10, "all records should still be present"
        self.helpRollbackTblTemp()

    def testExtendedTypeHandling(self):
        class XtendString(str):
            pass

        class XtendInt(int):
            pass

        class XtendFloat(float):
            pass

        xs = XtendString(randomstring(30))
        xi = XtendInt(random.randint(-100, 500))
        xf = XtendFloat(random.random())
        self.helpForceDropOnTblTemp()
        conn = self.getConnection()
        crsr = conn.cursor()
        tabdef = (
            """
            CREATE TABLE xx_%s (
                s VARCHAR(40) NOT NULL,
                i INTEGER NOT NULL,
                f REAL NOT NULL)"""
            % config.tmp
        )
        crsr.execute(tabdef)
        crsr.execute(
            "INSERT INTO xx_%s (s, i, f) VALUES (?, ?, ?)" % config.tmp, (xs, xi, xf)
        )
        crsr.close()
        conn = self.getConnection()
        with self.getCursor() as crsr:
            selectSql = "SELECT s, i, f from xx_%s" % config.tmp
            crsr.execute(
                selectSql
            )  # closing the connection should _not_ have forced rollback
            row = crsr.fetchone()
            self.assertEqual(row.s, xs)
            self.assertEqual(row.i, xi)
            self.assertAlmostEqual(row.f, xf)
        self.helpRollbackTblTemp()


class TestADOwithSQLServer(CommonDBTests):
    def setUp(self):
        self.conn = config.dbSqlServerconnect(
            *config.connStrSQLServer[0], **config.connStrSQLServer[1]
        )
        self.conn.timeout = 30  # turn timeout back up
        self.engine = "MSSQL"
        self.db = config.dbSqlServerconnect

    def tearDown(self):
        try:
            self.conn.rollback()
        except:
            pass
        try:
            self.conn.close()
        except:
            pass
        self.conn = None

    def getConnection(self):
        return self.conn

    def getAnotherConnection(self, addkeys=None):
        keys = dict(config.connStrSQLServer[1])
        if addkeys:
            keys.update(addkeys)
        return config.dbSqlServerconnect(*config.connStrSQLServer[0], **keys)

    def testVariableReturningStoredProcedure(self):
        crsr = self.conn.cursor()
        spdef = """
            CREATE PROCEDURE sp_DeleteMeOnlyForTesting
                @theInput varchar(50),
                @theOtherInput varchar(50),
                @theOutput varchar(100) OUTPUT
            AS
                SET @theOutput=@theInput+@theOtherInput
                    """
        try:
            crsr.execute("DROP PROCEDURE sp_DeleteMeOnlyForTesting")
            self.conn.commit()
        except:  # Make sure it is empty
            pass
        crsr.execute(spdef)

        retvalues = crsr.callproc(
            "sp_DeleteMeOnlyForTesting", ("Dodsworth", "Anne", "              ")
        )
        assert retvalues[0] == "Dodsworth", f'{retvalues[0]!r} is not "Dodsworth"'
        assert retvalues[1] == "Anne", f'{retvalues[1]!r} is not "Anne"'
        assert (
            retvalues[2] == "DodsworthAnne"
        ), f'{retvalues[2]!r} is not "DodsworthAnne"'
        self.conn.rollback()

    def testMultipleSetReturn(self):
        crsr = self.getCursor()
        self.helpCreateAndPopulateTableTemp(crsr)

        spdef = """
            CREATE PROCEDURE sp_DeleteMe_OnlyForTesting
            AS
                SELECT fldData FROM xx_%s ORDER BY fldData ASC
                SELECT fldData From xx_%s where fldData = -9999
                SELECT fldData FROM xx_%s ORDER BY fldData DESC
                    """ % (
            config.tmp,
            config.tmp,
            config.tmp,
        )
        try:
            crsr.execute("DROP PROCEDURE sp_DeleteMe_OnlyForTesting")
            self.conn.commit()
        except:  # Make sure it is empty
            pass
        crsr.execute(spdef)

        retvalues = crsr.callproc("sp_DeleteMe_OnlyForTesting")
        row = crsr.fetchone()
        self.assertEqual(row[0], 0)
        assert crsr.nextset() == True, "Operation should succeed"
        assert not crsr.fetchall(), "Should be an empty second set"
        assert crsr.nextset() == True, "third set should be present"
        rowdesc = crsr.fetchall()
        self.assertEqual(rowdesc[0][0], 8)
        assert crsr.nextset() is None, "No more return sets, should return None"

        self.helpRollbackTblTemp()

    def testDatetimeProcedureParameter(self):
        crsr = self.conn.cursor()
        spdef = """
            CREATE PROCEDURE sp_DeleteMeOnlyForTesting
                @theInput DATETIME,
                @theOtherInput varchar(50),
                @theOutput varchar(100) OUTPUT
            AS
                SET @theOutput = CONVERT(CHARACTER(20), @theInput, 0) + @theOtherInput
                    """
        try:
            crsr.execute("DROP PROCEDURE sp_DeleteMeOnlyForTesting")
            self.conn.commit()
        except:  # Make sure it is empty
            pass
        crsr.execute(spdef)

        result = crsr.callproc(
            "sp_DeleteMeOnlyForTesting",
            [adodbapi.Timestamp(2014, 12, 25, 0, 1, 0), "Beep", " " * 30],
        )

        assert result[2] == "Dec 25 2014 12:01AM Beep", 'value was="%s"' % result[2]
        self.conn.rollback()

    def testIncorrectStoredProcedureParameter(self):
        crsr = self.conn.cursor()
        spdef = """
            CREATE PROCEDURE sp_DeleteMeOnlyForTesting
                @theInput DATETIME,
                @theOtherInput varchar(50),
                @theOutput varchar(100) OUTPUT
            AS
                SET @theOutput = CONVERT(CHARACTER(20), @theInput) + @theOtherInput
                    """
        try:
            crsr.execute("DROP PROCEDURE sp_DeleteMeOnlyForTesting")
            self.conn.commit()
        except:  # Make sure it is empty
            pass
        crsr.execute(spdef)

        # calling the sproc with a string for the first parameter where a DateTime is expected
        result = tryconnection.try_operation_with_expected_exception(
            (api.DataError, api.DatabaseError),
            crsr.callproc,
            ["sp_DeleteMeOnlyForTesting"],
            {"parameters": ["this is wrong", "Anne", "not Alice"]},
        )
        if result[0]:  # the expected exception was raised
            assert "@theInput" in str(result[1]) or "DatabaseError" in str(
                result
            ), "Identifies the wrong erroneous parameter"
        else:
            assert result[0], result[1]  # incorrect or no exception
        self.conn.rollback()


class TestADOwithAccessDB(CommonDBTests):
    def setUp(self):
        self.conn = config.dbAccessconnect(
            *config.connStrAccess[0], **config.connStrAccess[1]
        )
        self.conn.timeout = 30  # turn timeout back up
        self.engine = "ACCESS"
        self.db = config.dbAccessconnect

    def tearDown(self):
        try:
            self.conn.rollback()
        except:
            pass
        try:
            self.conn.close()
        except:
            pass
        self.conn = None

    def getConnection(self):
        return self.conn

    def getAnotherConnection(self, addkeys=None):
        raise NotImplementedError("Jet cannot use a second connection to the database")

    def testOkConnect(self):
        c = self.db(*config.connStrAccess[0], **config.connStrAccess[1])
        assert c is not None
        c.close()


class TestADOwithMySql(CommonDBTests):
    def setUp(self):
        self.conn = config.dbMySqlconnect(
            *config.connStrMySql[0], **config.connStrMySql[1]
        )
        self.conn.timeout = 30  # turn timeout back up
        self.engine = "MySQL"
        self.db = config.dbMySqlconnect

    def tearDown(self):
        try:
            self.conn.rollback()
        except:
            pass
        try:
            self.conn.close()
        except:
            pass
        self.conn = None

    def getConnection(self):
        return self.conn

    def getAnotherConnection(self, addkeys=None):
        keys = dict(config.connStrMySql[1])
        if addkeys:
            keys.update(addkeys)
        return config.dbMySqlconnect(*config.connStrMySql[0], **keys)

    def testOkConnect(self):
        c = self.db(*config.connStrMySql[0], **config.connStrMySql[1])
        assert c is not None

    # def testStoredProcedure(self):
    #     crsr = self.conn.cursor()
    #     try:
    #         crsr.execute("DROP PROCEDURE DeleteMeOnlyForTesting")
    #         self.conn.commit()
    #     except:  # Make sure it is empty
    #         pass
    #     spdef = """
    #             DELIMITER $$
    #             CREATE PROCEDURE DeleteMeOnlyForTesting (onein CHAR(10), twoin CHAR(10), OUT theout CHAR(20))
    #             DETERMINISTIC
    #              BEGIN
    #                 SET theout = onein //|| twoin;
    #                 /* (SELECT 'a small string' as result; */
    #                 END $$
    #             """
    #     crsr.execute(spdef)
    #     retvalues = crsr.callproc(
    #         "DeleteMeOnlyForTesting", ("Dodsworth", "Anne", "              ")
    #     )
    #     # print(f"return value (mysql)={crsr.returnValue!r}")
    #     assert retvalues[0] == "Dodsworth", f'{retvalues[0]!r} is not "Dodsworth"'
    #     assert retvalues[1] == "Anne", f'{retvalues[1]!r} is not "Anne"'
    #     assert (
    #         retvalues[2] == "DodsworthAnne"
    #     ), f'{retvalues[2]!r} is not "DodsworthAnne"'
    #     try:
    #         crsr.execute("DROP PROCEDURE, DeleteMeOnlyForTesting")
    #         self.conn.commit()
    #     except:  # Make sure it is empty
    #         pass


class TestADOwithPostgres(CommonDBTests):
    def setUp(self):
        self.conn = config.dbPostgresConnect(
            *config.connStrPostgres[0], **config.connStrPostgres[1]
        )
        self.conn.timeout = 30  # turn timeout back up
        self.engine = "PostgreSQL"
        self.db = config.dbPostgresConnect

    def tearDown(self):
        try:
            self.conn.rollback()
        except:
            pass
        try:
            self.conn.close()
        except:
            pass
        self.conn = None

    def getConnection(self):
        return self.conn

    def getAnotherConnection(self, addkeys=None):
        keys = dict(config.connStrPostgres[1])
        if addkeys:
            keys.update(addkeys)
        return config.dbPostgresConnect(*config.connStrPostgres[0], **keys)

    def testOkConnect(self):
        c = self.db(*config.connStrPostgres[0], **config.connStrPostgres[1])
        assert c is not None

    # def testStoredProcedure(self):
    #     crsr = self.conn.cursor()
    #     spdef = """
    #         CREATE OR REPLACE FUNCTION DeleteMeOnlyForTesting (text, text)
    #         RETURNS text AS $funk$
    #         BEGIN
    #           RETURN $1 || $2;
    #         END;
    #         $funk$
    #         LANGUAGE SQL;
    #         """

    #     crsr.execute(spdef)
    #     retvalues = crsr.callproc(
    #         "DeleteMeOnlyForTesting", ("Dodsworth", "Anne", "              ")
    #     )
    #     # print(f"return value (pg)={crsr.returnValue!r}")
    #     assert retvalues[0] == "Dodsworth", f'{retvalues[0]!r} is not "Dodsworth"'
    #     assert retvalues[1] == "Anne", f'{retvalues[1]!r} is not "Anne"'
    #     assert (
    #         retvalues[2] == "DodsworthAnne"
    #     ), f'{retvalues[2]!r} is not "DodsworthAnne"'
    #     self.conn.rollback()
    #     try:
    #         crsr.execute("DROP PROCEDURE, DeleteMeOnlyForTesting")
    #         self.conn.commit()
    #     except:  # Make sure it is empty
    #         pass


class TimeConverterInterfaceTest(unittest.TestCase):
    def testIDate(self):
        assert self.tc.Date(1990, 2, 2)

    def testITime(self):
        assert self.tc.Time(13, 2, 2)

    def testITimestamp(self):
        assert self.tc.Timestamp(1990, 2, 2, 13, 2, 1)

    def testIDateObjectFromCOMDate(self):
        assert self.tc.DateObjectFromCOMDate(37435.7604282)

    def testICOMDate(self):
        assert hasattr(self.tc, "COMDate")

    def testExactDate(self):
        d = self.tc.Date(1994, 11, 15)
        comDate = self.tc.COMDate(d)
        correct = 34653.0
        assert comDate == correct, comDate

    def testExactTimestamp(self):
        d = self.tc.Timestamp(1994, 11, 15, 12, 0, 0)
        comDate = self.tc.COMDate(d)
        correct = 34653.5
        self.assertEqual(comDate, correct)

        d = self.tc.Timestamp(2003, 5, 6, 14, 15, 17)
        comDate = self.tc.COMDate(d)
        correct = 37747.593946759262
        self.assertEqual(comDate, correct)

    def testIsoFormat(self):
        d = self.tc.Timestamp(1994, 11, 15, 12, 3, 10)
        iso = self.tc.DateObjectToIsoFormatString(d)
        self.assertEqual(str(iso[:19]), "1994-11-15 12:03:10")

        dt = self.tc.Date(2003, 5, 2)
        iso = self.tc.DateObjectToIsoFormatString(dt)
        self.assertEqual(str(iso[:10]), "2003-05-02")


class TestPythonTimeConverter(TimeConverterInterfaceTest):
    def setUp(self):
        self.tc = api.pythonTimeConverter()

    def testCOMDate(self):
        mk = time.mktime((2002, 6, 28, 18, 15, 1, 4, 31 + 28 + 31 + 30 + 31 + 28, -1))
        t = time.localtime(mk)
        # Fri, 28 Jun 2002 18:15:01 +0000
        cmd = self.tc.COMDate(t)
        assert abs(cmd - 37435.7604282) < 1.0 / 24, "%f more than an hour wrong" % cmd

    def testDateObjectFromCOMDate(self):
        cmd = self.tc.DateObjectFromCOMDate(37435.7604282)
        t1 = time.gmtime(
            time.mktime((2002, 6, 28, 0, 14, 1, 4, 31 + 28 + 31 + 30 + 31 + 28, -1))
        )
        # there are errors in the implementation of gmtime which we ignore
        t2 = time.gmtime(
            time.mktime((2002, 6, 29, 12, 14, 2, 4, 31 + 28 + 31 + 30 + 31 + 28, -1))
        )
        assert t1 < cmd < t2, f'"{cmd}" should be about 2002-6-28 12:15:01'

    def testDate(self):
        t1 = time.mktime((2002, 6, 28, 18, 15, 1, 4, 31 + 28 + 31 + 30 + 31 + 30, 0))
        t2 = time.mktime((2002, 6, 30, 18, 15, 1, 4, 31 + 28 + 31 + 30 + 31 + 28, 0))
        obj = self.tc.Date(2002, 6, 29)
        assert t1 < time.mktime(obj) < t2, obj

    def testTime(self):
        self.assertEqual(
            self.tc.Time(18, 15, 2), time.gmtime(18 * 60 * 60 + 15 * 60 + 2)
        )

    def testTimestamp(self):
        t1 = time.localtime(
            time.mktime((2002, 6, 28, 18, 14, 1, 4, 31 + 28 + 31 + 30 + 31 + 28, -1))
        )
        t2 = time.localtime(
            time.mktime((2002, 6, 28, 18, 16, 1, 4, 31 + 28 + 31 + 30 + 31 + 28, -1))
        )
        obj = self.tc.Timestamp(2002, 6, 28, 18, 15, 2)
        assert t1 < obj < t2, obj


class TestPythonDateTimeConverter(TimeConverterInterfaceTest):
    def setUp(self):
        self.tc = api.pythonDateTimeConverter()

    def testCOMDate(self):
        t = datetime.datetime(2002, 6, 28, 18, 15, 1)
        # Fri, 28 Jun 2002 18:15:01 +0000
        cmd = self.tc.COMDate(t)
        assert abs(cmd - 37435.7604282) < 1.0 / 24, "more than an hour wrong"

    def testDateObjectFromCOMDate(self):
        cmd = self.tc.DateObjectFromCOMDate(37435.7604282)
        t1 = datetime.datetime(2002, 6, 28, 18, 14, 1)
        t2 = datetime.datetime(2002, 6, 28, 18, 16, 1)
        assert t1 < cmd < t2, cmd

        tx = datetime.datetime(
            2002, 6, 28, 18, 14, 1, 900000
        )  # testing that microseconds don't become milliseconds
        c1 = self.tc.DateObjectFromCOMDate(self.tc.COMDate(tx))
        assert t1 < c1 < t2, c1

    def testDate(self):
        t1 = datetime.date(2002, 6, 28)
        t2 = datetime.date(2002, 6, 30)
        obj = self.tc.Date(2002, 6, 29)
        assert t1 < obj < t2, obj

    def testTime(self):
        self.assertEqual(self.tc.Time(18, 15, 2).isoformat()[:8], "18:15:02")

    def testTimestamp(self):
        t1 = datetime.datetime(2002, 6, 28, 18, 14, 1)
        t2 = datetime.datetime(2002, 6, 28, 18, 16, 1)
        obj = self.tc.Timestamp(2002, 6, 28, 18, 15, 2)
        assert t1 < obj < t2, obj


suites = [
    unittest.defaultTestLoader.loadTestsFromModule(TestPythonDateTimeConverter, "test")
]
if config.doTimeTest:
    suites.append(
        unittest.defaultTestLoader.loadTestsFromModule(TestPythonTimeConverter, "test")
    )
if config.doAccessTest:
    suites.append(
        unittest.defaultTestLoader.loadTestsFromModule(TestADOwithAccessDB, "test")
    )
if config.doSqlServerTest:
    suites.append(
        unittest.defaultTestLoader.loadTestsFromModule(TestADOwithSQLServer, "test")
    )
if config.doMySqlTest:
    suites.append(
        unittest.defaultTestLoader.loadTestsFromModule(TestADOwithMySql, "test")
    )
if config.doPostgresTest:
    suites.append(
        unittest.defaultTestLoader.loadTestsFromModule(TestADOwithPostgres, "test")
    )


class cleanup_manager:
    def __enter__(self):
        pass

    def __exit__(self, exc_type, exc_val, exc_tb):
        config.cleanup(config.testfolder, config.mdb_name)


suite = unittest.TestSuite(suites)
if __name__ == "__main__":
    mysuite = copy.deepcopy(suite)
    with cleanup_manager():
        defaultDateConverter = adodbapi.dateconverter
        print(__doc__)
        print("Default Date Converter is %s" % (defaultDateConverter,))
        dateconverter = defaultDateConverter
        unittest.TextTestRunner().run(mysuite)

        if config.doTimeTest:
            mysuite = copy.deepcopy(
                suite
            )  # work around a side effect of unittest.TextTestRunner
            adodbapi.adodbapi.dateconverter = api.pythonTimeConverter()
            print("Changed dateconverter to ")
            print(adodbapi.adodbapi.dateconverter)
            unittest.TextTestRunner().run(mysuite)
