Persist the pending ConPTY lifetime, process snapshot and native font ownership fixes. Keep lifecycle regression tests and the Windows memory stress tool together with the ownership contracts.
129 lines
5.7 KiB
Python
129 lines
5.7 KiB
Python
import unittest
|
|
from unittest.mock import Mock
|
|
import subprocess
|
|
|
|
from scripts.windows_memory_stress import MIB, close_owned_process, summarize
|
|
|
|
|
|
def sample(phase, commit, resident=40):
|
|
return {"phase": phase, "private_commit_bytes": commit * MIB,
|
|
"private_working_set_bytes": resident * MIB, "working_set_bytes": (resident + 50) * MIB}
|
|
|
|
|
|
def complete_samples():
|
|
return [sample("idle", 100), sample("settled-0", 130), sample("settled-1", 131),
|
|
sample("tabs-open-0", 170), sample("tabs-closed-0", 132),
|
|
sample("tabs-open-1", 171), sample("tabs-closed-1", 133)]
|
|
|
|
|
|
class WindowsMemoryStressTests(unittest.TestCase):
|
|
def checkpoints(self):
|
|
return [{"phase": row["phase"], "handles": 700, "threads": 120}
|
|
for row in complete_samples() if not row["phase"].startswith("tabs-open")]
|
|
|
|
def test_warm_cache_and_reclaimed_tabs_fit_both_budgets(self):
|
|
result = summarize(complete_samples(), 2, 180, 4)
|
|
self.assertTrue(result["budget_enforced"])
|
|
self.assertEqual(result["violations"], [])
|
|
self.assertEqual(result["tab_growth_after_warmup_bytes"], MIB)
|
|
|
|
def test_falling_working_set_does_not_hide_private_commit_growth(self):
|
|
samples = complete_samples()
|
|
samples[-1] = sample("tabs-closed-1", 155, resident=10)
|
|
result = summarize(samples, 2, 180, 4)
|
|
self.assertEqual(len(result["violations"]), 1)
|
|
self.assertIn("retained", result["violations"][0])
|
|
|
|
def test_transient_peak_is_checked_even_when_all_tabs_release(self):
|
|
samples = complete_samples() + [sample("tabs-open-1", 300)]
|
|
result = summarize(samples, 2, 180, 4)
|
|
self.assertEqual(result["sampled_peak_private_commit_bytes"], 300 * MIB)
|
|
self.assertEqual(len(result["violations"]), 1)
|
|
self.assertIn("peak", result["violations"][0])
|
|
|
|
def test_late_release_does_not_hide_earlier_retained_growth(self):
|
|
samples = complete_samples()
|
|
samples[-1] = sample("tabs-closed-1", 155)
|
|
samples += [sample("settled-2", 130), sample("tabs-closed-2", 132)]
|
|
result = summarize(samples, 3, 180, 4)
|
|
self.assertGreater(result["tab_growth_after_warmup_bytes"], 4 * MIB)
|
|
self.assertEqual(len(result["violations"]), 1)
|
|
|
|
def test_incomplete_run_cannot_report_a_budget_pass(self):
|
|
with self.assertRaisesRegex(ValueError, "tabs-closed-1"):
|
|
summarize(complete_samples()[:-1], 2, 180, 4)
|
|
with self.assertRaises(ValueError):
|
|
summarize([], 2, 180, 4)
|
|
|
|
def test_observation_without_limits_does_not_claim_a_budget(self):
|
|
result = summarize(complete_samples(), 2, None, None)
|
|
self.assertFalse(result["budget_enforced"])
|
|
self.assertIsNone(result["peak_budget_mib"])
|
|
self.assertIsNone(result["growth_budget_mib"])
|
|
|
|
def test_resource_leak_fails_even_when_memory_is_below_budget(self):
|
|
checkpoints = self.checkpoints()
|
|
checkpoints[-1].update(handles=736, threads=128)
|
|
result = summarize(complete_samples(), 2, 180, 4, checkpoints=checkpoints,
|
|
handle_growth_limit=8, thread_growth_limit=4)
|
|
self.assertEqual(result["resource_growth_after_warmup"], {"handles": 36, "threads": 8})
|
|
self.assertEqual(len(result["violations"]), 2)
|
|
|
|
def test_missing_resource_checkpoints_cannot_pass_resource_budgets(self):
|
|
with self.assertRaisesRegex(ValueError, "checkpoints"):
|
|
summarize(complete_samples(), 2, 180, 4, handle_growth_limit=8)
|
|
with self.assertRaisesRegex(ValueError, "tabs-closed-1"):
|
|
summarize(complete_samples(), 2, 180, 4, checkpoints=self.checkpoints()[:-1],
|
|
thread_growth_limit=4)
|
|
|
|
def test_invalid_budgets_cannot_disable_the_gate(self):
|
|
for limit in (float("nan"), float("inf"), -1):
|
|
with self.subTest(limit=limit), self.assertRaises(ValueError):
|
|
summarize(complete_samples(), 2, limit, 4)
|
|
|
|
def test_normal_window_close_does_not_force_terminate(self):
|
|
process = Mock(pid=123)
|
|
process.poll.return_value = None
|
|
process.wait.side_effect = lambda **_: setattr(process.poll, "return_value", 0)
|
|
native = Mock()
|
|
result = close_owned_process(process, native)
|
|
self.assertTrue(result["normal_exit"])
|
|
self.assertFalse(result["forced_termination"])
|
|
process.terminate.assert_not_called()
|
|
|
|
def test_sampler_initialization_failure_still_reclaims_its_process(self):
|
|
process = Mock(pid=123)
|
|
process.poll.return_value = None
|
|
process.wait.side_effect = lambda **_: setattr(process.poll, "return_value", 1)
|
|
result = close_owned_process(process, None)
|
|
self.assertFalse(result["normal_exit"])
|
|
self.assertTrue(result["forced_termination"])
|
|
process.terminate.assert_called_once()
|
|
|
|
def test_a_crash_cannot_count_as_normal_cleanup(self):
|
|
process = Mock(pid=123)
|
|
process.poll.return_value = -1073741819
|
|
result = close_owned_process(process, Mock())
|
|
self.assertFalse(result["normal_exit"])
|
|
self.assertFalse(result["forced_termination"])
|
|
process.terminate.assert_not_called()
|
|
|
|
def test_hung_close_is_a_failure_even_if_forced_cleanup_succeeds(self):
|
|
process = Mock(pid=123)
|
|
process.poll.return_value = None
|
|
|
|
def wait(**_):
|
|
if not process.terminate.called:
|
|
raise subprocess.TimeoutExpired("owned test process", 15)
|
|
process.poll.return_value = 1
|
|
|
|
process.wait.side_effect = wait
|
|
result = close_owned_process(process, Mock())
|
|
self.assertFalse(result["normal_exit"])
|
|
self.assertTrue(result["forced_termination"])
|
|
self.assertTrue(result["errors"])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|