333 lines
10 KiB
Python
333 lines
10 KiB
Python
from __future__ import annotations
|
|
|
|
import getopt
|
|
import os
|
|
import platform
|
|
import re
|
|
import shutil
|
|
import sys
|
|
import traceback
|
|
import unittest
|
|
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 isn't ideal or perfect :)
|
|
win32com.__path__[0] = win32com_src_dir
|
|
|
|
import pythoncom
|
|
import win32com.client.gencache
|
|
from pywin32_testutil import TestLoader, TestRunner
|
|
from win32com.test.util import (
|
|
CapturingFunctionTestCase,
|
|
CheckClean,
|
|
RegisterPythonServer,
|
|
ShellTestCase,
|
|
TestCase,
|
|
)
|
|
|
|
verbosity = 1 # default unittest verbosity.
|
|
|
|
|
|
def CleanGenerated() -> None:
|
|
if os.path.isdir(win32com.__gen_path__):
|
|
if verbosity > 1:
|
|
print(f"Deleting files from", win32com.__gen_path__)
|
|
shutil.rmtree(win32com.__gen_path__)
|
|
|
|
win32com.client.gencache.__init__() # Reset
|
|
|
|
|
|
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
|
|
if last_line_pos < 0:
|
|
# All the output
|
|
return ""
|
|
data = data[:last_line_pos]
|
|
|
|
return data
|
|
|
|
|
|
def ExecuteSilentlyIfOK(cmd: str, testcase: TestCase) -> str:
|
|
f = os.popen(cmd)
|
|
data = f.read().strip()
|
|
rc = f.close()
|
|
if rc:
|
|
print(data)
|
|
testcase.fail(f"Executing '{cmd}' failed ({rc})")
|
|
# for "_d" builds, strip the '[xxx refs]' line
|
|
return RemoveRefCountOutput(data)
|
|
|
|
|
|
@unittest.skipIf(
|
|
platform.machine() == "ARM64",
|
|
"PyCOMTest cannot currently be run on ARM64 "
|
|
+ "due to lacking win32com.universal implementation "
|
|
+ "in com/win32com/src/univgw.cpp",
|
|
)
|
|
class PyCOMTest(TestCase):
|
|
no_leak_tests = True # done by the test itself
|
|
|
|
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(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(this_folder, "testPyComTest.py")
|
|
cmd = f'{sys.executable} "{fname}" -q 2>&1'
|
|
data = ExecuteSilentlyIfOK(cmd, self)
|
|
|
|
|
|
class PippoTest(TestCase):
|
|
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(this_folder, "testPippo.py")
|
|
cmd = f'{python} "{fname}" 2>&1'
|
|
ExecuteSilentlyIfOK(cmd, self)
|
|
|
|
|
|
# This is a list of "win32com.test.???" module names, optionally with a
|
|
# 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",
|
|
],
|
|
# 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"],
|
|
# Level 3 tests - Requires Office or other non-free stuff.
|
|
[
|
|
"testMSOffice.TestAll",
|
|
"testMSOfficeEvents.test",
|
|
"testAccess.test",
|
|
"testExplorer.TestAll",
|
|
"testExchange.test",
|
|
],
|
|
# Level 4 tests - we try and run `makepy` over every typelib installed!
|
|
["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"],
|
|
# Level 2 tests.
|
|
[],
|
|
# Level 3 tests.
|
|
[],
|
|
# Level 4 tests.
|
|
[],
|
|
]
|
|
|
|
|
|
output_checked_programs = [
|
|
# Level 1 tests.
|
|
[],
|
|
# Level 2 tests.
|
|
[
|
|
("cscript.exe /nologo //E:vbscript testInterp.vbs", "VBScript test worked OK"),
|
|
(
|
|
"cscript.exe /nologo //E:vbscript testDictionary.vbs",
|
|
"VBScript has successfully tested Python.Dictionary",
|
|
),
|
|
],
|
|
# Level 3 tests
|
|
[],
|
|
# Level 4 tests.
|
|
[],
|
|
]
|
|
|
|
custom_test_cases = [
|
|
# Level 1 tests.
|
|
[
|
|
PyCOMTest,
|
|
],
|
|
# Level 2 tests.
|
|
[
|
|
PippoTest,
|
|
],
|
|
# Level 3 tests
|
|
[],
|
|
# Level 4 tests.
|
|
[],
|
|
]
|
|
|
|
|
|
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:
|
|
mod_name = test_name
|
|
func_name = None
|
|
fq_mod_name = "win32com.test." + mod_name
|
|
try:
|
|
__import__(fq_mod_name)
|
|
mod = sys.modules[fq_mod_name]
|
|
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: int,
|
|
) -> tuple[unittest.TestSuite, list[tuple[str, BaseException]]]:
|
|
suite = unittest.TestSuite()
|
|
import_failures: list[tuple[str, BaseException]] = []
|
|
loader = TestLoader()
|
|
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:
|
|
raise ModuleNotFoundError(f"no such module '{mod_name}'")
|
|
if func is not None:
|
|
test = CapturingFunctionTestCase(func, description=mod_name)
|
|
else:
|
|
if hasattr(mod, "suite"):
|
|
test = mod.suite()
|
|
else:
|
|
test = loader.loadTestsFromModule(mod)
|
|
assert test.countTestCases() > 0, f"No tests loaded from {mod!r}"
|
|
suite.addTest(test)
|
|
for cmd, output in output_checked_programs[i]:
|
|
suite.addTest(ShellTestCase(cmd, output))
|
|
|
|
for test_class in custom_test_cases[i]:
|
|
suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(test_class))
|
|
# other "normal" unittest modules.
|
|
for i in range(test_level):
|
|
for mod_name in unittest_other_modules[i]:
|
|
try:
|
|
__import__(mod_name)
|
|
except Exception as error:
|
|
import_failures.append((mod_name, error))
|
|
continue
|
|
|
|
mod = sys.modules[mod_name]
|
|
if hasattr(mod, "suite"):
|
|
test = mod.suite()
|
|
else:
|
|
test = loader.loadTestsFromModule(mod)
|
|
assert test.countTestCases() > 0, f"No tests loaded from {mod!r}"
|
|
suite.addTest(test)
|
|
|
|
return suite, import_failures
|
|
|
|
|
|
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)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
opts, args = getopt.getopt(sys.argv[1:], "v")
|
|
except getopt.error as why:
|
|
usage(why)
|
|
for opt, val in opts:
|
|
if opt == "-v":
|
|
verbosity += 1
|
|
|
|
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(test_level)
|
|
if verbosity:
|
|
if hasattr(sys, "gettotalrefcount"):
|
|
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(
|
|
f"Executing level {test_level} tests - {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())
|
|
testResult = TestRunner(verbosity=verbosity).run(suite)
|
|
if import_failures:
|
|
testResult.stream.writeln(
|
|
"*** The following test modules could not be imported ***"
|
|
)
|
|
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(
|
|
f"*** {len(import_failures)} test(s) could not be run ***"
|
|
)
|
|
|
|
# re-print unit-test error here so it is noticed
|
|
if not testResult.wasSuccessful():
|
|
print("*" * 20, "- unittest tests FAILED")
|
|
|
|
CheckClean()
|
|
pythoncom.CoUninitialize()
|
|
CleanGenerated()
|
|
if not testResult.wasSuccessful():
|
|
sys.exit(1)
|