mirror of
https://github.com/x64dbg/x64dbg
synced 2026-08-11 18:23:06 -04:00
Capture crash dumps for tests in CI
This commit is contained in:
parent
385e1ce1d0
commit
d8ea621435
5 changed files with 49 additions and 12 deletions
10
.github/workflows/build.yml
vendored
10
.github/workflows/build.yml
vendored
|
|
@ -88,7 +88,15 @@ jobs:
|
|||
- name: Run Tests
|
||||
shell: pwsh
|
||||
run: |
|
||||
py src/tests/run.py --arch ${{ matrix.arch }} --engine ${{ matrix.engine }} --artifacts-dir "test-artifacts/${{ matrix.arch }}-${{ matrix.engine }}"
|
||||
$artifactDir = Join-Path $PWD "test-artifacts/${{ matrix.arch }}-${{ matrix.engine }}"
|
||||
$dumpDir = Join-Path $artifactDir "crash-dumps"
|
||||
$localDumps = "HKCU:\Software\Microsoft\Windows\Windows Error Reporting\LocalDumps\headless.exe"
|
||||
New-Item -Path $localDumps -Force | Out-Null
|
||||
New-ItemProperty -Path $localDumps -Name DumpFolder -Value $dumpDir -PropertyType ExpandString -Force | Out-Null
|
||||
New-ItemProperty -Path $localDumps -Name DumpType -Value 2 -PropertyType DWord -Force | Out-Null
|
||||
New-ItemProperty -Path $localDumps -Name DumpCount -Value 4 -PropertyType DWord -Force | Out-Null
|
||||
Write-Host "Full headless crash dumps will be written to $dumpDir"
|
||||
py src/tests/run.py --arch ${{ matrix.arch }} --engine ${{ matrix.engine }} --artifacts-dir $artifactDir
|
||||
|
||||
- name: Upload Test Artifacts
|
||||
if: failure()
|
||||
|
|
|
|||
|
|
@ -44,16 +44,25 @@ static void setFirstFailureMessage(const String & message)
|
|||
gTestState.firstFailureMessage = message;
|
||||
}
|
||||
|
||||
static void logTestMessage(const String & message)
|
||||
{
|
||||
// Test protocol messages must bypass the asynchronous debugger logger.
|
||||
// testfinalize closes headless immediately, so a queued FINAL line can be
|
||||
// discarded during process teardown before the test runner observes it.
|
||||
// Start a fresh line in case another thread emitted a partial log message.
|
||||
GuiAddLogMessage(("\n" + message).c_str());
|
||||
}
|
||||
|
||||
static void logAssertionFailure(const char* source, const char* expression, const char* message)
|
||||
{
|
||||
if(expression && *expression && message && *message)
|
||||
dprintf_untranslated("[x64dbg-test] ASSERT FAIL source=%s expr=\"%s\" message=\"%s\"\n", source, expression, message);
|
||||
logTestMessage(StringUtils::sprintf("[x64dbg-test] ASSERT FAIL source=%s expr=\"%s\" message=\"%s\"\n", source, expression, message));
|
||||
else if(expression && *expression)
|
||||
dprintf_untranslated("[x64dbg-test] ASSERT FAIL source=%s expr=\"%s\"\n", source, expression);
|
||||
logTestMessage(StringUtils::sprintf("[x64dbg-test] ASSERT FAIL source=%s expr=\"%s\"\n", source, expression));
|
||||
else if(message && *message)
|
||||
dprintf_untranslated("[x64dbg-test] ASSERT FAIL source=%s message=\"%s\"\n", source, message);
|
||||
logTestMessage(StringUtils::sprintf("[x64dbg-test] ASSERT FAIL source=%s message=\"%s\"\n", source, message));
|
||||
else
|
||||
dprintf_untranslated("[x64dbg-test] ASSERT FAIL source=%s\n", source);
|
||||
logTestMessage(StringUtils::sprintf("[x64dbg-test] ASSERT FAIL source=%s\n", source));
|
||||
}
|
||||
|
||||
static bool assertCommon(bool condition, const char* source, const char* expression, const char* message)
|
||||
|
|
@ -169,9 +178,10 @@ bool cbInstrTestFinalize(int argc, char* argv[])
|
|||
reason = "script_failed";
|
||||
|
||||
if(reason)
|
||||
dprintf_untranslated("[x64dbg-test] FINAL status=fail asserts=%llu reason=%s\n", asserts, reason);
|
||||
logTestMessage(StringUtils::sprintf("[x64dbg-test] FINAL status=fail asserts=%llu reason=%s\n", asserts, reason));
|
||||
else
|
||||
dprintf_untranslated("[x64dbg-test] FINAL status=pass asserts=%llu\n", asserts);
|
||||
logTestMessage(StringUtils::sprintf("[x64dbg-test] FINAL status=pass asserts=%llu\n", asserts));
|
||||
GuiFlushLog();
|
||||
|
||||
if(BridgeIsHeadless())
|
||||
GuiCloseApplication();
|
||||
|
|
|
|||
|
|
@ -23,9 +23,10 @@ static int curScriptId = 0;
|
|||
static bool dbgStopped = false;
|
||||
static DWORD dwGuiThreadId = 0;
|
||||
static moodycamel::BlockingConcurrentQueue<std::function<bool()>> queue;
|
||||
static constexpr DWORD NoShutdownCtrlType = MAXDWORD;
|
||||
static std::atomic<bool> shutdownRequested{ false };
|
||||
static std::atomic<bool> consoleCloseRequested{ false };
|
||||
static std::atomic<DWORD> shutdownCtrlType{ 0 };
|
||||
static std::atomic<DWORD> shutdownCtrlType{ NoShutdownCtrlType };
|
||||
static std::atomic<HANDLE> commandThreadHandle{ nullptr };
|
||||
static std::mutex redirectLogMutex;
|
||||
static FILE* redirectLogFile = nullptr;
|
||||
|
|
@ -141,7 +142,7 @@ extern "C" __declspec(dllexport) int _gui_guiinit(int argc, char* argv[])
|
|||
|
||||
shutdownRequested = false;
|
||||
consoleCloseRequested = false;
|
||||
shutdownCtrlType = 0;
|
||||
shutdownCtrlType = NoShutdownCtrlType;
|
||||
commandThreadHandle = nullptr;
|
||||
|
||||
// Init debugger
|
||||
|
|
@ -492,6 +493,11 @@ extern "C" __declspec(dllexport) const char* _gui_translate_text(const char* sou
|
|||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
// GitHub Actions and other Node-based parents can set
|
||||
// SEM_NOGPFAULTERRORBOX. Clear it so WER LocalDumps can capture an
|
||||
// unhandled headless crash without showing legacy hard-error dialogs.
|
||||
SetErrorMode(SEM_FAILCRITICALERRORS);
|
||||
|
||||
dwGuiThreadId = GetCurrentThreadId();
|
||||
|
||||
// Construct user directory from executable name
|
||||
|
|
|
|||
|
|
@ -42,6 +42,12 @@ def append_log(log_path: Path, text: str) -> None:
|
|||
log_file.write("\n")
|
||||
|
||||
|
||||
def process_exit_reason(prefix: str, returncode: int) -> str:
|
||||
if os.name == "nt" and (returncode < 0 or returncode > 0xFF):
|
||||
return f"{prefix}_0x{returncode & 0xFFFFFFFF:08X}"
|
||||
return f"{prefix}_{returncode}"
|
||||
|
||||
|
||||
def fail(log_path: Path, reason: str, message: str) -> int:
|
||||
append_log(log_path, f'[x64dbg-test] ASSERT FAIL source=driver message="{message}"')
|
||||
append_log(log_path, f"[x64dbg-test] FINAL status=fail asserts=1 reason={reason}")
|
||||
|
|
@ -157,7 +163,8 @@ def main() -> int:
|
|||
|
||||
(artifacts_dir / "headless.stdout.txt").write_text(completed.stdout, encoding="utf-8", errors="replace")
|
||||
if completed.returncode != 0:
|
||||
return fail(log_path, f"headless_exit_{completed.returncode}", f"headless exited with {completed.returncode}")
|
||||
reason = process_exit_reason("headless_exit", completed.returncode)
|
||||
return fail(log_path, reason, f"headless exited with {completed.returncode} ({reason})")
|
||||
|
||||
debug_log = log_path.read_text(encoding="utf-8", errors="replace") if log_path.is_file() else ""
|
||||
if INIT_SCRIPT_MARKER in completed.stdout or INIT_SCRIPT_MARKER in debug_log:
|
||||
|
|
|
|||
|
|
@ -93,6 +93,12 @@ def timeout_output(output: str | bytes | None) -> str:
|
|||
return output or ""
|
||||
|
||||
|
||||
def process_exit_reason(prefix: str, returncode: int) -> str:
|
||||
if os.name == "nt" and (returncode < 0 or returncode > 0xFF):
|
||||
return f"{prefix}_0x{returncode & 0xFFFFFFFF:08X}"
|
||||
return f"{prefix}_{returncode}"
|
||||
|
||||
|
||||
def parse_test_variant(script_name: str) -> str | None:
|
||||
if not script_name.startswith("test") or not script_name.endswith(".txt"):
|
||||
return None
|
||||
|
|
@ -318,7 +324,7 @@ def run_driver_test(headless: Path, test: TestCase, timeout: int, artifact_dir:
|
|||
if completed.returncode != 0:
|
||||
passed = False
|
||||
if reason in {"pass", "missing_final"}:
|
||||
reason = f"driver_exit_{completed.returncode}"
|
||||
reason = process_exit_reason("driver_exit", completed.returncode)
|
||||
if passed and test.fallback_check is not None:
|
||||
passed, reason = run_fallback_check(test.fallback_check, log_path, userdir, test.runtime_dir, artifact_dir)
|
||||
|
||||
|
|
@ -382,7 +388,7 @@ def run_test(headless: Path, test: TestCase, timeout: int, artifact_root: Path,
|
|||
if completed.returncode != 0:
|
||||
passed = False
|
||||
if reason == "pass":
|
||||
reason = f"process_exit_{completed.returncode}"
|
||||
reason = process_exit_reason("process_exit", completed.returncode)
|
||||
if passed and test.fallback_check is not None:
|
||||
passed, reason = run_fallback_check(test.fallback_check, log_path, userdir, test.runtime_dir, artifact_dir)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue