Improved self-cleaning tests.
This commit is contained in:
1 parent
77db0b83ab
commit
e3de71e594
14 files changed
+487
-39
No files matched your search
@@ -36,6 +36,7 @@ Testing/
|
||||
*.src.tar.*
|
||||
|
||||
# Editor backups and desktop metadata
|
||||
__pycache__/
|
||||
*~
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
+14
-10
@@ -70,6 +70,16 @@ target_compile_options(wine-access PRIVATE
|
||||
|
||||
include(CTest)
|
||||
if(BUILD_TESTING)
|
||||
function(add_wine_test name timeout)
|
||||
add_test(NAME "${name}" COMMAND python3
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/run_wine_test.py"
|
||||
--build-dir "${CMAKE_CURRENT_BINARY_DIR}" --name "${name}"
|
||||
--timeout "${timeout}" -- ${ARGN})
|
||||
math(EXPR ctestTimeout "${timeout} + 15")
|
||||
# Fixtures share the private display. Serialize them even with ctest -j,
|
||||
# and allow the runner to finish teardown before CTest kills it.
|
||||
set_tests_properties("${name}" PROPERTIES TIMEOUT "${ctestTimeout}" RUN_SERIAL TRUE)
|
||||
endfunction()
|
||||
set(foreground_fixture "${CMAKE_CURRENT_BINARY_DIR}/foreground-fixture.exe")
|
||||
add_custom_command(OUTPUT "${foreground_fixture}"
|
||||
COMMAND "${MINGW_C_COMPILER}" -std=c11 -Wall -Wextra -mwindows
|
||||
@@ -81,16 +91,12 @@ if(BUILD_TESTING)
|
||||
target_compile_features(foreground-focus-test PRIVATE cxx_std_20)
|
||||
target_compile_options(foreground-focus-test PRIVATE -Wall -Wextra)
|
||||
target_link_libraries(foreground-focus-test PRIVATE user32 PkgConfig::XCB)
|
||||
add_test(NAME foreground-focus COMMAND python3
|
||||
add_wine_test(foreground-focus 90 python3
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/foreground_focus_test.py" "${CMAKE_CURRENT_BINARY_DIR}")
|
||||
set_tests_properties(foreground-focus PROPERTIES TIMEOUT 90 ENVIRONMENT
|
||||
"WINEPREFIX=${CMAKE_CURRENT_BINARY_DIR}/focus-prefix;CTHULHU_WINE_ACCESS_MANAGED=1;WINEDEBUG=-all;WINEDLLOVERRIDES=mscoree,mshtml=")
|
||||
add_executable(msaa-states-test tests/msaa_states_test.cpp)
|
||||
target_compile_features(msaa-states-test PRIVATE cxx_std_20)
|
||||
target_compile_options(msaa-states-test PRIVATE -Wall -Wextra)
|
||||
add_test(NAME msaa-states COMMAND "${CMAKE_CURRENT_BINARY_DIR}/msaa-states-test.exe")
|
||||
set_tests_properties(msaa-states PROPERTIES ENVIRONMENT
|
||||
"WINEPREFIX=${CMAKE_CURRENT_BINARY_DIR}/test-prefix;CTHULHU_WINE_ACCESS_MANAGED=1;WINEDEBUG=-all;WINEDLLOVERRIDES=mscoree,mshtml=")
|
||||
add_wine_test(msaa-states 35 "${CMAKE_CURRENT_BINARY_DIR}/msaa-states-test.exe")
|
||||
add_executable(review-capture-test tests/review_capture_test.cpp)
|
||||
target_compile_features(review-capture-test PRIVATE cxx_std_20)
|
||||
target_compile_options(review-capture-test PRIVATE -Wall -Wextra)
|
||||
@@ -111,10 +117,8 @@ if(BUILD_TESTING)
|
||||
DEPENDS tests/review_fixture.c VERBATIM)
|
||||
add_custom_target(review-fixture${arch} ALL DEPENDS "${review_fixture}")
|
||||
add_dependencies(review-capture-test review-fixture${arch})
|
||||
add_test(NAME review-capture${arch}
|
||||
COMMAND "${CMAKE_CURRENT_BINARY_DIR}/review-capture-test.exe" --fixture "${review_fixture}")
|
||||
set_tests_properties(review-capture${arch} PROPERTIES TIMEOUT 35 ENVIRONMENT
|
||||
"WINEPREFIX=${CMAKE_CURRENT_BINARY_DIR}/review-prefix${arch};CTHULHU_WINE_ACCESS_MANAGED=1;WINEDEBUG=-all;WINEDLLOVERRIDES=mscoree,mshtml=")
|
||||
add_wine_test(review-capture${arch} 35
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/review-capture-test.exe" --fixture "${review_fixture}")
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
|
||||
@@ -1,5 +1,46 @@
|
||||
# Wine control integration
|
||||
|
||||
## Temporary files and failure inspection
|
||||
|
||||
Wine CTests and the integration scripts below use fresh prefixes under the
|
||||
temporary directory (`/tmp` by default). Successful runs stop their Wine
|
||||
servers and remove their temporary files. **Failures, timeouts and ordinary
|
||||
interruptions retain the original files by default**, with the directory path
|
||||
printed before the test starts and again at exit. This preserves a one-off
|
||||
failure for inspection without requiring a second reproduction. Retention
|
||||
keeps files and registry state; test processes are still stopped.
|
||||
|
||||
Console output and fixture `.log` files are saved under
|
||||
`<build-dir>/Testing/WineAccessLogs/`, outside the disposable prefix. These logs
|
||||
remain even when temporary files are removed. CTest also keeps its usual
|
||||
`Testing/Temporary/LastTest.log` report. Saved logs need occasional maintenance;
|
||||
review them for private text before sharing.
|
||||
|
||||
To retain successful runs too, or explicitly clean a failed run:
|
||||
|
||||
```sh
|
||||
xvfb-run -a python3 tests/menu_integration.py --build-dir build --keep
|
||||
xvfb-run -a python3 tests/menu_integration.py --build-dir build --clean-on-failure
|
||||
WINE_ACCESS_TEST_KEEP=1 xvfb-run -a ctest --test-dir build -V
|
||||
WINE_ACCESS_TEST_KEEP_ON_FAILURE=0 xvfb-run -a ctest --test-dir build --output-on-failure
|
||||
```
|
||||
|
||||
Each run requires at least 1024 MiB free on the temporary filesystem before
|
||||
starting. If retained failures accumulate, the next run stops with an explicit
|
||||
space error; it never deletes earlier evidence automatically. Remove retained
|
||||
directories manually after inspecting them. `TMPDIR` can select another
|
||||
filesystem, and `WINE_ACCESS_TEST_MIN_FREE_MIB` can adjust the space guard for
|
||||
larger fixtures. CTests run serially because their fixtures share one display.
|
||||
A teardown or log-preservation failure always retains the temporary files.
|
||||
SIGKILL, machine shutdown and an outer test-runner kill cannot run Python
|
||||
cleanup; occasional `/tmp` cleanup remains useful.
|
||||
|
||||
Check the runner without Wine:
|
||||
|
||||
```sh
|
||||
python3 tests/wine_test_environment_test.py
|
||||
```
|
||||
|
||||
## UIA notifications
|
||||
|
||||
UIA notification routing uses the actual native application hooks and a private
|
||||
|
||||
@@ -4,23 +4,26 @@ import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
from gi.repository import Gio, GLib
|
||||
|
||||
from wine_test_environment import add_test_options, run_wine_test, wine_test_directory
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--build-dir", default="build")
|
||||
parser.add_argument("--reader", choices=("cthulhu", "orca"))
|
||||
parser.add_argument("--extension-endpoint", action="store_true")
|
||||
parser.add_argument("--session", action="store_true")
|
||||
add_test_options(parser)
|
||||
args = parser.parse_args()
|
||||
build = Path(args.build_dir).resolve()
|
||||
if not args.session:
|
||||
sys.exit(subprocess.call(["dbus-run-session", "--", sys.executable, __file__,
|
||||
sys.exit(run_wine_test(["dbus-run-session", "--", sys.executable, __file__,
|
||||
"--session", "--build-dir", str(build),
|
||||
*(["--extension-endpoint"] if args.extension_endpoint else []),
|
||||
*(["--reader", args.reader] if args.reader else [])]))
|
||||
*(["--reader", args.reader] if args.reader else [])], build, "controller_integration",
|
||||
keep=args.keep, keepOnFailure=args.keepOnFailure))
|
||||
|
||||
busName = "org.gnome.Orca1.Service" if args.reader == "orca" else "org.stormux.Cthulhu1.Service"
|
||||
interface = busName.rsplit(".", 1)[0] + ".WineAccess" if args.extension_endpoint else busName
|
||||
@@ -70,7 +73,7 @@ def wait_for_client(client, expected=0):
|
||||
assert client.poll() == expected, f"client failed or timed out: {client.poll()}"
|
||||
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="wine-access-controller-") as temporary:
|
||||
with wine_test_directory(prefix="wine-access-controller-") as temporary:
|
||||
tempPath = Path(temporary)
|
||||
env = os.environ.copy()
|
||||
env.update(WINEPREFIX=str(tempPath / "prefix"), WINEDEBUG="-all",
|
||||
|
||||
@@ -9,20 +9,23 @@ from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
from gi.repository import Gio, GLib
|
||||
|
||||
from wine_test_environment import add_test_options, run_wine_test, wine_test_directory
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--build-dir", type=Path, default=Path("build"))
|
||||
parser.add_argument("--session", action="store_true")
|
||||
add_test_options(parser)
|
||||
args = parser.parse_args()
|
||||
buildDir = args.build_dir.resolve()
|
||||
if not args.session:
|
||||
sys.exit(subprocess.call(["dbus-run-session", "--", sys.executable, __file__,
|
||||
"--session", "--build-dir", str(buildDir)]))
|
||||
sys.exit(run_wine_test(["dbus-run-session", "--", sys.executable, __file__,
|
||||
"--session", "--build-dir", str(buildDir)], buildDir, "diagnostics_test",
|
||||
keep=args.keep, keepOnFailure=args.keepOnFailure))
|
||||
|
||||
busName = "org.stormux.Cthulhu1.Service"
|
||||
objectPath = "/org/stormux/Cthulhu1/Service"
|
||||
@@ -39,7 +42,7 @@ def record(connection, sender, path, interface, method, parameters, invocation):
|
||||
invocation.return_value(GLib.Variant("(b)", (True,)))
|
||||
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="wine-access-diagnostics-") as temporary:
|
||||
with wine_test_directory(prefix="wine-access-diagnostics-") as temporary:
|
||||
testDir = Path(temporary)
|
||||
env = os.environ.copy()
|
||||
env.update(WINEPREFIX=str(testDir / "prefix"), WINEDEBUG="-all",
|
||||
|
||||
@@ -8,11 +8,12 @@ import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
from gi.repository import Gio, GLib
|
||||
|
||||
from wine_test_environment import add_test_options, run_wine_test, wine_test_directory
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--build-dir", default="build")
|
||||
parser.add_argument("--32-bit", dest="is32Bit", action="store_true")
|
||||
@@ -21,15 +22,17 @@ parser.add_argument("--session", action="store_true")
|
||||
parser.add_argument("--getter", action="store_true")
|
||||
parser.add_argument("--background", action="store_true")
|
||||
parser.add_argument("--popup", action="store_true")
|
||||
add_test_options(parser)
|
||||
args = parser.parse_args()
|
||||
build = Path(args.build_dir).resolve()
|
||||
if not args.session:
|
||||
sys.exit(subprocess.call(["dbus-run-session", "--", sys.executable, __file__,
|
||||
sys.exit(run_wine_test(["dbus-run-session", "--", sys.executable, __file__,
|
||||
"--session", "--build-dir", str(build), "--reader", args.reader,
|
||||
*(["--32-bit"] if args.is32Bit else []),
|
||||
*(["--getter"] if args.getter else []),
|
||||
*(["--background"] if args.background else []),
|
||||
*(["--popup"] if args.popup else [])]))
|
||||
*(["--popup"] if args.popup else [])], build, "event_stall_integration",
|
||||
keep=args.keep, keepOnFailure=args.keepOnFailure))
|
||||
|
||||
interface = "org.gnome.Orca1.Service" if args.reader == "orca" else "org.stormux.Cthulhu1.Service"
|
||||
objectPath = "/" + interface.replace(".", "/")
|
||||
@@ -67,7 +70,7 @@ def pump_until(predicate, seconds):
|
||||
raise AssertionError(f"condition timed out; events: {events}")
|
||||
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="wine-event-stall-") as temporary:
|
||||
with wine_test_directory(prefix="wine-event-stall-") as temporary:
|
||||
root = Path(temporary)
|
||||
env = os.environ.copy()
|
||||
env.update(WINEPREFIX=str(root / "prefix"), WINEDEBUG="-all",
|
||||
|
||||
@@ -4,9 +4,16 @@ from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from wine_test_environment import run_wine_test
|
||||
|
||||
buildDir = Path(sys.argv[1]).resolve()
|
||||
if "WINE_ACCESS_TEST_ROOT" not in os.environ:
|
||||
sys.exit(run_wine_test([sys.executable, __file__, str(buildDir)], buildDir,
|
||||
"foreground-focus", timeout=90,
|
||||
keep=os.environ.get("WINE_ACCESS_TEST_KEEP") == "1",
|
||||
keepOnFailure=os.environ.get("WINE_ACCESS_TEST_KEEP_ON_FAILURE", "1") != "0"))
|
||||
env = os.environ.copy()
|
||||
env.update(WINEPREFIX=str(buildDir / "focus-prefix"),
|
||||
env.update(WINEPREFIX=os.environ["WINEPREFIX"],
|
||||
CTHULHU_WINE_ACCESS_MANAGED="1", WINEDEBUG="-all",
|
||||
WINEDLLOVERRIDES="mscoree,mshtml=")
|
||||
try:
|
||||
|
||||
@@ -9,11 +9,12 @@ from pathlib import Path
|
||||
import subprocess
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
from gi.repository import Gio, GLib
|
||||
|
||||
from wine_test_environment import add_test_options, run_wine_test, wine_test_directory
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--build-dir", default="build")
|
||||
parser.add_argument("--session", action="store_true")
|
||||
@@ -25,6 +26,7 @@ parser.add_argument("--goldwave-exe", type=Path)
|
||||
parser.add_argument("--32-bit", dest="is32Bit", action="store_true")
|
||||
parser.add_argument("--late-helper", action="store_true")
|
||||
parser.add_argument("--restart-helper", action="store_true")
|
||||
add_test_options(parser)
|
||||
args = parser.parse_args()
|
||||
if args.is32Bit and args.fixture not in ("menu", "edit", "winforms", "list", "listbox", "delphi", "controls"):
|
||||
parser.error("32-bit checks currently require --fixture menu, edit, winforms, list, listbox, delphi or controls")
|
||||
@@ -36,7 +38,7 @@ if args.fixture == "goldwave" and (not args.goldwave_exe or not args.goldwave_ex
|
||||
parser.error("--fixture goldwave requires --goldwave-exe pointing to GoldWave 5.70")
|
||||
build = Path(args.build_dir).resolve()
|
||||
if not args.session:
|
||||
sys.exit(subprocess.call(["dbus-run-session", "--", sys.executable,
|
||||
sys.exit(run_wine_test(["dbus-run-session", "--", sys.executable,
|
||||
__file__, "--session", "--build-dir", str(build),
|
||||
"--fixture", args.fixture,
|
||||
*(["--extension-endpoint"] if args.extension_endpoint else []),
|
||||
@@ -45,7 +47,8 @@ if not args.session:
|
||||
*(["--goldwave-exe", str(args.goldwave_exe.resolve())] if args.goldwave_exe else []),
|
||||
*(["--32-bit"] if args.is32Bit else []),
|
||||
*(["--late-helper"] if args.late_helper else []),
|
||||
*(["--restart-helper"] if args.restart_helper else [])]))
|
||||
*(["--restart-helper"] if args.restart_helper else [])], build, "menu_integration",
|
||||
keep=args.keep, keepOnFailure=args.keepOnFailure))
|
||||
|
||||
busName = "org.gnome.Orca1.Service" if args.reader == "orca" else "org.stormux.Cthulhu1.Service"
|
||||
interface = busName.rsplit(".", 1)[0] + ".WineAccess" if args.extension_endpoint else busName
|
||||
@@ -75,7 +78,7 @@ connection.call_sync("org.freedesktop.DBus", "/org/freedesktop/DBus",
|
||||
GLib.Variant("(su)", (busName, 0)), None,
|
||||
Gio.DBusCallFlags.NONE, 1000, None)
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="wine-access-menus-") as temporary:
|
||||
with wine_test_directory(prefix="wine-access-menus-") as temporary:
|
||||
env = os.environ.copy()
|
||||
env.update(WINEPREFIX=str(Path(temporary) / "prefix"), WINEDEBUG=os.environ.get("WINE_ACCESS_TEST_DEBUG", "-all"),
|
||||
WINEDLLOVERRIDES="mscoree,mshtml=", CTHULHU_WINE_ACCESS_MANAGED="1")
|
||||
|
||||
@@ -4,11 +4,12 @@ import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
from gi.repository import Gio, GLib
|
||||
|
||||
from wine_test_environment import add_test_options, run_wine_test, wine_test_directory
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--build-dir", default="build")
|
||||
parser.add_argument("--reader", choices=("cthulhu", "orca"), default="cthulhu")
|
||||
@@ -16,17 +17,19 @@ parser.add_argument("--32-bit", dest="is32Bit", action="store_true")
|
||||
parser.add_argument("--session", action="store_true")
|
||||
parser.add_argument("--prism-dll", type=Path)
|
||||
parser.add_argument("--prism-include", type=Path)
|
||||
add_test_options(parser)
|
||||
args = parser.parse_args()
|
||||
if bool(args.prism_dll) != bool(args.prism_include) or (args.prism_dll and args.is32Bit):
|
||||
parser.error("Prism checks require both --prism-dll and --prism-include, and a 64-bit DLL")
|
||||
build = Path(args.build_dir).resolve()
|
||||
if not args.session:
|
||||
sys.exit(subprocess.call(["dbus-run-session", "--", sys.executable, __file__,
|
||||
sys.exit(run_wine_test(["dbus-run-session", "--", sys.executable, __file__,
|
||||
"--session", "--build-dir", str(build), "--reader", args.reader,
|
||||
*(["--prism-dll", str(args.prism_dll.resolve()),
|
||||
"--prism-include", str(args.prism_include.resolve())]
|
||||
if args.prism_dll else []),
|
||||
*(["--32-bit"] if args.is32Bit else [])]))
|
||||
*(["--32-bit"] if args.is32Bit else [])], build, "notification_integration",
|
||||
keep=args.keep, keepOnFailure=args.keepOnFailure))
|
||||
|
||||
interface = "org.gnome.Orca1.Service" if args.reader == "orca" else "org.stormux.Cthulhu1.Service"
|
||||
objectPath = "/" + interface.replace(".", "/")
|
||||
@@ -52,7 +55,7 @@ connection.call_sync("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freed
|
||||
"RequestName", GLib.Variant("(su)", (interface, 0)), None,
|
||||
Gio.DBusCallFlags.NONE, 1000, None)
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="wine-access-notifications-") as temporary:
|
||||
with wine_test_directory(prefix="wine-access-notifications-") as temporary:
|
||||
tempPath = Path(temporary)
|
||||
env = os.environ.copy()
|
||||
env.update(WINEPREFIX=str(tempPath / "prefix"), WINEDEBUG="-all",
|
||||
|
||||
@@ -7,25 +7,28 @@ import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
from gi.repository import Gio, GLib
|
||||
|
||||
from wine_test_environment import add_test_options, run_wine_test, wine_test_directory
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--build-dir", default="build")
|
||||
parser.add_argument("--32-bit", dest="is32Bit", action="store_true")
|
||||
parser.add_argument("--timeout", action="store_true")
|
||||
parser.add_argument("--focus-error", action="store_true")
|
||||
parser.add_argument("--session", action="store_true")
|
||||
add_test_options(parser)
|
||||
args = parser.parse_args()
|
||||
build = Path(args.build_dir).resolve()
|
||||
if not args.session:
|
||||
sys.exit(subprocess.call(["dbus-run-session", "--", sys.executable, __file__,
|
||||
sys.exit(run_wine_test(["dbus-run-session", "--", sys.executable, __file__,
|
||||
"--session", "--build-dir", str(build),
|
||||
*(["--32-bit"] if args.is32Bit else []),
|
||||
*(["--timeout"] if args.timeout else []),
|
||||
*(["--focus-error"] if args.focus_error else [])]))
|
||||
*(["--focus-error"] if args.focus_error else [])], build, "reader_stall_integration",
|
||||
keep=args.keep, keepOnFailure=args.keepOnFailure))
|
||||
|
||||
interface = "org.stormux.Cthulhu1.Service"
|
||||
object_path = "/org/stormux/Cthulhu1/Service"
|
||||
@@ -51,7 +54,7 @@ def pump_until(predicate, seconds):
|
||||
raise AssertionError(f"condition timed out; events: {events}")
|
||||
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="wine-reader-stall-") as temporary:
|
||||
with wine_test_directory(prefix="wine-reader-stall-") as temporary:
|
||||
root = Path(temporary)
|
||||
waiting, typed, resume = (root / name for name in ("waiting", "typed", "resume"))
|
||||
env = os.environ.copy()
|
||||
|
||||
@@ -4,24 +4,27 @@ import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from gi.repository import Gio, GLib
|
||||
|
||||
from wine_test_environment import add_test_options, run_wine_test, wine_test_directory
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--build-dir', default='build')
|
||||
parser.add_argument('--32-bit', dest='is32', action='store_true')
|
||||
parser.add_argument('--session', action='store_true')
|
||||
parser.add_argument('--reader', choices=('cthulhu', 'orca'))
|
||||
parser.add_argument('--extension-endpoint', action='store_true')
|
||||
add_test_options(parser)
|
||||
args = parser.parse_args()
|
||||
build = Path(args.build_dir).resolve()
|
||||
if not args.session:
|
||||
sys.exit(subprocess.call(['dbus-run-session', '--', sys.executable, __file__,
|
||||
sys.exit(run_wine_test(['dbus-run-session', '--', sys.executable, __file__,
|
||||
'--session', '--build-dir', str(build),
|
||||
*(['--extension-endpoint'] if args.extension_endpoint else []),
|
||||
*(['--reader', args.reader] if args.reader else []),
|
||||
*(['--32-bit'] if args.is32 else [])]))
|
||||
*(['--32-bit'] if args.is32 else [])], build, "review_integration",
|
||||
keep=args.keep, keepOnFailure=args.keepOnFailure))
|
||||
busName = 'org.gnome.Orca1.Service' if args.reader == 'orca' else 'org.stormux.Cthulhu1.Service'
|
||||
iface = busName.rsplit('.', 1)[0] + '.WineAccess' if args.extension_endpoint else busName
|
||||
path = '/' + busName.replace('.', '/') + ('/WineAccess' if args.extension_endpoint else '')
|
||||
@@ -75,7 +78,7 @@ def request(conn, sender, focus):
|
||||
GLib.VariantType.new('(a{sv})'),Gio.DBusCallFlags.NONE,1500,None,finished)
|
||||
return result
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix='wine-review-wire-') as temporary:
|
||||
with wine_test_directory(prefix='wine-review-wire-') as temporary:
|
||||
env = os.environ.copy()
|
||||
env.update(WINEPREFIX=str(Path(temporary)/'prefix'),WINEDEBUG=os.environ.get('WINE_ACCESS_TEST_DEBUG','-all'),WINEDLLOVERRIDES='mscoree,mshtml=',CTHULHU_WINE_ACCESS_MANAGED='1')
|
||||
fixture = build/('review-fixture32.exe' if args.is32 else 'review-fixture64.exe')
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
"""Run a native Wine fixture with managed temporary files and persistent logs."""
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from wine_test_environment import add_test_options, run_wine_test
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--build-dir", type=Path, required=True)
|
||||
parser.add_argument("--name", required=True)
|
||||
parser.add_argument("--timeout", type=float, default=120)
|
||||
parser.add_argument("--min-free-mib", type=int)
|
||||
add_test_options(parser)
|
||||
parser.add_argument("command", nargs=argparse.REMAINDER)
|
||||
args = parser.parse_args()
|
||||
command = args.command[1:] if args.command[:1] == ["--"] else args.command
|
||||
if not command:
|
||||
parser.error("a test command is required after --")
|
||||
sys.exit(run_wine_test(command, args.build_dir, args.name, timeout=args.timeout,
|
||||
keep=args.keep, keepOnFailure=args.keepOnFailure,
|
||||
minFreeMiB=args.min_free_mib))
|
||||
@@ -0,0 +1,209 @@
|
||||
"""Own disposable Wine test files, saved output and prefix teardown."""
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime, timezone
|
||||
import os
|
||||
from pathlib import Path
|
||||
import selectors
|
||||
import shutil
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
|
||||
def add_test_options(parser):
|
||||
parser.add_argument("--keep", action="store_true",
|
||||
default=os.environ.get("WINE_ACCESS_TEST_KEEP") == "1",
|
||||
help="retain temporary files even after success")
|
||||
parser.add_argument("--clean-on-failure", dest="keepOnFailure", action="store_false",
|
||||
default=os.environ.get("WINE_ACCESS_TEST_KEEP_ON_FAILURE", "1") != "0",
|
||||
help="remove temporary files after failure too; logs remain")
|
||||
|
||||
|
||||
@contextmanager
|
||||
def wine_test_directory(prefix):
|
||||
# The outer runner owns deletion, after all child processes have stopped.
|
||||
# In particular, an inner finally/TemporaryDirectory must not erase the
|
||||
# failed fixture before the runner can retain it for inspection.
|
||||
yield tempfile.mkdtemp(prefix=prefix, dir=os.environ["WINE_ACCESS_TEST_ROOT"])
|
||||
|
||||
|
||||
@contextmanager
|
||||
def test_signals():
|
||||
def interrupted(signum, frame):
|
||||
raise KeyboardInterrupt(signum)
|
||||
|
||||
handlers = {signum: signal.signal(signum, interrupted)
|
||||
for signum in (signal.SIGINT, signal.SIGTERM)}
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
for signum, handler in handlers.items():
|
||||
signal.signal(signum, handler)
|
||||
|
||||
|
||||
def stop_wine_prefixes(testRoot, env, log):
|
||||
prefixes = set()
|
||||
if (testRoot / "prefix").exists():
|
||||
prefixes.add(testRoot / "prefix")
|
||||
for current, dirs, files in os.walk(testRoot, followlinks=False):
|
||||
if "system.reg" in files or "drive_c" in dirs:
|
||||
prefixes.add(Path(current))
|
||||
dirs.clear()
|
||||
stopped = True
|
||||
for prefix in sorted(prefixes):
|
||||
# These paths originate in this run's private directory. Never stop a
|
||||
# server selected by the caller's WINEPREFIX or a symlink outside it.
|
||||
if not prefix.resolve().is_relative_to(testRoot):
|
||||
stopped = False
|
||||
continue
|
||||
prefixEnv = dict(env, WINEPREFIX=str(prefix))
|
||||
try:
|
||||
# -k also returns 1 when the fixture already stopped its server.
|
||||
# -w confirms the final state without treating that as a failure.
|
||||
subprocess.run(["wineserver", "-k"], env=prefixEnv, check=False,
|
||||
timeout=5, stdout=log, stderr=subprocess.STDOUT)
|
||||
subprocess.run(["wineserver", "-w"], env=prefixEnv, check=True,
|
||||
timeout=5, stdout=log, stderr=subprocess.STDOUT)
|
||||
except (OSError, subprocess.SubprocessError) as error:
|
||||
log.write(f"Wine prefix teardown failed: {prefix}: {error}\n".encode())
|
||||
stopped = False
|
||||
return stopped
|
||||
|
||||
|
||||
def run_wine_test(command, buildDir, name, *, timeout=120, keep=False,
|
||||
keepOnFailure=True, minFreeMiB=None):
|
||||
if not name or any(char not in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-" for char in name):
|
||||
raise ValueError("test name must contain only letters, digits, underscores or hyphens")
|
||||
if timeout <= 0:
|
||||
raise ValueError("test timeout must be positive")
|
||||
tempBase = Path(tempfile.gettempdir())
|
||||
if minFreeMiB is None:
|
||||
minFreeMiB = int(os.environ.get("WINE_ACCESS_TEST_MIN_FREE_MIB", "1024"))
|
||||
if minFreeMiB < 0:
|
||||
raise ValueError("minimum free space must not be negative")
|
||||
freeMiB = shutil.disk_usage(tempBase).free // (1024 * 1024)
|
||||
if freeMiB < minFreeMiB:
|
||||
print(f"Wine test {name}: {tempBase} has {freeMiB} MiB free; "
|
||||
f"at least {minFreeMiB} MiB is required. Clean up retained test directories first.",
|
||||
file=sys.stderr, flush=True)
|
||||
return 2
|
||||
|
||||
logDir = Path(buildDir).resolve() / "Testing" / "WineAccessLogs"
|
||||
logDir.mkdir(parents=True, exist_ok=True)
|
||||
stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S.%fZ")
|
||||
logPath = logDir / f"{name}-{stamp}-{os.getpid()}.log"
|
||||
testRoot = Path(tempfile.mkdtemp(prefix=f"wine-access-{name}-", dir=tempBase)).resolve()
|
||||
env = os.environ.copy()
|
||||
env.update(WINE_ACCESS_TEST_ROOT=str(testRoot), TMPDIR=str(testRoot),
|
||||
WINEPREFIX=str(testRoot / "prefix"), CTHULHU_WINE_ACCESS_MANAGED="1",
|
||||
WINEDEBUG=env.get("WINE_ACCESS_TEST_DEBUG", "-all"),
|
||||
WINEDLLOVERRIDES="mscoree,mshtml=", PYTHONUNBUFFERED="1")
|
||||
process = None
|
||||
returnCode = 1
|
||||
|
||||
with logPath.open("wb", buffering=0) as log, test_signals():
|
||||
def report(message):
|
||||
print(message, flush=True)
|
||||
log.write((message + "\n").encode())
|
||||
|
||||
report(f"Wine test directory: {testRoot}")
|
||||
report(f"Wine test log: {logPath}")
|
||||
try:
|
||||
process = subprocess.Popen(command, env=env, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT, start_new_session=True)
|
||||
deadline = time.monotonic() + timeout
|
||||
exitedAt = None
|
||||
with selectors.DefaultSelector() as selector:
|
||||
selector.register(process.stdout, selectors.EVENT_READ)
|
||||
while selector.get_map():
|
||||
now = time.monotonic()
|
||||
if process.poll() is None and now >= deadline:
|
||||
raise subprocess.TimeoutExpired(command, timeout)
|
||||
if process.poll() is not None:
|
||||
if exitedAt is None:
|
||||
exitedAt = now
|
||||
# Detached Wine services may retain the output pipe.
|
||||
# Prefix teardown below closes them; do not wait forever.
|
||||
if now - exitedAt >= 1:
|
||||
break
|
||||
for key, _ in selector.select(.1):
|
||||
data = os.read(key.fd, 32768)
|
||||
if not data:
|
||||
selector.unregister(key.fileobj)
|
||||
continue
|
||||
log.write(data)
|
||||
if hasattr(sys.stdout, "buffer"):
|
||||
sys.stdout.buffer.write(data)
|
||||
sys.stdout.buffer.flush()
|
||||
else:
|
||||
sys.stdout.write(data.decode(errors="replace"))
|
||||
returnCode = process.wait(timeout=max(.1, deadline - time.monotonic()))
|
||||
if returnCode < 0:
|
||||
returnCode = 128 - returnCode
|
||||
except subprocess.TimeoutExpired:
|
||||
returnCode = 124
|
||||
report(f"Wine test timed out after {timeout} seconds")
|
||||
except KeyboardInterrupt as error:
|
||||
returnCode = 128 + (error.args[0] if error.args else signal.SIGINT)
|
||||
report("Wine test interrupted")
|
||||
except OSError as error:
|
||||
report(f"Wine test could not run: {error}")
|
||||
finally:
|
||||
# Ignore repeated interrupts during the bounded teardown, then
|
||||
# restore the caller's handlers. Never signal another test's group.
|
||||
for signum in (signal.SIGINT, signal.SIGTERM):
|
||||
signal.signal(signum, signal.SIG_IGN)
|
||||
if process is not None:
|
||||
try:
|
||||
os.killpg(process.pid, signal.SIGTERM)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
try:
|
||||
process.wait(timeout=1)
|
||||
except subprocess.TimeoutExpired:
|
||||
os.killpg(process.pid, signal.SIGKILL)
|
||||
process.wait(timeout=2)
|
||||
stopped = stop_wine_prefixes(testRoot, env, log)
|
||||
if process is not None:
|
||||
# Save any last diagnostics from services stopped above.
|
||||
os.set_blocking(process.stdout.fileno(), False)
|
||||
while True:
|
||||
try:
|
||||
data = os.read(process.stdout.fileno(), 32768)
|
||||
except BlockingIOError:
|
||||
break
|
||||
if not data:
|
||||
break
|
||||
log.write(data)
|
||||
process.stdout.close()
|
||||
if not stopped:
|
||||
returnCode = returnCode or 1
|
||||
report("Wine teardown did not finish; retaining temporary files")
|
||||
evidenceSaved = True
|
||||
try:
|
||||
for current, _, files in os.walk(testRoot, followlinks=False):
|
||||
for name in files:
|
||||
source = Path(current) / name
|
||||
if source.suffix != ".log" or source.is_symlink():
|
||||
continue
|
||||
destination = logDir / logPath.stem / source.relative_to(testRoot)
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(source, destination)
|
||||
except OSError as error:
|
||||
evidenceSaved = False
|
||||
returnCode = returnCode or 1
|
||||
report(f"Could not save fixture logs; retaining temporary files: {error}")
|
||||
retain = keep or (returnCode != 0 and keepOnFailure) or not stopped or not evidenceSaved
|
||||
if retain:
|
||||
report(f"Retained Wine test directory: {testRoot}")
|
||||
else:
|
||||
try:
|
||||
shutil.rmtree(testRoot)
|
||||
report(f"Removed Wine test directory: {testRoot}")
|
||||
except OSError as error:
|
||||
returnCode = returnCode or 1
|
||||
report(f"Could not remove Wine test directory: {testRoot}: {error}")
|
||||
report(f"Wine test exit status: {returnCode}; log saved: {logPath}")
|
||||
return returnCode
|
||||
@@ -0,0 +1,143 @@
|
||||
"""Check test-file retention, saved evidence, space guards and cancellation."""
|
||||
from contextlib import redirect_stderr, redirect_stdout
|
||||
import io
|
||||
import os
|
||||
from pathlib import Path
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from types import SimpleNamespace
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from wine_test_environment import run_wine_test, stop_wine_prefixes
|
||||
|
||||
|
||||
class WineTestEnvironmentTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temporary = tempfile.TemporaryDirectory(prefix="wine-test-runner-check-")
|
||||
self.addCleanup(self.temporary.cleanup)
|
||||
self.root = Path(self.temporary.name)
|
||||
self.buildDir = self.root / "build"
|
||||
|
||||
def run_fixture(self, source, **options):
|
||||
output = io.StringIO()
|
||||
with patch("wine_test_environment.tempfile.gettempdir", return_value=str(self.root)), \
|
||||
redirect_stdout(output), redirect_stderr(output):
|
||||
code = run_wine_test([sys.executable, "-c", source], self.buildDir,
|
||||
"runner-check", minFreeMiB=0, **options)
|
||||
lines = output.getvalue().splitlines()
|
||||
testRoot = Path(next(line.removeprefix("Wine test directory: ")
|
||||
for line in lines if line.startswith("Wine test directory: ")))
|
||||
logPath = Path(next(line.removeprefix("Wine test log: ")
|
||||
for line in lines if line.startswith("Wine test log: ")))
|
||||
return code, testRoot, logPath.read_text()
|
||||
|
||||
def test_success_removes_files_but_preserves_log(self):
|
||||
code, testRoot, log = self.run_fixture("print('fixture diagnostic')")
|
||||
self.assertEqual(code, 0)
|
||||
self.assertFalse(testRoot.exists())
|
||||
self.assertIn("fixture diagnostic", log)
|
||||
self.assertIn("Removed Wine test directory:", log)
|
||||
|
||||
def test_failure_retains_original_fixture_and_log_by_default(self):
|
||||
source = "import os, pathlib; pathlib.Path(os.environ['WINE_ACCESS_TEST_ROOT'], 'evidence').write_text('fixture'); print('failure detail'); raise SystemExit(7)"
|
||||
code, testRoot, log = self.run_fixture(source)
|
||||
self.assertEqual(code, 7)
|
||||
self.assertEqual((testRoot / "evidence").read_text(), "fixture")
|
||||
self.assertIn("failure detail", log)
|
||||
self.assertIn("Retained Wine test directory:", log)
|
||||
|
||||
def test_fixture_log_survives_successful_cleanup(self):
|
||||
source = "import os, pathlib; pathlib.Path(os.environ['WINE_ACCESS_TEST_ROOT'], 'helper.log').write_text('saved helper detail')"
|
||||
code, testRoot, _ = self.run_fixture(source)
|
||||
self.assertEqual(code, 0)
|
||||
self.assertFalse(testRoot.exists())
|
||||
saved = list((self.buildDir / "Testing/WineAccessLogs").rglob("helper.log"))
|
||||
self.assertEqual(len(saved), 1)
|
||||
self.assertEqual(saved[0].read_text(), "saved helper detail")
|
||||
|
||||
def test_keep_preserves_success_and_clean_option_preserves_failure_status(self):
|
||||
code, testRoot, _ = self.run_fixture("pass", keep=True)
|
||||
self.assertEqual(code, 0)
|
||||
self.assertTrue(testRoot.exists())
|
||||
code, testRoot, log = self.run_fixture("raise SystemExit(9)", keepOnFailure=False)
|
||||
self.assertEqual(code, 9)
|
||||
self.assertFalse(testRoot.exists())
|
||||
self.assertIn("exit status: 9", log)
|
||||
|
||||
def test_cli_retention_switches(self):
|
||||
runner = Path(__file__).with_name("run_wine_test.py")
|
||||
env = dict(os.environ, TMPDIR=str(self.root), WINE_ACCESS_TEST_KEEP="0",
|
||||
WINE_ACCESS_TEST_KEEP_ON_FAILURE="1")
|
||||
for flags, exitCode, retained in (([], 7, True), (["--clean-on-failure"], 7, False),
|
||||
(["--keep"], 0, True)):
|
||||
result = subprocess.run([sys.executable, str(runner), "--build-dir", str(self.buildDir),
|
||||
"--name", "cli-check", "--min-free-mib", "0", *flags,
|
||||
"--", sys.executable, "-c", f"raise SystemExit({exitCode})"],
|
||||
env=env, capture_output=True, text=True, timeout=5)
|
||||
self.assertEqual(result.returncode, exitCode, result.stdout + result.stderr)
|
||||
root = Path(next(line.removeprefix("Wine test directory: ")
|
||||
for line in result.stdout.splitlines() if line.startswith("Wine test directory: ")))
|
||||
self.assertEqual(root.exists(), retained, result.stdout)
|
||||
|
||||
def test_timeout_stops_fixture_and_retains_its_log(self):
|
||||
code, testRoot, log = self.run_fixture("import time; print('before stall', flush=True); time.sleep(60)", timeout=.2)
|
||||
self.assertEqual(code, 124)
|
||||
self.assertTrue(testRoot.exists())
|
||||
self.assertIn("before stall", log)
|
||||
self.assertIn("timed out", log)
|
||||
|
||||
def test_sigterm_retains_files_and_restores_signal_handlers(self):
|
||||
before = signal.getsignal(signal.SIGTERM)
|
||||
code, testRoot, log = self.run_fixture("import os, signal; os.kill(os.getppid(), signal.SIGTERM)")
|
||||
self.assertEqual(code, 143)
|
||||
self.assertTrue(testRoot.exists())
|
||||
self.assertIn("interrupted", log)
|
||||
self.assertEqual(signal.getsignal(signal.SIGTERM), before)
|
||||
|
||||
def test_descendant_holding_output_does_not_hang_runner(self):
|
||||
code, testRoot, log = self.run_fixture("import subprocess; subprocess.Popen(['sleep', '60']); print('parent finished')")
|
||||
self.assertEqual(code, 0)
|
||||
self.assertFalse(testRoot.exists())
|
||||
self.assertIn("parent finished", log)
|
||||
|
||||
def test_failed_teardown_overrides_cleanup_and_success(self):
|
||||
with patch("wine_test_environment.stop_wine_prefixes", return_value=False):
|
||||
code, testRoot, log = self.run_fixture("pass", keepOnFailure=False)
|
||||
self.assertEqual(code, 1)
|
||||
self.assertTrue(testRoot.exists())
|
||||
self.assertIn("teardown did not finish", log)
|
||||
|
||||
def test_low_space_stops_before_starting_or_creating_files(self):
|
||||
output = io.StringIO()
|
||||
with patch("wine_test_environment.shutil.disk_usage", return_value=SimpleNamespace(free=0)), \
|
||||
patch("wine_test_environment.subprocess.Popen") as launch, \
|
||||
redirect_stderr(output):
|
||||
code = run_wine_test(["unused"], self.buildDir, "low-space", minFreeMiB=1024)
|
||||
self.assertEqual(code, 2)
|
||||
launch.assert_not_called()
|
||||
self.assertEqual(list(self.root.iterdir()), [])
|
||||
self.assertIn("at least 1024 MiB", output.getvalue())
|
||||
|
||||
def test_teardown_is_limited_to_owned_prefixes(self):
|
||||
testRoot = self.root / "test"
|
||||
testRoot.mkdir()
|
||||
ownPrefix = testRoot / "fixture" / "prefix"
|
||||
ownPrefix.mkdir(parents=True)
|
||||
(ownPrefix / "system.reg").touch()
|
||||
with patch("wine_test_environment.subprocess.run") as run:
|
||||
self.assertTrue(stop_wine_prefixes(testRoot, {"WINEPREFIX": "/unrelated-prefix"}, io.BytesIO()))
|
||||
self.assertEqual(run.call_count, 2)
|
||||
for call in run.call_args_list:
|
||||
self.assertEqual(call.kwargs["env"]["WINEPREFIX"], str(ownPrefix))
|
||||
(testRoot / "prefix").symlink_to(self.root, target_is_directory=True)
|
||||
with patch("wine_test_environment.subprocess.run") as run:
|
||||
self.assertFalse(stop_wine_prefixes(testRoot, {}, io.BytesIO()))
|
||||
self.assertTrue(all(call.kwargs["env"]["WINEPREFIX"] == str(ownPrefix)
|
||||
for call in run.call_args_list))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user