Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 96 additions & 93 deletions com/win32com/test/testall.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,30 @@
from __future__ import annotations

import getopt
import os
import re
import shutil
import sys
import traceback
import unittest

try:
this_file = __file__
except NameError:
this_file = sys.argv[0]

win32com_src_dir = os.path.abspath(os.path.join(this_file, "../.."))
from collections.abc import Callable
from textwrap import dedent
from types import ModuleType
from typing import NoReturn

import win32com

this_folder = os.path.dirname(__file__)
win32com_src_dir = os.path.abspath(os.path.join(this_folder, ".."))
# We'd prefer the win32com namespace to be the parent of __file__ - ie, our source-tree,
# rather than the version installed - otherwise every .py change needs a full install to
# test!
# We can't patch win32comext as most of them have a .pyd in their root :(
# This clearly ins't ideal or perfect :)
# This clearly isn't ideal or perfect :)
win32com.__path__[0] = win32com_src_dir

import pythoncom
import win32com.client
import win32com.client.gencache
from pywin32_testutil import TestLoader, TestRunner
from win32com.test.util import (
CapturingFunctionTestCase,
Expand All @@ -35,32 +37,17 @@
verbosity = 1 # default unittest verbosity.


def GenerateAndRunOldStyle():
from . import GenTestScripts

GenTestScripts.GenerateAll()
try:
pass #
finally:
GenTestScripts.CleanAll()


def CleanGenerated():
import shutil

import win32com

def CleanGenerated() -> None:
if os.path.isdir(win32com.__gen_path__):
if verbosity > 1:
print("Deleting files from %s" % (win32com.__gen_path__))
print(f"Deleting files from {win32com.__gen_path__}")
shutil.rmtree(win32com.__gen_path__)
import win32com.client.gencache

win32com.client.gencache.__init__() # Reset


def RemoveRefCountOutput(data):
while 1:
def RemoveRefCountOutput(data: str) -> str:
while True:
last_line_pos = data.rfind("\n")
if not re.match(r"\[\d+ refs\]", data[last_line_pos + 1 :]):
break
Expand All @@ -72,47 +59,45 @@ def RemoveRefCountOutput(data):
return data


def ExecuteSilentlyIfOK(cmd, testcase):
def ExecuteSilentlyIfOK(cmd: str, testcase: TestCase) -> str:
f = os.popen(cmd)
data = f.read().strip()
rc = f.close()
if rc:
print(data)
testcase.fail("Executing '%s' failed (%d)" % (cmd, rc))
testcase.fail(f"Executing '{cmd}' failed ({rc})")
# for "_d" builds, strip the '[xxx refs]' line
return RemoveRefCountOutput(data)


class PyCOMTest(TestCase):
no_leak_tests = True # done by the test itself

def testit(self):
def testit(self) -> None:
# Check that the item is registered, so we get the correct
# 'skipped' behaviour (and recorded as such) rather than either
# error or silence due to non-registration.
RegisterPythonServer(
os.path.join(
os.path.dirname(__file__), "..", "servers", "test_pycomtest.py"
),
os.path.join(win32com_src_dir, "servers", "test_pycomtest.py"),
"Python.Test.PyCOMTest",
)

# Execute testPyComTest in its own process so it can play
# with the Python thread state
fname = os.path.join(os.path.dirname(this_file), "testPyComTest.py")
fname = os.path.join(this_folder, "testPyComTest.py")
cmd = f'{sys.executable} "{fname}" -q 2>&1'
data = ExecuteSilentlyIfOK(cmd, self)


class PippoTest(TestCase):
def testit(self):
def testit(self) -> None:
# Check we are registered before spawning the process.
from win32com.test import pippo_server

RegisterPythonServer(pippo_server.__file__, "Python.Test.Pippo")

python = sys.executable
fname = os.path.join(os.path.dirname(this_file), "testPippo.py")
fname = os.path.join(this_folder, "testPippo.py")
cmd = f'{python} "{fname}" 2>&1'
ExecuteSilentlyIfOK(cmd, self)

Expand All @@ -121,35 +106,44 @@ def testit(self):
# function in that module if the module isn't unitest based...
unittest_modules = [
# Level 1 tests - fast and few dependencies - good for CI!
"""testIterators testvbscript_regexp testStorage
testStreams testWMI policySemantics testShell testROT
testxslt testCollections
errorSemantics.test testArrays
testClipboard
testConversionErrors
""".split(),
[
"testIterators",
"testvbscript_regexp",
"testStorage",
"testStreams",
"testWMI",
"policySemantics",
"testShell",
"testROT",
"testxslt",
"testCollections",
"errorSemantics.test",
"testArrays",
"testClipboard",
"testConversionErrors",
],
# Level 2 tests - wants our demo COM objects registered.
# (these are strange; on github CI they get further than expected when
# our objects are not installed, so fail to quietly fail with "can't
# register" like they do locally. So really just a nod to CI)
"""
testAXScript testDictionary testServers testvb testMarshal
""".split(),
["testAXScript", "testDictionary", "testServers", "testvb", "testMarshal"],
# Level 3 tests - Requires Office or other non-free stuff.
"""testMSOffice.TestAll testMSOfficeEvents.test testAccess.test
testExplorer.TestAll testExchange.test
""".split(),
[
"testMSOffice.TestAll",
"testMSOfficeEvents.test",
"testAccess.test",
"testExplorer.TestAll",
"testExchange.test",
],
# Level 4 tests - we try and run `makepy` over every typelib installed!
"""testmakepy.TestAll
""".split(),
["testmakepy.TestAll"],
]

# A list of other unittest modules we use - these are fully qualified module
# names and the module is assumed to be unittest based.
unittest_other_modules = [
# Level 1 tests.
"""win32com.directsound.test.ds_test
""".split(),
["win32com.directsound.test.ds_test"],
# Level 2 tests.
[],
# Level 3 tests.
Expand Down Expand Up @@ -192,7 +186,9 @@ def testit(self):
]


def get_test_mod_and_func(test_name, import_failures):
def get_test_mod_and_func(
test_name: str, import_failures: list[tuple[str, BaseException]]
) -> tuple[None, None] | tuple[ModuleType, Callable[[], object] | None]:
if test_name.find(".") > 0:
mod_name, func_name = test_name.split(".")
else:
Expand All @@ -202,19 +198,21 @@ def get_test_mod_and_func(test_name, import_failures):
try:
__import__(fq_mod_name)
mod = sys.modules[fq_mod_name]
except:
import_failures.append((mod_name, sys.exc_info()[:2]))
except Exception as error:
import_failures.append((mod_name, error))
return None, None
func = None if func_name is None else getattr(mod, func_name)
return mod, func


# Return a test suite all loaded with the tests we want to run
def make_test_suite(test_level=1):
def make_test_suite(
test_level: int,
) -> tuple[unittest.TestSuite, list[tuple[str, BaseException]]]:
suite = unittest.TestSuite()
import_failures = []
import_failures: list[tuple[str, BaseException]] = []
loader = TestLoader()
for i in range(testLevel):
for i in range(test_level):
for mod_name in unittest_modules[i]:
mod, func = get_test_mod_and_func(mod_name, import_failures)
if mod is None:
Expand All @@ -234,12 +232,12 @@ def make_test_suite(test_level=1):
for test_class in custom_test_cases[i]:
suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(test_class))
# other "normal" unittest modules.
for i in range(testLevel):
for i in range(test_level):
for mod_name in unittest_other_modules[i]:
try:
__import__(mod_name)
except:
import_failures.append((mod_name, sys.exc_info()[:2]))
except Exception as error:
import_failures.append((mod_name, error))
continue

