Version and better logging added.
This commit is contained in:
1 parent
b16c6968ae
commit
77db0b83ab
10 files changed
+326
-8
No files matched your search
+8
-1
@@ -44,6 +44,13 @@ set(generated_header "${generated_dir}/nvdaController.h")
|
||||
set(generated_server "${generated_dir}/nvdaController_s.c")
|
||||
file(MAKE_DIRECTORY "${generated_dir}")
|
||||
|
||||
add_custom_target(wine-access-version
|
||||
COMMAND "${CMAKE_COMMAND}" "-DSOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR}"
|
||||
"-DOUTPUT_FILE=${generated_dir}/version.h"
|
||||
-P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/GenerateVersion.cmake"
|
||||
BYPRODUCTS "${generated_dir}/version.h"
|
||||
VERBATIM)
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT "${generated_header}" "${generated_server}"
|
||||
COMMAND "${WIDL_EXECUTABLE}" --win64 -h -H "${generated_header}"
|
||||
@@ -52,7 +59,7 @@ add_custom_command(
|
||||
VERBATIM)
|
||||
|
||||
add_executable(wine-access reader_events.cpp msaa_events.cpp main.cpp foreground_focus.cpp list_view.cpp native_items.cpp review_capture.cpp review_x11.cpp review_service.cpp "${generated_server}")
|
||||
add_dependencies(wine-access wine-access-menu wine-access-menu32)
|
||||
add_dependencies(wine-access wine-access-menu wine-access-menu32 wine-access-version)
|
||||
target_compile_features(wine-access PRIVATE cxx_std_20)
|
||||
target_include_directories(wine-access PRIVATE "${generated_dir}")
|
||||
target_link_libraries(wine-access PRIVATE
|
||||
|
||||
@@ -115,6 +115,44 @@ The helper is started automatically when Cthulhu detects an active Wine
|
||||
loader for the same user and prefix. It requires the selected reader service
|
||||
to be running; it exits if that service is unavailable.
|
||||
|
||||
## Version and diagnostics
|
||||
|
||||
Edit the single line in [`VERSION`](VERSION) to set the release date, using
|
||||
`YYYY.MM.DD`. Every build embeds that version, the source checkout's Git
|
||||
revision and a modified-build flag. Incremental builds refresh the identity
|
||||
after commits or source changes. The flag includes untracked files that Git
|
||||
does not ignore; a source archive without `.git` reports revision `unavailable`.
|
||||
It identifies the helper sidecar being executed, rather than a separate
|
||||
installed version file or the installed package database.
|
||||
|
||||
Query an installed executable directly, for example:
|
||||
|
||||
```sh
|
||||
/usr/libexec/wine-access/wine-access.exe --version
|
||||
```
|
||||
|
||||
This exits before reader discovery or singleton acquisition, so it works
|
||||
without a reader and while another helper is running. Wine itself still needs
|
||||
to initialize normally. To identify an already running helper, use its startup
|
||||
record; replacing files on disk does not update the running process.
|
||||
|
||||
Ordinary startup writes `wine-access: identity` and `wine-access: ready` JSON
|
||||
records to stderr. Identity is emitted even when later startup fails. The ready
|
||||
record means initialization reached the message loop; inspect its individual
|
||||
booleans for unavailable components. Hook handles and `host32_started` report
|
||||
installation/launch success, not attachment to a particular application or
|
||||
confirmation that the companion installed its hooks. Failures retain concise
|
||||
`wine-access:` diagnostics without accessible event text or password values.
|
||||
Reader replies that merely filter duplicate/unwanted events are not errors.
|
||||
|
||||
Updated Cthulhu versions capture these records in their normal log, including
|
||||
worker/hook startup failures, reader-call failures, recovery and queue overflow.
|
||||
Executable, loader and prefix paths appear only in Cthulhu's debug log. For a
|
||||
bug report, include the helper identity and initialization records, relevant
|
||||
diagnostics and the exact navigation steps. Review debug output for private
|
||||
text before sharing. No reader-specific logging service is required by the
|
||||
helper; other readers can consume the same stderr records.
|
||||
|
||||
## Reader selection and Orca integration
|
||||
|
||||
The default invocation and `--reader cthulhu` use the existing Cthulhu endpoint.
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# Refresh on every build, including incremental builds after a commit or edit.
|
||||
# configure_file only changes the header when the embedded identity changes.
|
||||
file(READ "${SOURCE_DIR}/VERSION" releaseVersion)
|
||||
string(STRIP "${releaseVersion}" releaseVersion)
|
||||
if(NOT releaseVersion MATCHES "^[0-9][0-9][0-9][0-9][.][0-9][0-9][.][0-9][0-9]$")
|
||||
message(FATAL_ERROR "VERSION must contain a date in YYYY.MM.DD format")
|
||||
endif()
|
||||
set(gitRevision "unavailable")
|
||||
set(gitModified "false")
|
||||
set(buildSuffix "")
|
||||
find_package(Git QUIET)
|
||||
# Do not accidentally identify a source archive as its enclosing repository.
|
||||
if(GIT_FOUND AND EXISTS "${SOURCE_DIR}/.git")
|
||||
execute_process(COMMAND "${GIT_EXECUTABLE}" rev-parse --short=12 HEAD
|
||||
WORKING_DIRECTORY "${SOURCE_DIR}" RESULT_VARIABLE revisionResult
|
||||
OUTPUT_VARIABLE revision OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET)
|
||||
execute_process(COMMAND "${GIT_EXECUTABLE}" status --porcelain --untracked-files=normal
|
||||
WORKING_DIRECTORY "${SOURCE_DIR}" RESULT_VARIABLE statusResult
|
||||
OUTPUT_VARIABLE status OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET)
|
||||
if(revisionResult EQUAL 0 AND revision MATCHES "^[0-9a-f]+$")
|
||||
set(gitRevision "${revision}")
|
||||
endif()
|
||||
if(statusResult EQUAL 0 AND NOT status STREQUAL "")
|
||||
set(gitModified "true")
|
||||
set(buildSuffix ", modified")
|
||||
endif()
|
||||
endif()
|
||||
configure_file("${SOURCE_DIR}/cmake/version.h.in" "${OUTPUT_FILE}" @ONLY)
|
||||
@@ -0,0 +1,6 @@
|
||||
// Generated from VERSION and the source checkout; do not edit.
|
||||
#pragma once
|
||||
#define WINE_ACCESS_VERSION "@releaseVersion@"
|
||||
#define WINE_ACCESS_REVISION "@gitRevision@"
|
||||
#define WINE_ACCESS_MODIFIED "@gitModified@"
|
||||
#define WINE_ACCESS_BUILD "Wine Access @releaseVersion@ (@gitRevision@@buildSuffix@)"
|
||||
@@ -1,6 +1,6 @@
|
||||
# Maintainer: Storm Dragon <storm_dragon@stormux.org>
|
||||
pkgname=wine-access-git
|
||||
pkgver=r40.g4c47cc5
|
||||
pkgver=r41.gb16c696
|
||||
pkgrel=1
|
||||
pkgdesc='Wine accessibility helper with MSAA, NVDA Controller, and Tolk compatibility for screen readers'
|
||||
arch=('x86_64')
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "reader_endpoint.h"
|
||||
#include "msaa_events.h"
|
||||
#include "reader_events.h"
|
||||
#include "version.h"
|
||||
|
||||
#include <commctrl.h>
|
||||
#include <gio/gio.h>
|
||||
@@ -1049,6 +1050,17 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE, char *, int) {
|
||||
int argumentCount = 0;
|
||||
wchar_t **arguments = CommandLineToArgvW(commandLine, &argumentCount);
|
||||
if (arguments == nullptr) return 1;
|
||||
// Version queries must work without a reader, even while another helper owns
|
||||
// the singleton. The identity is embedded in this executable's sidecar.
|
||||
for (int index = 1; index < argumentCount; ++index) {
|
||||
if (lstrcmpW(arguments[index], L"--version") == 0) {
|
||||
std::puts(WINE_ACCESS_BUILD);
|
||||
LocalFree(arguments);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
std::fprintf(stderr, "wine-access: identity {\"version\":\"%s\",\"revision\":\"%s\",\"modified\":%s}\n",
|
||||
WINE_ACCESS_VERSION, WINE_ACCESS_REVISION, WINE_ACCESS_MODIFIED);
|
||||
bool validReader = true;
|
||||
bool validEndpoint = true;
|
||||
std::string readerPath, readerInterface;
|
||||
@@ -1097,14 +1109,20 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE, char *, int) {
|
||||
// These strings remain alive until the workers have stopped at shutdown.
|
||||
if (!readerPath.empty()) readerEndpoint.objectPath = readerPath.c_str();
|
||||
if (!readerInterface.empty()) readerEndpoint.interfaceName = readerInterface.c_str();
|
||||
if (!dialogReaderEnabled && !controllerEnabled)
|
||||
if (!dialogReaderEnabled && !controllerEnabled) {
|
||||
std::fprintf(stderr, "wine-access: disabled by command-line options\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
HANDLE singleton =
|
||||
CreateMutexW(nullptr, TRUE, L"Local\\cthulhu-wine-access-singleton");
|
||||
if (singleton == nullptr)
|
||||
if (singleton == nullptr) {
|
||||
std::fprintf(stderr, "wine-access: singleton creation failed (error %lu)\n",
|
||||
static_cast<unsigned long>(GetLastError()));
|
||||
return 1;
|
||||
}
|
||||
if (GetLastError() == ERROR_ALREADY_EXISTS) {
|
||||
std::fprintf(stderr, "wine-access: another helper already owns this Wine desktop\n");
|
||||
CloseHandle(singleton);
|
||||
return 0;
|
||||
}
|
||||
@@ -1147,8 +1165,11 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE, char *, int) {
|
||||
CloseHandle(singleton);
|
||||
return 1;
|
||||
}
|
||||
reviewService.start(connection,reviewIdentities,readerEndpoint);
|
||||
if (dialogReaderEnabled && !msaaEvents.start()) {
|
||||
const bool reviewActive = reviewService.start(connection,reviewIdentities,readerEndpoint);
|
||||
if (!reviewActive)
|
||||
std::fprintf(stderr, "wine-access: review service unavailable\n");
|
||||
const bool msaaActive = dialogReaderEnabled && msaaEvents.start();
|
||||
if (dialogReaderEnabled && !msaaActive) {
|
||||
std::fprintf(stderr, "wine-access: MSAA event worker unavailable\n");
|
||||
}
|
||||
bool controllerActive = false;
|
||||
@@ -1171,6 +1192,8 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE, char *, int) {
|
||||
DestroyWindow(window);
|
||||
window = nullptr;
|
||||
}
|
||||
if (controllerEnabled && !controllerActive)
|
||||
std::fprintf(stderr, "wine-access: controller endpoint unavailable\n");
|
||||
|
||||
HWINEVENTHOOK hook = nullptr;
|
||||
HHOOK menuHook = nullptr;
|
||||
@@ -1178,9 +1201,13 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE, char *, int) {
|
||||
HMODULE menuModule = nullptr;
|
||||
HWND menuBridge = nullptr;
|
||||
HANDLE menuHost32 = nullptr;
|
||||
DWORD menuHookError = 0, inputHookError = 0;
|
||||
if (dialogReaderEnabled) {
|
||||
hook = SetWinEventHook(EVENT_MIN, EVENT_MAX, nullptr, winevent_callback, 0,
|
||||
0, WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS);
|
||||
if (hook == nullptr)
|
||||
std::fprintf(stderr, "wine-access: WinEvent hook unavailable (error %lu)\n",
|
||||
static_cast<unsigned long>(GetLastError()));
|
||||
WNDCLASSW bridgeClass{};
|
||||
bridgeClass.lpfnWndProc = menu_bridge_proc;
|
||||
bridgeClass.hInstance = instance;
|
||||
@@ -1189,18 +1216,27 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE, char *, int) {
|
||||
menuBridge = CreateWindowExW(WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW,
|
||||
WINE_ACCESS_MENU_CLASS, L"", 0, 0, 0, 0, 0,
|
||||
nullptr, nullptr, instance, nullptr);
|
||||
if (menuBridge != nullptr && (menuModule = load_menu_hook()) != nullptr) {
|
||||
if (menuBridge == nullptr) {
|
||||
menuHookError = inputHookError = GetLastError();
|
||||
} else if ((menuModule = load_menu_hook()) == nullptr) {
|
||||
menuHookError = inputHookError = GetLastError();
|
||||
} else {
|
||||
const auto callback = std::bit_cast<HOOKPROC>(GetProcAddress(menuModule, "WineAccessMenuHook"));
|
||||
if (callback != nullptr)
|
||||
menuHook = SetWindowsHookExW(WH_CALLWNDPROC, callback, menuModule, 0);
|
||||
if (menuHook == nullptr) menuHookError = GetLastError();
|
||||
const auto inputCallback = std::bit_cast<HOOKPROC>(GetProcAddress(menuModule, "WineAccessInputHook"));
|
||||
if (inputCallback != nullptr)
|
||||
inputHook = SetWindowsHookExW(WH_GETMESSAGE, inputCallback, menuModule, 0);
|
||||
if (inputHook == nullptr) inputHookError = GetLastError();
|
||||
editReaderActive = menuHook != nullptr && inputHook != nullptr;
|
||||
}
|
||||
if (menuHook == nullptr)
|
||||
std::fprintf(stderr, "wine-access: native menu hook unavailable (error %lu)\n",
|
||||
static_cast<unsigned long>(GetLastError()));
|
||||
static_cast<unsigned long>(menuHookError));
|
||||
if (inputHook == nullptr)
|
||||
std::fprintf(stderr, "wine-access: native input hook unavailable (error %lu)\n",
|
||||
static_cast<unsigned long>(inputHookError));
|
||||
if (menuBridge != nullptr) {
|
||||
menuHost32 = start_menu_host32(menuBridge);
|
||||
if (menuHost32 == nullptr)
|
||||
@@ -1212,6 +1248,18 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE, char *, int) {
|
||||
// Native lists, tabs, trees and combo boxes can omit change WinEvents in Wine.
|
||||
// Observe only the foreground control; never read background selections.
|
||||
const UINT_PTR listTimer = dialogReaderEnabled ? SetTimer(nullptr, 0, 100, nullptr) : 0;
|
||||
if (dialogReaderEnabled && !listTimer)
|
||||
std::fprintf(stderr, "wine-access: control polling timer unavailable\n");
|
||||
// Initialization status is distinct from process creation. Hook handles and
|
||||
// a started host do not prove attachment to a particular application.
|
||||
std::fprintf(stderr, "wine-access: ready {\"reader\":\"%s\",\"controls_enabled\":%s,"
|
||||
"\"controller\":%s,\"review\":%s,\"msaa_worker\":%s,\"win_events\":%s,"
|
||||
"\"menu_hook\":%s,\"input_hook\":%s,\"host32_started\":%s,\"control_poll\":%s}\n",
|
||||
readerEndpoint.busName, dialogReaderEnabled ? "true" : "false",
|
||||
controllerActive ? "true" : "false", reviewActive ? "true" : "false",
|
||||
msaaActive ? "true" : "false", hook ? "true" : "false",
|
||||
menuHook ? "true" : "false", inputHook ? "true" : "false",
|
||||
menuHost32 ? "true" : "false", listTimer ? "true" : "false");
|
||||
while (GetMessageW(&message, nullptr, 0, 0) > 0) {
|
||||
if (listTimer && message.message == WM_TIMER && message.hwnd == nullptr &&
|
||||
message.wParam == listTimer) {
|
||||
|
||||
@@ -574,3 +574,27 @@ the new focus event; the Go presenter must suppress that unchanged state
|
||||
presentation. For live acceptance, change the document, close Notepad and
|
||||
verify each Tab announces only the newly focused button, including reverse
|
||||
navigation. Escape must return to the editor without discarding the document.
|
||||
|
||||
## Helper identity and startup diagnostics
|
||||
|
||||
```sh
|
||||
python3 tests/version_metadata_test.py
|
||||
xvfb-run -a python3 tests/diagnostics_test.py --build-dir build
|
||||
```
|
||||
|
||||
The metadata check uses a temporary Git repository and a source archive to
|
||||
verify clean/modified builds, version edits, commit changes and unchanged
|
||||
header timestamps. The Wine check uses a private D-Bus service, display and
|
||||
prefix. It queries `--version` without a reader and while the singleton is
|
||||
held, verifies identity on failed reader discovery, and checks normal and
|
||||
missing-hook initialization records. It never contacts the active reader.
|
||||
|
||||
After installing both updates and restarting Cthulhu, launch AudioQuake and
|
||||
check its log for `Wine accessibility helper identity` and
|
||||
`Wine accessibility helper initialized`, with the expected version/revision.
|
||||
Enable debug logging to confirm the executable, loader and prefix selected.
|
||||
Check the launcher list with Down/Down/Up and Tab/Shift+Tab, its map chooser,
|
||||
and returning to the launcher after game exit. Recheck ordinary Wine Notepad
|
||||
typing, arrow navigation, menus and shutdown, including password exclusion.
|
||||
Audible speech and physical Braille require separate live checks. This change
|
||||
adds diagnostics; it does not establish a fix for a remote machine's silence.
|
||||
@@ -0,0 +1,110 @@
|
||||
"""Exercise version/startup diagnostics on a private D-Bus session and Wine prefix.
|
||||
|
||||
Run under xvfb-run. No requests go to the active screen reader.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
from gi.repository import Gio, GLib
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--build-dir", type=Path, default=Path("build"))
|
||||
parser.add_argument("--session", action="store_true")
|
||||
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)]))
|
||||
|
||||
busName = "org.stormux.Cthulhu1.Service"
|
||||
objectPath = "/org/stormux/Cthulhu1/Service"
|
||||
xml = '<node><interface name="' + busName + '">'
|
||||
for method, signature in (("PresentAccessibleEvent", "sssssssiis"),
|
||||
("SpeakText", "sb"), ("CancelSpeech", "")):
|
||||
xml += '<method name="' + method + '">'
|
||||
xml += ''.join('<arg type="' + kind + '" direction="in"/>' for kind in signature)
|
||||
xml += '<arg type="b" direction="out"/></method>'
|
||||
xml += '</interface></node>'
|
||||
|
||||
|
||||
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:
|
||||
testDir = Path(temporary)
|
||||
env = os.environ.copy()
|
||||
env.update(WINEPREFIX=str(testDir / "prefix"), WINEDEBUG="-all",
|
||||
WINEDLLOVERRIDES="mscoree,mshtml=", CTHULHU_WINE_ACCESS_MANAGED="1")
|
||||
helper = None
|
||||
try:
|
||||
subprocess.run(["wineboot", "-u"], env=env, check=True, timeout=45)
|
||||
executable = buildDir / "wine-access.exe"
|
||||
version = subprocess.run([str(executable), "--version"], env=env,
|
||||
check=True, capture_output=True, text=True, timeout=5)
|
||||
assert version.stdout.startswith("Wine Access ") and not version.stderr
|
||||
# No reader exists yet; ordinary startup must identify itself and fail.
|
||||
failed = subprocess.run([str(executable), "--no-controller"], env=env,
|
||||
capture_output=True, text=True, timeout=5)
|
||||
assert failed.returncode != 0 and "reader service" in failed.stderr
|
||||
assert "wine-access: identity " in failed.stderr and "wine-access: ready " not in failed.stderr
|
||||
|
||||
connection = Gio.bus_get_sync(Gio.BusType.SESSION, None)
|
||||
connection.register_object(objectPath, Gio.DBusNodeInfo.new_for_xml(xml).interfaces[0], record, None, None)
|
||||
connection.call_sync("org.freedesktop.DBus", "/org/freedesktop/DBus",
|
||||
"org.freedesktop.DBus", "RequestName",
|
||||
GLib.Variant("(su)", (busName, 0)), None,
|
||||
Gio.DBusCallFlags.NONE, 1000, None)
|
||||
for missingHooks in (False, True):
|
||||
selected = executable
|
||||
if missingHooks:
|
||||
incompleteDir = testDir / "incomplete"
|
||||
incompleteDir.mkdir()
|
||||
for name in ("wine-access.exe", "wine-access.exe.so"):
|
||||
shutil.copy2(buildDir / name, incompleteDir / name)
|
||||
selected = incompleteDir / "wine-access.exe"
|
||||
logPath = testDir / ("missing.log" if missingHooks else "healthy.log")
|
||||
with logPath.open("w") as diagnostics:
|
||||
helper = subprocess.Popen([str(selected), "--no-controller"], env=env,
|
||||
stdout=subprocess.DEVNULL, stderr=diagnostics)
|
||||
deadline = time.monotonic() + 5
|
||||
while "wine-access: ready " not in logPath.read_text() and time.monotonic() < deadline:
|
||||
while GLib.MainContext.default().iteration(False):
|
||||
pass
|
||||
assert helper.poll() is None, logPath.read_text()
|
||||
time.sleep(0.01)
|
||||
lines = logPath.read_text().splitlines()
|
||||
identity = next(json.loads(line.removeprefix("wine-access: identity "))
|
||||
for line in lines if line.startswith("wine-access: identity "))
|
||||
ready = next(json.loads(line.removeprefix("wine-access: ready "))
|
||||
for line in lines if line.startswith("wine-access: ready "))
|
||||
assert identity["version"] in version.stdout and identity["revision"] in version.stdout
|
||||
for field in ("controls_enabled", "review", "msaa_worker", "win_events", "control_poll"):
|
||||
assert ready[field], (field, ready)
|
||||
assert not ready["controller"]
|
||||
for field in ("menu_hook", "input_hook", "host32_started"):
|
||||
assert ready[field] == (not missingHooks), (field, ready)
|
||||
if missingHooks:
|
||||
assert "native menu hook unavailable" in logPath.read_text()
|
||||
assert "32-bit menu host unavailable" in logPath.read_text()
|
||||
assert "unavailable (error 0)" not in logPath.read_text(), logPath.read_text()
|
||||
# A query must bypass the singleton held by this helper.
|
||||
queried = subprocess.run([str(selected), "--version"], env=env,
|
||||
check=True, capture_output=True, text=True, timeout=5)
|
||||
assert queried.stdout == version.stdout and not queried.stderr
|
||||
helper.terminate()
|
||||
helper.wait(timeout=5)
|
||||
helper = None
|
||||
finally:
|
||||
subprocess.run(["wineserver", "-k"], env=env, timeout=10, check=False)
|
||||
if helper is not None:
|
||||
helper.wait(timeout=10)
|
||||
print("PASS: version without reader/with singleton, unavailable reader, healthy and missing-hook startup")
|
||||
@@ -0,0 +1,56 @@
|
||||
"""Check build identity refresh and source-archive handling without running Wine."""
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
|
||||
projectDir = Path(__file__).resolve().parents[1]
|
||||
with tempfile.TemporaryDirectory(prefix="wine-access-version-") as temporary:
|
||||
sourceDir = Path(temporary) / "source"
|
||||
sourceDir.mkdir()
|
||||
shutil.copytree(projectDir / "cmake", sourceDir / "cmake")
|
||||
(sourceDir / "VERSION").write_text("2026.10.15\n")
|
||||
outputFile = Path(temporary) / "version.h"
|
||||
|
||||
def generate_version():
|
||||
subprocess.run(["cmake", f"-DSOURCE_DIR={sourceDir}",
|
||||
f"-DOUTPUT_FILE={outputFile}", "-P",
|
||||
str(sourceDir / "cmake/GenerateVersion.cmake")], check=True)
|
||||
return outputFile.read_text()
|
||||
|
||||
def git(*arguments):
|
||||
return subprocess.check_output(["git", "-C", str(sourceDir), *arguments],
|
||||
text=True, stderr=subprocess.PIPE).strip()
|
||||
|
||||
assert 'WINE_ACCESS_REVISION "unavailable"' in generate_version()
|
||||
git("init", "--quiet")
|
||||
git("config", "user.name", "Fixture Author")
|
||||
git("config", "user.email", "author@example.invalid")
|
||||
git("add", ".")
|
||||
git("-c", "commit.gpgsign=false", "commit", "--quiet", "-m", "Initial fixture")
|
||||
firstRevision = git("rev-parse", "--short=12", "HEAD")
|
||||
cleanHeader = generate_version()
|
||||
assert firstRevision in cleanHeader and 'WINE_ACCESS_MODIFIED "false"' in cleanHeader
|
||||
unchangedTime = outputFile.stat().st_mtime_ns
|
||||
assert generate_version() == cleanHeader
|
||||
assert outputFile.stat().st_mtime_ns == unchangedTime, "unchanged build rewrote version header"
|
||||
(sourceDir / "VERSION").write_text("2026.10.16\n")
|
||||
dirtyHeader = generate_version()
|
||||
assert 'WINE_ACCESS_VERSION "2026.10.16"' in dirtyHeader
|
||||
assert 'WINE_ACCESS_MODIFIED "true"' in dirtyHeader and ", modified)" in dirtyHeader
|
||||
git("add", "VERSION")
|
||||
git("-c", "commit.gpgsign=false", "commit", "--quiet", "-m", "Updated fixture")
|
||||
secondRevision = git("rev-parse", "--short=12", "HEAD")
|
||||
assert secondRevision != firstRevision
|
||||
assert secondRevision in generate_version(), "incremental identity retained previous commit"
|
||||
(sourceDir / "untracked.txt").write_text("fixture\n")
|
||||
assert 'WINE_ACCESS_MODIFIED "true"' in generate_version()
|
||||
# An archive nested inside another repository must not borrow its identity.
|
||||
archiveDir = sourceDir / "archive"
|
||||
archiveDir.mkdir()
|
||||
shutil.copytree(projectDir / "cmake", archiveDir / "cmake")
|
||||
(archiveDir / "VERSION").write_text("2026.10.15\n")
|
||||
sourceDir = archiveDir
|
||||
assert 'WINE_ACCESS_REVISION "unavailable"' in generate_version()
|
||||
print("PASS: clean, modified, incremental, unchanged and archive build identities")
|
||||
Reference in new issue
Block a user