mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/telegramdesktop/tdesktop
synced 2026-09-20 08:03:45 +08:00
[ai] Report a death after TEST_COMPLETE in test-run
Task: 2026/08/10/report-post-complete-death-in-test-run
This commit is contained in:
@@ -191,9 +191,10 @@ handles that unmarked live folder by step 3.
|
||||
|
||||
Deletion guard — the only folder the flow may ever delete is a live `TelegramForcePortable` that
|
||||
either carries the `testing` marker or coexists with `real_...` (step 3). If the test account
|
||||
breaks mid-loop (login screen, `AUTH_KEY_DUPLICATED`), delete the MARKED live folder, re-run SETUP
|
||||
for a fresh golden copy, and retry once; if it is still broken the run is UNRECOVERABLE. Never
|
||||
delete or alter `test_...` or `real_...` under any circumstances.
|
||||
breaks mid-loop (login screen, `AUTH_KEY_DUPLICATED`), delete the MARKED live folder (that deletion
|
||||
takes the Crashpad database under `tdata/dumps/completed/` with it, so copy out any dump worth
|
||||
keeping first), re-run SETUP for a fresh golden copy, and retry once; if it is still broken the run
|
||||
is UNRECOVERABLE. Never delete or alter `test_...` or `real_...` under any circumstances.
|
||||
|
||||
**Serialize app runs.** Never have two `Telegram.exe` instances alive against this account at once —
|
||||
concurrent reuse of one auth key can trigger a server-side session reset. Before SETUP, launching, or
|
||||
@@ -519,9 +520,16 @@ bypass it with hand-built relative paths.
|
||||
`<EVIDENCE_DIR>/app_stdout.txt` and stderr to `<EVIDENCE_DIR>/app_stderr.txt` (the flag prevents
|
||||
modal crash hangs, and stderr captures assertion text), enforces **a hard wall-clock deadline
|
||||
from launch** and a quiet-log watchdog while polling `<EVIDENCE_DIR>/test_log.txt`, detects
|
||||
`TEST_COMPLETE` (success) versus process death (crash) versus the caps elapsing (hang), kills any
|
||||
`TEST_COMPLETE` versus process death (crash) versus the caps elapsing (hang), kills any
|
||||
straggler, and returns one JSON report with the parsed markers, stderr tail, fresh crash
|
||||
diagnostics, and `stale_crash_cleared`. That field is an ordered list of `{from, kind, to}`
|
||||
diagnostics, `crashpad_dumps_added`, `death_signals`, and `stale_crash_cleared`.
|
||||
`TEST_COMPLETE` alone is not success: when the process writes it and then dies, the verdict is
|
||||
`died-after-complete`, not `complete`, on any of three independent signals — a non-zero
|
||||
`exit_code`, a new `.dmp` in the live `tdata/dumps/completed/` Crashpad database across the run,
|
||||
or a fresh top-level `tdata/dumps/*.dmp` from a Breakpad build. `crashpad_dumps_added` is that
|
||||
before/after delta, listed in full because `test-run` never clears `completed/` between runs;
|
||||
`death_signals` names which of `"breakpad_dump"`, `"crashpad_dump"` and `"exit_code"` fired, and
|
||||
is `[]` for a healthy run. `stale_crash_cleared` is an ordered list of `{from, kind, to}`
|
||||
entries whose `kind` is `"report"` or `"dump"`, and is `[]` when nothing was cleared. If the
|
||||
stale report cannot be moved, `test-run` refuses before launch, prints the helper error on stderr,
|
||||
exits non-zero, and emits no JSON. If a dump cannot be moved, `test-run` leaves it in place,
|
||||
@@ -545,11 +553,15 @@ set, the binary:
|
||||
`<EVIDENCE_DIR>/app_stderr.txt`, tagged `[testagent]`;
|
||||
- also turns on debug logging (`-testagent` implies `-debug`).
|
||||
|
||||
**Do NOT key the crash decision on exit code.** Breakpad handles the crash and the process usually
|
||||
exits **0** — exactly as tdesktop's own crash detection assumes. The reliable crash signals are: the
|
||||
process is gone WITHOUT a `TEST_COMPLETE` marker, AND a fresh non-empty
|
||||
`<workdir>/tdata/working` exists. On macOS, a fresh matching system `.ips`
|
||||
report is also sufficient when Telegram's reporter wrote nothing. So **always
|
||||
**Do NOT key a pre-`TEST_COMPLETE` crash decision on exit code.** Breakpad handles the crash and
|
||||
the process usually exits **0** — exactly as tdesktop's own crash detection assumes. The reliable
|
||||
crash signals before `TEST_COMPLETE` are: the process is gone WITHOUT a `TEST_COMPLETE` marker,
|
||||
AND a fresh non-empty `<workdir>/tdata/working` exists. On macOS, a fresh matching system `.ips`
|
||||
report is also sufficient when Telegram's reporter wrote nothing. **After** `TEST_COMPLETE` the
|
||||
opposite holds: `CrashReports::Finish()` unlinks `tdata/working` during the clean shutdown that
|
||||
precedes a teardown fault, so the only signals left are a non-zero `exit_code`, a new
|
||||
`tdata/dumps/completed/*.dmp` on the macOS Crashpad build, and a new top-level `tdata/dumps/*.dmp`
|
||||
on the Breakpad builds — `test-run` reads all three and reports `died-after-complete`. So **always
|
||||
pass `-testagent`**, and on a crash gather diagnostics in this order before
|
||||
deciding the verdict:
|
||||
|
||||
@@ -559,8 +571,12 @@ deciding the verdict:
|
||||
`CrtAssert:` annotations, the failed `file:line`, and `Caught signal …` / minidump id. Plain text;
|
||||
read it directly. `<workdir>` is the launch `-workdir` (in portable test runs,
|
||||
`out/Debug/TelegramForcePortable/`).
|
||||
3. **`<workdir>/tdata/dumps/*.dmp`** — the minidump (full stack, needs symbols to read; note its path
|
||||
in `test.md`, don't try to symbolize inline).
|
||||
3. **`<workdir>/tdata/dumps/`** — the minidump (full stack, needs symbols to read; note its path in
|
||||
`test.md`, don't try to symbolize inline). Breakpad writes `*.dmp` at that top level; the macOS
|
||||
Crashpad build keeps its database one directory below, in `<workdir>/tdata/dumps/completed/`, so
|
||||
a top-level listing can be empty while a real dump exists. `test-run` reports this run's fresh
|
||||
top-level dumps in `dumps` and its new `completed/` entries in `crashpad_dumps_added`, so on a
|
||||
Breakpad build the first is the field to read and the second is always `[]`.
|
||||
4. **macOS `~/Library/Logs/DiagnosticReports/Telegram-*.ips`** — when the preceding files are empty,
|
||||
inspect reports created after the exact process launch and match the app UUID/start time. These
|
||||
reports can contain a fully symbolicated stack even when Telegram's reporter wrote nothing.
|
||||
|
||||
@@ -575,19 +575,30 @@ rules, with these external-task safety adaptations:
|
||||
wall-clock deadline and quiet-log watchdog and returns one JSON report:
|
||||
outcome, `TEST_COMPLETE` state, parsed
|
||||
`TEST_STEP`/`TEST_RESULT`/`SCREENSHOT` markers, stderr tail, fresh
|
||||
`tdata/working` crash excerpt, minidump paths, and
|
||||
`stale_crash_cleared`. That field is an ordered list of
|
||||
`{from, kind, to}` entries whose `kind` is `"report"` or `"dump"`, and is
|
||||
`[]` when nothing was cleared. If a stale report cannot be moved, `test-run`
|
||||
refuses before launch, prints the helper error on stderr, exits non-zero,
|
||||
and emits no JSON. If a dump cannot be moved, the run leaves it in place,
|
||||
records its entry with `"to": null` (a null destination), and continues to
|
||||
launch. The performer then judges the evidence itself — the runner gathers,
|
||||
it never assesses. Crash detection keys on process death without
|
||||
`TEST_COMPLETE` plus a fresh `tdata/working`, not exit code.
|
||||
`tdata/working` crash excerpt, minidump paths, `crashpad_dumps_added`,
|
||||
`death_signals`, and `stale_crash_cleared`. `crashpad_dumps_added` is the
|
||||
before/after delta of the live `tdata/dumps/completed/` Crashpad database
|
||||
across the run — a delta because `test-run` never clears that directory
|
||||
between runs — and `death_signals` names which of `"breakpad_dump"` /
|
||||
`"crashpad_dump"` / `"exit_code"` fired. A run that writes `TEST_COMPLETE`
|
||||
and then dies on any of those signals reports `died-after-complete`, not
|
||||
`complete`. `stale_crash_cleared` is an ordered list of `{from, kind, to}`
|
||||
entries whose `kind` is `"report"` or `"dump"`, and is `[]` when nothing
|
||||
was cleared. If a stale report cannot be moved, `test-run` refuses before
|
||||
launch, prints the helper error on stderr, exits non-zero, and emits no
|
||||
JSON. If a dump cannot be moved, the run leaves it in place, records its
|
||||
entry with `"to": null` (a null destination), and continues to launch. The
|
||||
performer then judges the evidence itself — the runner gathers, it never
|
||||
assesses. Pre-`TEST_COMPLETE` crash detection keys on process death
|
||||
without `TEST_COMPLETE` plus a fresh `tdata/working`, not exit code;
|
||||
post-`TEST_COMPLETE` death keys on `exit_code`, the `completed/` delta and
|
||||
a fresh top-level `tdata/dumps/*.dmp`, because the clean shutdown unlinks
|
||||
`tdata/working` before the fault.
|
||||
- If the account breaks mid-loop (login screen, `AUTH_KEY_DUPLICATED`), run
|
||||
`test-account-reset --exe EXE` — it deletes only a marked live copy and
|
||||
re-copies golden — then retry once.
|
||||
re-copies golden — then retry once. That deletion takes the Crashpad
|
||||
database under `tdata/dumps/completed/` with it, so copy out any dump
|
||||
worth keeping first.
|
||||
- Enforce the in-app watchdog too. Count test runs independently from
|
||||
implementation attempts and stop at `MAX_TEST_RUNS`.
|
||||
- A repeated setup failure is not a reason to stop below that cap. Apply the
|
||||
|
||||
@@ -56,6 +56,7 @@ OVERLAY_SUBMODULES_DIR = "test-overlay-submodules"
|
||||
TEST_LOG_FILE = "test_log.txt"
|
||||
TEST_COMPLETE_MARKER = "TEST_COMPLETE"
|
||||
STALE_CRASH_DIR = "stale-crash"
|
||||
CRASHPAD_COMPLETED_DIR = "completed"
|
||||
BUILD_LOCK_PROCESS_NAMES = {
|
||||
"cl.exe",
|
||||
"cmake.exe",
|
||||
@@ -1816,6 +1817,8 @@ def command_test_run(args):
|
||||
stderr_path = run_dir / "app_stderr.txt"
|
||||
working = portable / PORTABLE_LIVE / "tdata" / "working"
|
||||
dumps_dir = portable / PORTABLE_LIVE / "tdata" / "dumps"
|
||||
completed_dir = dumps_dir / CRASHPAD_COMPLETED_DIR
|
||||
completed_before = set(completed_dir.glob("*.dmp"))
|
||||
|
||||
launched_at = time.time()
|
||||
with stdout_path.open("wb") as out, stderr_path.open("wb") as err:
|
||||
@@ -1880,15 +1883,27 @@ def command_test_run(args):
|
||||
str(path) for path in dumps_dir.glob("*.dmp")
|
||||
if path.stat().st_mtime >= launched_at
|
||||
) if dumps_dir.is_dir() else []
|
||||
crashpad_dumps_added = sorted(
|
||||
str(path)
|
||||
for path in set(completed_dir.glob("*.dmp")) - completed_before
|
||||
)
|
||||
death_signals = []
|
||||
if dumps:
|
||||
death_signals.append("breakpad_dump")
|
||||
if crashpad_dumps_added:
|
||||
death_signals.append("crashpad_dump")
|
||||
if outcome == "exited" and exit_code:
|
||||
death_signals.append("exit_code")
|
||||
after_complete = "died-after-complete" if death_signals else "complete"
|
||||
if outcome == "exited":
|
||||
if test_complete:
|
||||
verdict_hint = "complete"
|
||||
verdict_hint = after_complete
|
||||
elif crash_report_fresh or dumps:
|
||||
verdict_hint = "crash"
|
||||
else:
|
||||
verdict_hint = "died-without-complete"
|
||||
elif outcome == "killed-after-complete":
|
||||
verdict_hint = "complete"
|
||||
verdict_hint = after_complete
|
||||
else:
|
||||
verdict_hint = "hang"
|
||||
|
||||
@@ -1901,6 +1916,8 @@ def command_test_run(args):
|
||||
else None
|
||||
),
|
||||
"crash_report_fresh": crash_report_fresh,
|
||||
"crashpad_dumps_added": crashpad_dumps_added,
|
||||
"death_signals": death_signals,
|
||||
"dumps": dumps,
|
||||
"duration_seconds": round(ended_at - launched_at, 1),
|
||||
"exe": str(exe),
|
||||
|
||||
@@ -1149,6 +1149,23 @@ def write_complete_markers_exe(path):
|
||||
))
|
||||
|
||||
|
||||
def write_dump_after_complete_exe(path, dump, tail, windows_tail):
|
||||
directory = dump.parent
|
||||
return write_fake_exe(path, (
|
||||
'LOG="$TDESKTOP_TEST_EVIDENCE_DIR/test_log.txt"\n'
|
||||
'echo "TEST_COMPLETE" >> "$LOG"\n'
|
||||
f'mkdir -p "{directory}"\n'
|
||||
f'echo "MDMP fresh minidump" > "{dump}"\n'
|
||||
+ tail
|
||||
), (
|
||||
'set "LOG=%TDESKTOP_TEST_EVIDENCE_DIR%\\test_log.txt"\n'
|
||||
'echo TEST_COMPLETE>>"%LOG%"\n'
|
||||
f'if not exist "{directory}" mkdir "{directory}"\n'
|
||||
f'echo MDMP fresh minidump>"{dump}"\n'
|
||||
+ windows_tail
|
||||
))
|
||||
|
||||
|
||||
def make_portable_root(root):
|
||||
debug = root / "out" / "Debug"
|
||||
golden = debug / workspace.PORTABLE_GOLDEN
|
||||
@@ -1452,6 +1469,115 @@ class MechanicsTest(unittest.TestCase):
|
||||
self.assertEqual(result["markers"]["screenshots"], ["/tmp/fake.png"])
|
||||
self.assertFalse(result["crash_report_fresh"])
|
||||
|
||||
def test_test_run_reports_a_death_after_complete(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
debug = make_portable_root(root)
|
||||
exe = write_fake_exe(debug / "Telegram", (
|
||||
'LOG="$TDESKTOP_TEST_EVIDENCE_DIR/test_log.txt"\n'
|
||||
'echo "TEST_RESULT: PASS: row painted" >> "$LOG"\n'
|
||||
'echo "TEST_COMPLETE" >> "$LOG"\n'
|
||||
"exit 3\n"
|
||||
), (
|
||||
'set "LOG=%TDESKTOP_TEST_EVIDENCE_DIR%\\test_log.txt"\n'
|
||||
'echo TEST_RESULT: PASS: row painted>>"%LOG%"\n'
|
||||
'echo TEST_COMPLETE>>"%LOG%"\n'
|
||||
"exit /b 3\n"
|
||||
))
|
||||
result = run_test_run(exe, root / "run1")
|
||||
self.assertEqual(result["outcome"], "exited")
|
||||
self.assertEqual(result["verdict_hint"], "died-after-complete")
|
||||
self.assertTrue(result["test_complete"])
|
||||
self.assertEqual(result["exit_code"], 3)
|
||||
self.assertEqual(result["death_signals"], ["exit_code"])
|
||||
self.assertEqual(result["crashpad_dumps_added"], [])
|
||||
self.assertFalse(result["crash_report_fresh"])
|
||||
self.assertEqual(result["dumps"], [])
|
||||
|
||||
def test_test_run_reports_both_death_signals_after_complete(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary).resolve()
|
||||
debug = make_portable_root(root)
|
||||
fresh = (
|
||||
debug / workspace.PORTABLE_LIVE / "tdata" / "dumps"
|
||||
/ workspace.CRASHPAD_COMPLETED_DIR / "both.dmp"
|
||||
)
|
||||
exe = write_dump_after_complete_exe(
|
||||
debug / "Telegram", fresh, "exit 11\n", "exit /b 11\n",
|
||||
)
|
||||
result = run_test_run(exe, root / "run1")
|
||||
self.assertEqual(result["verdict_hint"], "died-after-complete")
|
||||
self.assertEqual(result["exit_code"], 11)
|
||||
self.assertEqual(
|
||||
result["death_signals"],
|
||||
["crashpad_dump", "exit_code"],
|
||||
)
|
||||
self.assertEqual(result["crashpad_dumps_added"], [str(fresh)])
|
||||
|
||||
def test_test_run_counts_a_crashpad_dump_written_during_the_run(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary).resolve()
|
||||
debug = make_portable_root(root)
|
||||
fresh = (
|
||||
debug / workspace.PORTABLE_LIVE / "tdata" / "dumps"
|
||||
/ workspace.CRASHPAD_COMPLETED_DIR / "fresh.dmp"
|
||||
)
|
||||
exe = write_dump_after_complete_exe(
|
||||
debug / "Telegram", fresh, "exit 0\n", "exit /b 0\n",
|
||||
)
|
||||
result = run_test_run(exe, root / "run1")
|
||||
self.assertEqual(result["outcome"], "exited")
|
||||
self.assertEqual(result["exit_code"], 0)
|
||||
self.assertTrue(result["test_complete"])
|
||||
self.assertEqual(result["verdict_hint"], "died-after-complete")
|
||||
self.assertEqual(result["death_signals"], ["crashpad_dump"])
|
||||
self.assertEqual(result["crashpad_dumps_added"], [str(fresh)])
|
||||
self.assertEqual(result["dumps"], [])
|
||||
|
||||
def test_test_run_counts_a_breakpad_dump_written_during_the_run(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary).resolve()
|
||||
debug = make_portable_root(root)
|
||||
fresh = (
|
||||
debug / workspace.PORTABLE_LIVE / "tdata" / "dumps"
|
||||
/ "breakpad.dmp"
|
||||
)
|
||||
exe = write_dump_after_complete_exe(
|
||||
debug / "Telegram", fresh, "exit 0\n", "exit /b 0\n",
|
||||
)
|
||||
result = run_test_run(exe, root / "run1")
|
||||
self.assertEqual(result["outcome"], "exited")
|
||||
self.assertEqual(result["exit_code"], 0)
|
||||
self.assertTrue(result["test_complete"])
|
||||
self.assertEqual(result["verdict_hint"], "died-after-complete")
|
||||
self.assertEqual(result["death_signals"], ["breakpad_dump"])
|
||||
self.assertEqual(result["dumps"], [str(fresh)])
|
||||
self.assertEqual(result["crashpad_dumps_added"], [])
|
||||
|
||||
def test_test_run_ignores_a_crashpad_dump_from_before_the_run(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary).resolve()
|
||||
debug = make_portable_root(root)
|
||||
self.assertEqual(workspace.setup_test_account(debug), "fresh-copy")
|
||||
completed = (
|
||||
debug / workspace.PORTABLE_LIVE / "tdata" / "dumps"
|
||||
/ workspace.CRASHPAD_COMPLETED_DIR
|
||||
)
|
||||
completed.mkdir(parents=True)
|
||||
old = completed / "old.dmp"
|
||||
old.write_bytes(b"MDMP old minidump\n")
|
||||
exe = write_complete_markers_exe(debug / "Telegram")
|
||||
run_dir = root / "run1"
|
||||
result = run_test_run(exe, run_dir)
|
||||
self.assertEqual(result["account"], "reused-marked-live")
|
||||
self.assertEqual(result["verdict_hint"], "complete")
|
||||
self.assertEqual(result["exit_code"], 0)
|
||||
self.assertEqual(result["death_signals"], [])
|
||||
self.assertEqual(result["crashpad_dumps_added"], [])
|
||||
self.assertEqual(result["stale_crash_cleared"], [])
|
||||
self.assertEqual(old.read_bytes(), b"MDMP old minidump\n")
|
||||
self.assertFalse((run_dir / workspace.STALE_CRASH_DIR).exists())
|
||||
|
||||
def test_test_run_reports_crash_diagnostics(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
@@ -1543,6 +1669,45 @@ class MechanicsTest(unittest.TestCase):
|
||||
self.assertEqual(result["verdict_hint"], "hang")
|
||||
self.assertFalse(result["test_complete"])
|
||||
|
||||
def test_test_run_keeps_a_grace_kill_complete(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
debug = make_portable_root(root)
|
||||
exe = write_fake_exe(debug / "Telegram", (
|
||||
'LOG="$TDESKTOP_TEST_EVIDENCE_DIR/test_log.txt"\n'
|
||||
'echo "TEST_COMPLETE" >> "$LOG"\n'
|
||||
"sleep 30\n"
|
||||
), (
|
||||
'set "LOG=%TDESKTOP_TEST_EVIDENCE_DIR%\\test_log.txt"\n'
|
||||
'echo TEST_COMPLETE>>"%LOG%"\n'
|
||||
":loop\ngoto loop\n"
|
||||
))
|
||||
result = run_test_run(exe, root / "run1", grace=1.0)
|
||||
self.assertEqual(result["outcome"], "killed-after-complete")
|
||||
self.assertEqual(result["verdict_hint"], "complete")
|
||||
self.assertTrue(result["test_complete"])
|
||||
self.assertIsNone(result["exit_code"])
|
||||
self.assertEqual(result["death_signals"], [])
|
||||
|
||||
def test_test_run_reports_a_grace_kill_with_a_dump(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary).resolve()
|
||||
debug = make_portable_root(root)
|
||||
dump = (
|
||||
debug / workspace.PORTABLE_LIVE / "tdata" / "dumps"
|
||||
/ workspace.CRASHPAD_COMPLETED_DIR / "grace.dmp"
|
||||
)
|
||||
exe = write_dump_after_complete_exe(
|
||||
debug / "Telegram", dump, "sleep 30\n", ":loop\ngoto loop\n",
|
||||
)
|
||||
result = run_test_run(exe, root / "run1", grace=1.0)
|
||||
self.assertEqual(result["outcome"], "killed-after-complete")
|
||||
self.assertEqual(result["verdict_hint"], "died-after-complete")
|
||||
self.assertTrue(result["test_complete"])
|
||||
self.assertIsNone(result["exit_code"])
|
||||
self.assertEqual(result["death_signals"], ["crashpad_dump"])
|
||||
self.assertEqual(result["crashpad_dumps_added"], [str(dump)])
|
||||
|
||||
def test_test_run_clears_and_preserves_stale_crash_state(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary).resolve()
|
||||
|
||||
Reference in New Issue
Block a user