mod = sys.modules[mod_name]
Expand All @@ -253,13 +251,15 @@ def make_test_suite(test_level=1):
return suite, import_failures


def usage(why):
print(why)
print()
print("win32com test suite")
print("usage: testall [-v] test_level")
print(" where test_level is an integer 1-3. Level 1 tests are quick,")
print(" level 2 tests invoke Word, IE etc, level 3 take ages!")
def usage(why: str) -> NoReturn:
print(f"""\
{why}

win32com test suite
usage: testall [-v] test_level
where test_level is an integer 1-4. Level 1 tests are quick,
level 2 tests invoke Word, IE etc, level 3 take ages!
level 4 we try and run `makepy` over every typelib installed""")
sys.exit(1)


Expand All @@ -271,44 +271,47 @@ def usage(why):
for opt, val in opts:
if opt == "-v":
verbosity += 1
testLevel = 2 # default to quick test with local objects
test_names = []
for arg in args:
try:
testLevel = int(arg)
if testLevel < 0 or testLevel > 4:
raise ValueError("Only levels 1-4 are supported")
except ValueError:
test_names.append(arg)
if test_names:

if len(args) > 1:
usage(
"Only a single test level argument is supported. "
+ "Test names are not supported yet"
)
try:
test_level = int(args[0])
except IndexError:
test_level = 2 # default to quick test with local objects
except ValueError:
usage("Test names are not supported yet")
if test_level < 1 or test_level > 4:
usage("Only levels 1-4 are supported")
CleanGenerated()

suite, import_failures = make_test_suite(testLevel)
suite, import_failures = make_test_suite(test_level)
if verbosity:
if hasattr(sys, "gettotalrefcount"):
print("This is a debug build - memory leak tests will also be run.")
print("These tests may take *many* minutes to run - be patient!")
print("(running from python.exe will avoid these leak tests)")
print(
dedent("""\
This is a debug build - memory leak tests will also be run.
These tests may take *many* minutes to run - be patient!
(running from python.exe will avoid these leak tests""")
)
print(
"Executing level %d tests - %d test cases will be run"
% (testLevel, suite.countTestCases())
f"Executing level {test_level} tests "
+ f"- {suite.countTestCases()} test cases will be run"
)
if verbosity == 1 and suite.countTestCases() < 70:
# A little row of markers so the dots show how close to finished
print("|" * suite.countTestCases())
testRunner = TestRunner(verbosity=verbosity)
testResult = testRunner.run(suite)
testResult = TestRunner(verbosity=verbosity).run(suite)
if import_failures:
testResult.stream.writeln(
"*** The following test modules could not be imported ***"
)
for mod_name, (exc_type, exc_val) in import_failures:
desc = "\n".join(traceback.format_exception_only(exc_type, exc_val))
for mod_name, error in import_failures:
desc = "\n".join(traceback.format_exception_only(type(error), error))
testResult.stream.write(f"{mod_name}: {desc}")
testResult.stream.writeln(
"*** %d test(s) could not be run ***" % len(import_failures)
)
testResult.stream.writeln(f"*** {import_failures} test(s) could not be run ***")

# re-print unit-test error here so it is noticed
if not testResult.wasSuccessful():
Expand Down