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.*
|
*.src.tar.*
|
||||||
|
|
||||||
# Editor backups and desktop metadata
|
# Editor backups and desktop metadata
|
||||||
|
__pycache__/
|
||||||
*~
|
*~
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
|
|||||||
+14
-10
@@ -70,6 +70,16 @@ target_compile_options(wine-access PRIVATE
|
|||||||
|
|
||||||
include(CTest)
|
include(CTest)
|
||||||
if(BUILD_TESTING)
|
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")
|
set(foreground_fixture "${CMAKE_CURRENT_BINARY_DIR}/foreground-fixture.exe")
|
||||||
add_custom_command(OUTPUT "${foreground_fixture}"
|
add_custom_command(OUTPUT "${foreground_fixture}"
|
||||||
COMMAND "${MINGW_C_COMPILER}" -std=c11 -Wall -Wextra -mwindows
|
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_features(foreground-focus-test PRIVATE cxx_std_20)
|
||||||
target_compile_options(foreground-focus-test PRIVATE -Wall -Wextra)
|
target_compile_options(foreground-focus-test PRIVATE -Wall -Wextra)
|
||||||
target_link_libraries(foreground-focus-test PRIVATE user32 PkgConfig::XCB)
|
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}")
|
"${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)
|
add_executable(msaa-states-test tests/msaa_states_test.cpp)
|
||||||
target_compile_features(msaa-states-test PRIVATE cxx_std_20)
|
target_compile_features(msaa-states-test PRIVATE cxx_std_20)
|
||||||
target_compile_options(msaa-states-test PRIVATE -Wall -Wextra)
|
target_compile_options(msaa-states-test PRIVATE -Wall -Wextra)
|
||||||
add_test(NAME msaa-states COMMAND "${CMAKE_CURRENT_BINARY_DIR}/msaa-states-test.exe")
|
add_wine_test(msaa-states 35 "${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_executable(review-capture-test tests/review_capture_test.cpp)
|
add_executable(review-capture-test tests/review_capture_test.cpp)
|
||||||
target_compile_features(review-capture-test PRIVATE cxx_std_20)
|
target_compile_features(review-capture-test PRIVATE cxx_std_20)
|
||||||
target_compile_options(review-capture-test PRIVATE -Wall -Wextra)
|
target_compile_options(review-capture-test PRIVATE -Wall -Wextra)
|
||||||
@@ -111,10 +117,8 @@ if(BUILD_TESTING)
|
|||||||
DEPENDS tests/review_fixture.c VERBATIM)
|
DEPENDS tests/review_fixture.c VERBATIM)
|
||||||
add_custom_target(review-fixture${arch} ALL DEPENDS "${review_fixture}")
|
add_custom_target(review-fixture${arch} ALL DEPENDS "${review_fixture}")
|
||||||
add_dependencies(review-capture-test review-fixture${arch})
|
add_dependencies(review-capture-test review-fixture${arch})
|
||||||
add_test(NAME review-capture${arch}
|
add_wine_test(review-capture${arch} 35
|
||||||
COMMAND "${CMAKE_CURRENT_BINARY_DIR}/review-capture-test.exe" --fixture "${review_fixture}")
|
"${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=")
|
|
||||||
endforeach()
|
endforeach()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,46 @@
|
|||||||
# Wine control integration
|
# 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 notifications
|
||||||
|
|
||||||
UIA notification routing uses the actual native application hooks and a private
|
UIA notification routing uses the actual native application hooks and a private
|
||||||
|
|||||||
@@ -4,23 +4,26 @@ import os
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from gi.repository import Gio, GLib
|
from gi.repository import Gio, GLib
|
||||||
|
|
||||||
|
from wine_test_environment import add_test_options, run_wine_test, wine_test_directory
|
||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("--build-dir", default="build")
|
parser.add_argument("--build-dir", default="build")
|
||||||
parser.add_argument("--reader", choices=("cthulhu", "orca"))
|
parser.add_argument("--reader", choices=("cthulhu", "orca"))
|
||||||
parser.add_argument("--extension-endpoint", action="store_true")
|
parser.add_argument("--extension-endpoint", action="store_true")
|
||||||
parser.add_argument("--session", action="store_true")
|
parser.add_argument("--session", action="store_true")
|
||||||
|
add_test_options(parser)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
build = Path(args.build_dir).resolve()
|
build = Path(args.build_dir).resolve()
|
||||||
if not args.session:
|
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),
|
"--session", "--build-dir", str(build),
|
||||||
*(["--extension-endpoint"] if args.extension_endpoint else []),
|
*(["--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"
|
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
|
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()}"
|
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)
|
tempPath = Path(temporary)
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
env.update(WINEPREFIX=str(tempPath / "prefix"), WINEDEBUG="-all",
|
env.update(WINEPREFIX=str(tempPath / "prefix"), WINEDEBUG="-all",
|
||||||
|
|||||||
@@ -9,20 +9,23 @@ from pathlib import Path
|
|||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from gi.repository import Gio, GLib
|
from gi.repository import Gio, GLib
|
||||||
|
|
||||||
|
from wine_test_environment import add_test_options, run_wine_test, wine_test_directory
|
||||||
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("--build-dir", type=Path, default=Path("build"))
|
parser.add_argument("--build-dir", type=Path, default=Path("build"))
|
||||||
parser.add_argument("--session", action="store_true")
|
parser.add_argument("--session", action="store_true")
|
||||||
|
add_test_options(parser)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
buildDir = args.build_dir.resolve()
|
buildDir = args.build_dir.resolve()
|
||||||
if not args.session:
|
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(buildDir)]))
|
"--session", "--build-dir", str(buildDir)], buildDir, "diagnostics_test",
|
||||||
|
keep=args.keep, keepOnFailure=args.keepOnFailure))
|
||||||
|
|
||||||
busName = "org.stormux.Cthulhu1.Service"
|
busName = "org.stormux.Cthulhu1.Service"
|
||||||
objectPath = "/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,)))
|
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)
|
testDir = Path(temporary)
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
env.update(WINEPREFIX=str(testDir / "prefix"), WINEDEBUG="-all",
|
env.update(WINEPREFIX=str(testDir / "prefix"), WINEDEBUG="-all",
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ import os
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from gi.repository import Gio, GLib
|
from gi.repository import Gio, GLib
|
||||||
|
|
||||||
|
from wine_test_environment import add_test_options, run_wine_test, wine_test_directory
|
||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("--build-dir", default="build")
|
parser.add_argument("--build-dir", default="build")
|
||||||
parser.add_argument("--32-bit", dest="is32Bit", action="store_true")
|
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("--getter", action="store_true")
|
||||||
parser.add_argument("--background", action="store_true")
|
parser.add_argument("--background", action="store_true")
|
||||||
parser.add_argument("--popup", action="store_true")
|
parser.add_argument("--popup", action="store_true")
|
||||||
|
add_test_options(parser)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
build = Path(args.build_dir).resolve()
|
build = Path(args.build_dir).resolve()
|
||||||
if not args.session:
|
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,
|
"--session", "--build-dir", str(build), "--reader", args.reader,
|
||||||
*(["--32-bit"] if args.is32Bit else []),
|
*(["--32-bit"] if args.is32Bit else []),
|
||||||
*(["--getter"] if args.getter else []),
|
*(["--getter"] if args.getter else []),
|
||||||
*(["--background"] if args.background 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"
|
interface = "org.gnome.Orca1.Service" if args.reader == "orca" else "org.stormux.Cthulhu1.Service"
|
||||||
objectPath = "/" + interface.replace(".", "/")
|
objectPath = "/" + interface.replace(".", "/")
|
||||||
@@ -67,7 +70,7 @@ def pump_until(predicate, seconds):
|
|||||||
raise AssertionError(f"condition timed out; events: {events}")
|
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)
|
root = Path(temporary)
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
env.update(WINEPREFIX=str(root / "prefix"), WINEDEBUG="-all",
|
env.update(WINEPREFIX=str(root / "prefix"), WINEDEBUG="-all",
|
||||||
|
|||||||
@@ -4,9 +4,16 @@ from pathlib import Path
|
|||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from wine_test_environment import run_wine_test
|
||||||
|
|
||||||
buildDir = Path(sys.argv[1]).resolve()
|
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 = os.environ.copy()
|
||||||
env.update(WINEPREFIX=str(buildDir / "focus-prefix"),
|
env.update(WINEPREFIX=os.environ["WINEPREFIX"],
|
||||||
CTHULHU_WINE_ACCESS_MANAGED="1", WINEDEBUG="-all",
|
CTHULHU_WINE_ACCESS_MANAGED="1", WINEDEBUG="-all",
|
||||||
WINEDLLOVERRIDES="mscoree,mshtml=")
|
WINEDLLOVERRIDES="mscoree,mshtml=")
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -9,11 +9,12 @@ from pathlib import Path
|
|||||||
import subprocess
|
import subprocess
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from gi.repository import Gio, GLib
|
from gi.repository import Gio, GLib
|
||||||
|
|
||||||
|
from wine_test_environment import add_test_options, run_wine_test, wine_test_directory
|
||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("--build-dir", default="build")
|
parser.add_argument("--build-dir", default="build")
|
||||||
parser.add_argument("--session", action="store_true")
|
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("--32-bit", dest="is32Bit", action="store_true")
|
||||||
parser.add_argument("--late-helper", action="store_true")
|
parser.add_argument("--late-helper", action="store_true")
|
||||||
parser.add_argument("--restart-helper", action="store_true")
|
parser.add_argument("--restart-helper", action="store_true")
|
||||||
|
add_test_options(parser)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
if args.is32Bit and args.fixture not in ("menu", "edit", "winforms", "list", "listbox", "delphi", "controls"):
|
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")
|
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")
|
parser.error("--fixture goldwave requires --goldwave-exe pointing to GoldWave 5.70")
|
||||||
build = Path(args.build_dir).resolve()
|
build = Path(args.build_dir).resolve()
|
||||||
if not args.session:
|
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),
|
__file__, "--session", "--build-dir", str(build),
|
||||||
"--fixture", args.fixture,
|
"--fixture", args.fixture,
|
||||||
*(["--extension-endpoint"] if args.extension_endpoint else []),
|
*(["--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 []),
|
*(["--goldwave-exe", str(args.goldwave_exe.resolve())] if args.goldwave_exe else []),
|
||||||
*(["--32-bit"] if args.is32Bit else []),
|
*(["--32-bit"] if args.is32Bit else []),
|
||||||
*(["--late-helper"] if args.late_helper 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"
|
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
|
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,
|
GLib.Variant("(su)", (busName, 0)), None,
|
||||||
Gio.DBusCallFlags.NONE, 1000, 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 = os.environ.copy()
|
||||||
env.update(WINEPREFIX=str(Path(temporary) / "prefix"), WINEDEBUG=os.environ.get("WINE_ACCESS_TEST_DEBUG", "-all"),
|
env.update(WINEPREFIX=str(Path(temporary) / "prefix"), WINEDEBUG=os.environ.get("WINE_ACCESS_TEST_DEBUG", "-all"),
|
||||||
WINEDLLOVERRIDES="mscoree,mshtml=", CTHULHU_WINE_ACCESS_MANAGED="1")
|
WINEDLLOVERRIDES="mscoree,mshtml=", CTHULHU_WINE_ACCESS_MANAGED="1")
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import os
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from gi.repository import Gio, GLib
|
from gi.repository import Gio, GLib
|
||||||
|
|
||||||
|
from wine_test_environment import add_test_options, run_wine_test, wine_test_directory
|
||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("--build-dir", default="build")
|
parser.add_argument("--build-dir", default="build")
|
||||||
parser.add_argument("--reader", choices=("cthulhu", "orca"), default="cthulhu")
|
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("--session", action="store_true")
|
||||||
parser.add_argument("--prism-dll", type=Path)
|
parser.add_argument("--prism-dll", type=Path)
|
||||||
parser.add_argument("--prism-include", type=Path)
|
parser.add_argument("--prism-include", type=Path)
|
||||||
|
add_test_options(parser)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
if bool(args.prism_dll) != bool(args.prism_include) or (args.prism_dll and args.is32Bit):
|
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")
|
parser.error("Prism checks require both --prism-dll and --prism-include, and a 64-bit DLL")
|
||||||
build = Path(args.build_dir).resolve()
|
build = Path(args.build_dir).resolve()
|
||||||
if not args.session:
|
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,
|
"--session", "--build-dir", str(build), "--reader", args.reader,
|
||||||
*(["--prism-dll", str(args.prism_dll.resolve()),
|
*(["--prism-dll", str(args.prism_dll.resolve()),
|
||||||
"--prism-include", str(args.prism_include.resolve())]
|
"--prism-include", str(args.prism_include.resolve())]
|
||||||
if args.prism_dll else []),
|
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"
|
interface = "org.gnome.Orca1.Service" if args.reader == "orca" else "org.stormux.Cthulhu1.Service"
|
||||||
objectPath = "/" + interface.replace(".", "/")
|
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,
|
"RequestName", GLib.Variant("(su)", (interface, 0)), None,
|
||||||
Gio.DBusCallFlags.NONE, 1000, 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)
|
tempPath = Path(temporary)
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
env.update(WINEPREFIX=str(tempPath / "prefix"), WINEDEBUG="-all",
|
env.update(WINEPREFIX=str(tempPath / "prefix"), WINEDEBUG="-all",
|
||||||
|
|||||||
@@ -7,25 +7,28 @@ import os
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from gi.repository import Gio, GLib
|
from gi.repository import Gio, GLib
|
||||||
|
|
||||||
|
from wine_test_environment import add_test_options, run_wine_test, wine_test_directory
|
||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("--build-dir", default="build")
|
parser.add_argument("--build-dir", default="build")
|
||||||
parser.add_argument("--32-bit", dest="is32Bit", action="store_true")
|
parser.add_argument("--32-bit", dest="is32Bit", action="store_true")
|
||||||
parser.add_argument("--timeout", action="store_true")
|
parser.add_argument("--timeout", action="store_true")
|
||||||
parser.add_argument("--focus-error", action="store_true")
|
parser.add_argument("--focus-error", action="store_true")
|
||||||
parser.add_argument("--session", action="store_true")
|
parser.add_argument("--session", action="store_true")
|
||||||
|
add_test_options(parser)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
build = Path(args.build_dir).resolve()
|
build = Path(args.build_dir).resolve()
|
||||||
if not args.session:
|
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),
|
"--session", "--build-dir", str(build),
|
||||||
*(["--32-bit"] if args.is32Bit else []),
|
*(["--32-bit"] if args.is32Bit else []),
|
||||||
*(["--timeout"] if args.timeout 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"
|
interface = "org.stormux.Cthulhu1.Service"
|
||||||
object_path = "/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}")
|
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)
|
root = Path(temporary)
|
||||||
waiting, typed, resume = (root / name for name in ("waiting", "typed", "resume"))
|
waiting, typed, resume = (root / name for name in ("waiting", "typed", "resume"))
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
|
|||||||
@@ -4,24 +4,27 @@ import os
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
|
||||||
import time
|
import time
|
||||||
from gi.repository import Gio, GLib
|
from gi.repository import Gio, GLib
|
||||||
|
|
||||||
|
from wine_test_environment import add_test_options, run_wine_test, wine_test_directory
|
||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument('--build-dir', default='build')
|
parser.add_argument('--build-dir', default='build')
|
||||||
parser.add_argument('--32-bit', dest='is32', action='store_true')
|
parser.add_argument('--32-bit', dest='is32', action='store_true')
|
||||||
parser.add_argument('--session', action='store_true')
|
parser.add_argument('--session', action='store_true')
|
||||||
parser.add_argument('--reader', choices=('cthulhu', 'orca'))
|
parser.add_argument('--reader', choices=('cthulhu', 'orca'))
|
||||||
parser.add_argument('--extension-endpoint', action='store_true')
|
parser.add_argument('--extension-endpoint', action='store_true')
|
||||||
|
add_test_options(parser)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
build = Path(args.build_dir).resolve()
|
build = Path(args.build_dir).resolve()
|
||||||
if not args.session:
|
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),
|
'--session', '--build-dir', str(build),
|
||||||
*(['--extension-endpoint'] if args.extension_endpoint else []),
|
*(['--extension-endpoint'] if args.extension_endpoint else []),
|
||||||
*(['--reader', args.reader] if args.reader 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'
|
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
|
iface = busName.rsplit('.', 1)[0] + '.WineAccess' if args.extension_endpoint else busName
|
||||||
path = '/' + busName.replace('.', '/') + ('/WineAccess' if args.extension_endpoint else '')
|
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)
|
GLib.VariantType.new('(a{sv})'),Gio.DBusCallFlags.NONE,1500,None,finished)
|
||||||
return result
|
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 = 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')
|
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')
|
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