|
5 | 5 |
|
6 | 6 | from __future__ import annotations |
7 | 7 |
|
| 8 | + |
| 9 | +import gc |
8 | 10 | import glob |
9 | 11 | import os |
10 | 12 | import os.path |
|
24 | 26 | from coverage.files import PathAliases, canonical_filename |
25 | 27 | from coverage.types import FilePathClasses, FilePathType, TArc, TLineNo |
26 | 28 |
|
| 29 | +from tests import osinfo |
27 | 30 | from tests.coveragetest import CoverageTest |
28 | 31 | from tests.helpers import DebugControlString, assert_count_equal |
29 | 32 |
|
@@ -745,7 +748,7 @@ def test_debug_output_with_debug_option(self) -> None: |
745 | 748 |
|
746 | 749 | assert re.search( |
747 | 750 | r"^" |
748 | | - + r"Closing dbs, force=False: {}\n" |
| 751 | + + r"Closing dbs, force=True: {}\n" |
749 | 752 | + r"Erasing data file '.*\.coverage' \(does not exist\)\n" |
750 | 753 | + r"Opening data file '.*\.coverage' \(does not exist\)\n" |
751 | 754 | + r"Initing data file '.*\.coverage' \(0 bytes, modified [-:. 0-9]+\)\n" |
@@ -1093,3 +1096,34 @@ def test_updating(self) -> None: |
1093 | 1096 | b = CoverageData(no_disk=True) |
1094 | 1097 | b.update(a) |
1095 | 1098 | assert b.measured_files() == {"foo.py"} |
| 1099 | + |
| 1100 | + @pytest.mark.flaky |
| 1101 | + def test_erase_doesnt_leak_memory(self) -> None: |
| 1102 | + # https://github.com/coveragepy/coveragepy/issues/2138 |
| 1103 | + # Repeated erase cycles on no_disk data should not leak memory |
| 1104 | + # from unclosed in-memory database connections. |
| 1105 | + |
| 1106 | + # Keep references to erased data objects, simulating how |
| 1107 | + # Coverage._data_to_close accumulates them. |
| 1108 | + old_data = [] |
| 1109 | + |
| 1110 | + # Warmup |
| 1111 | + covdata = CoverageData(no_disk=True) |
| 1112 | + covdata.add_lines({"foo.py": list(range(1, 1000))}) |
| 1113 | + covdata.erase() |
| 1114 | + old_data.append(covdata) |
| 1115 | + |
| 1116 | + gc.collect() |
| 1117 | + baseline = osinfo.process_ram() |
| 1118 | + |
| 1119 | + for _ in range(200): |
| 1120 | + covdata = CoverageData(no_disk=True) |
| 1121 | + covdata.add_lines({"foo.py": list(range(1, 1000))}) |
| 1122 | + covdata.erase() |
| 1123 | + old_data.append(covdata) |
| 1124 | + |
| 1125 | + gc.collect() |
| 1126 | + growth = osinfo.process_ram() - baseline |
| 1127 | + # Before fixing 2138, 200 cycles leaked ~44MB from unclosed in-memory |
| 1128 | + # SQLite databases. With the fix, growth is well under 1MB. |
| 1129 | + assert growth < 4_000_000, f"Memory grew by {growth:,} bytes over 200 erase cycles" |
0 commit comments