Skip to content

Commit 475a096

Browse files
committed
Switch from expecttest/assertExpectedInline to assertExpectedJournal
Implements our own assertExpectedJournal that writes expected results to a separate file rather than inline. This: 1) Make test files easier to read/edit (especially for AI coding tools) 2) Fixes a race in expecttest where multiple edits to the same file errored stack-info: PR: #241, branch: jansel/stack/80
1 parent 2d0cd8c commit 475a096

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+11795
-7462
lines changed

.pre-commit-config.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ repos:
55
- id: check-symlinks
66
- id: destroyed-symlinks
77
- id: trailing-whitespace
8+
exclude: '\.expected$'
89
- id: end-of-file-fixer
10+
exclude: '\.expected$'
911
- id: check-yaml
1012
- id: check-toml
1113
- id: check-ast

helion/_testing.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
from __future__ import annotations
22

3+
import collections
34
import importlib
5+
import inspect
6+
import os
47
from pathlib import Path
8+
import re
59
import sys
610
from typing import TYPE_CHECKING
711
from typing import Callable
12+
import unittest
813

914
import torch
1015
from triton.testing import do_bench
@@ -139,3 +144,139 @@ def check_example(
139144
)
140145
skip_accuracy or torch.testing.assert_close(result, expected, atol=1e-1, rtol=1e-2)
141146
return code
147+
148+
149+
class AssertExpectedJournal:
150+
"""
151+
Manages a <testfile>.expected file that contains expected output for TestCase.assertExpectedJournal() calls.
152+
153+
This replaces the previous `expecttest` assertExpectedInline approach by storing expected output
154+
in external .expected files rather than inline strings in test files. This provides better
155+
organization and avoids cluttering test files with large code blocks.
156+
157+
The .expected file format uses sections like:
158+
--- assertExpectedJournal(TestClass.test_method)
159+
expected output here
160+
161+
--- assertExpectedJournal(TestClass.test_method)
162+
second expected output for same test
163+
164+
Environment variable EXPECTTEST_ACCEPT=1 can be used to update expected outputs.
165+
"""
166+
167+
def __init__(self, cls: type[TestCase]) -> None:
168+
pyfile = os.path.abspath(inspect.getfile(cls))
169+
assert "/test/" in pyfile
170+
assert pyfile.endswith(".py")
171+
self.filename: Path = Path(pyfile[:-3] + ".expected")
172+
self._cache: dict[str, list[str]] | None = None
173+
self.current_id: str | None = None
174+
self.current_index: int = 0
175+
176+
@property
177+
def cache(self) -> dict[str, list[str]]:
178+
if self._cache is None:
179+
return self.reload()
180+
return self._cache
181+
182+
def reload(self) -> dict[str, list[str]]:
183+
if self.filename.exists():
184+
data = self.filename.read_text()
185+
else:
186+
data = ""
187+
result = collections.defaultdict(list)
188+
for name, expected in re.findall(
189+
r"--- assertExpectedJournal\(([^)]*)\)\n(.*?)(?=^--- assertExpectedJournal\(|\Z)",
190+
data,
191+
re.MULTILINE | re.DOTALL,
192+
):
193+
result[name].append(expected.strip())
194+
self._cache = result
195+
return result
196+
197+
def save(self) -> None:
198+
tmp = f"{self.filename}.tmp{os.getpid()}"
199+
with open(tmp, "w") as f:
200+
for name, expected_values in self.cache.items():
201+
f.writelines(
202+
f"--- assertExpectedJournal({name})\n{expected}\n\n"
203+
for expected in expected_values
204+
)
205+
os.rename(tmp, self.filename)
206+
207+
@staticmethod
208+
def normalize_id(test_id: str) -> str:
209+
match = re.search(r"\b([^.]+\.[^.]+)$", test_id)
210+
assert match, f"Test ID '{test_id}' does not match expected format"
211+
return match.group(1)
212+
213+
def lookup(self, test_id: str, value: str) -> tuple[str, str]:
214+
test_id = self.normalize_id(test_id)
215+
if self.current_id != test_id:
216+
self.current_id = test_id
217+
self.current_index = 0
218+
219+
expected_values = self.cache[test_id]
220+
if self.current_index < len(expected_values):
221+
expected = expected_values[self.current_index]
222+
else:
223+
assert self.current_index == len(expected_values)
224+
expected_values.append("")
225+
expected = ""
226+
227+
value = value.strip()
228+
if value != expected and os.environ.get("EXPECTTEST_ACCEPT", "0") not in {
229+
"0",
230+
"false",
231+
"False",
232+
"",
233+
}:
234+
expected_values[self.current_index] = value
235+
# Reload to play nicer with other processes
236+
self.reload()[test_id] = expected_values
237+
self.save()
238+
expected = value
239+
print(
240+
f"Expected output for {test_id} updated: {len(expected)} => {len(value)} bytes",
241+
file=sys.stderr,
242+
)
243+
self.current_index += 1
244+
return value, expected
245+
246+
247+
class TestCase(unittest.TestCase):
248+
maxDiff = 16384
249+
250+
@classmethod
251+
def setUpClass(cls) -> None:
252+
cls._expected_journal = AssertExpectedJournal(cls)
253+
super().setUpClass()
254+
255+
@classmethod
256+
def tearDownClass(cls) -> None:
257+
super().tearDownClass()
258+
del cls._expected_journal
259+
260+
def assertExpectedJournal(self, value: str) -> None:
261+
"""
262+
Assert that the given value matches the expected output stored in <testfile>.expected.
263+
264+
This method replaces assertExpectedInline for code generation tests. Instead of storing
265+
expected output as inline strings in test files, it uses external .expected files for
266+
better organization.
267+
268+
Args:
269+
value: The actual output to compare (usually generated Triton code)
270+
271+
Raises:
272+
AssertionError: If value doesn't match expected output
273+
274+
Note:
275+
Use EXPECTTEST_ACCEPT=1 environment variable to update expected outputs.
276+
"""
277+
value, expected = self._expected_journal.lookup(self.id(), value)
278+
self.assertMultiLineEqual(
279+
value,
280+
expected,
281+
msg="To accept the new output, re-run test with env EXPECTTEST_ACCEPT=1",
282+
)

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ dependencies = [
2424

2525
[project.optional-dependencies]
2626
dev = [
27-
"expecttest",
2827
"pytest",
2928
"pre-commit"
3029
]

requirements.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
expecttest
21
pytest
32
typing_extensions
43
pre-commit

0 commit comments

Comments
 (0)