Skip to content

Commit 8bbcd85

Browse files
authored
Fix: Don't show stacktraces when tests fail (#4485)
1 parent 3ab48e5 commit 8bbcd85

File tree

4 files changed

+96
-23
lines changed

4 files changed

+96
-23
lines changed

sqlmesh/core/test/definition.py

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -349,18 +349,21 @@ def create_test(
349349
else:
350350
_raise_error(f"Model '{name}' is an unsupported model type for testing", path)
351351

352-
return test_type(
353-
body,
354-
test_name,
355-
t.cast(Model, model),
356-
models,
357-
engine_adapter,
358-
dialect,
359-
path,
360-
preserve_fixtures,
361-
default_catalog,
362-
concurrency,
363-
)
352+
try:
353+
return test_type(
354+
body,
355+
test_name,
356+
t.cast(Model, model),
357+
models,
358+
engine_adapter,
359+
dialect,
360+
path,
361+
preserve_fixtures,
362+
default_catalog,
363+
concurrency,
364+
)
365+
except Exception as e:
366+
raise TestError(f"Failed to create test {test_name} ({path})\n{str(e)}")
364367

365368
def __str__(self) -> str:
366369
return f"{self.test_name} ({self.path})"
@@ -905,8 +908,8 @@ def _row_difference(left: pd.DataFrame, right: pd.DataFrame) -> pd.DataFrame:
905908

906909
def _raise_error(msg: str, path: Path | None = None) -> None:
907910
if path:
908-
raise TestError(f"{msg} at {path}")
909-
raise TestError(msg)
911+
raise TestError(f"Failed to run test at {path}:\n{msg}")
912+
raise TestError(f"Failed to run test:\n{msg}")
910913

911914

912915
def _normalize_df_value(value: t.Any) -> t.Any:

sqlmesh/core/test/result.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ class ModelTextTestResult(unittest.TextTestResult):
1717
def __init__(self, *args: t.Any, **kwargs: t.Any):
1818
super().__init__(*args, **kwargs)
1919
self.successes = []
20+
self.original_failures: t.List[t.Tuple[unittest.TestCase, ErrorType]] = []
21+
self.original_errors: t.List[t.Tuple[unittest.TestCase, ErrorType]] = []
2022

2123
def addSubTest(
2224
self,
@@ -48,8 +50,9 @@ def addFailure(self, test: unittest.TestCase, err: ErrorType) -> None:
4850
test: The test case.
4951
err: A tuple of the form returned by sys.exc_info(), i.e., (type, value, traceback).
5052
"""
51-
exctype, value, tb = err
52-
self.original_err = (test, err)
53+
exctype, value, _ = err
54+
self.original_failures.append((test, err))
55+
# Intentionally ignore the traceback to hide it from the user
5356
return super().addFailure(test, (exctype, value, None)) # type: ignore
5457

5558
def addError(self, test: unittest.TestCase, err: ErrorType) -> None:
@@ -59,8 +62,10 @@ def addError(self, test: unittest.TestCase, err: ErrorType) -> None:
5962
test: The test case.
6063
err: A tuple of the form returned by sys.exc_info(), i.e., (type, value, traceback).
6164
"""
62-
self.original_err = (test, err)
63-
return super().addError(test, err) # type: ignore
65+
exctype, value, _ = err
66+
self.original_errors.append((test, err))
67+
# Intentionally ignore the traceback to hide it from the user
68+
return super().addError(test, (exctype, value, None)) # type: ignore
6469

6570
def addSuccess(self, test: unittest.TestCase) -> None:
6671
"""Called when the test case test succeeds.
@@ -105,9 +110,10 @@ def log_test_report(self, test_duration: float) -> None:
105110
stream.writeln(unittest.TextTestResult.separator2)
106111
stream.writeln(failure)
107112

108-
for _, error in errors:
113+
for test_case, error in errors:
109114
stream.writeln(unittest.TextTestResult.separator1)
110-
stream.writeln(f"ERROR: {error}")
115+
stream.writeln(f"ERROR: {test_case}")
116+
stream.writeln(error)
111117

112118
# Output final report
113119
stream.writeln(unittest.TextTestResult.separator2)

sqlmesh/core/test/runner.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,11 @@ def _run_single_test(
150150
if result.successes:
151151
combined_results.addSuccess(result.successes[0])
152152
elif result.errors:
153-
combined_results.addError(result.original_err[0], result.original_err[1])
153+
for error_test, error in result.original_errors:
154+
combined_results.addError(error_test, error)
154155
elif result.failures:
155-
combined_results.addFailure(result.original_err[0], result.original_err[1])
156+
for failure_test, failure in result.original_failures:
157+
combined_results.addFailure(failure_test, failure)
156158
elif result.skipped:
157159
skipped_args = result.skipped[0]
158160
combined_results.addSkip(skipped_args[0], skipped_args[1])

tests/core/test_test.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
import datetime
44
import typing as t
5+
import io
56
from pathlib import Path
7+
import unittest
68
from unittest.mock import call, patch
79
from shutil import copyfile
810

@@ -28,6 +30,7 @@
2830
from sqlmesh.core.macros import MacroEvaluator, macro
2931
from sqlmesh.core.model import Model, SqlModel, load_sql_based_model, model
3032
from sqlmesh.core.test.definition import ModelTest, PythonModelTest, SqlModelTest
33+
from sqlmesh.core.test.result import ModelTextTestResult
3134
from sqlmesh.utils.errors import ConfigError, SQLMeshError, TestError
3235
from sqlmesh.utils.yaml import dump as dump_yaml
3336
from sqlmesh.utils.yaml import load as load_yaml
@@ -1039,7 +1042,8 @@ def test_unknown_column_error() -> None:
10391042
context=Context(config=Config(model_defaults=ModelDefaultsConfig(dialect="duckdb"))),
10401043
).run(),
10411044
expected_msg=(
1042-
"sqlmesh.utils.errors.TestError: Detected unknown column(s)\n\n"
1045+
"sqlmesh.utils.errors.TestError: Failed to run test:\n"
1046+
"Detected unknown column(s)\n\n"
10431047
"Expected column(s): id, value\n"
10441048
"Unknown column(s): foo\n"
10451049
),
@@ -2520,3 +2524,61 @@ def upstream_table_python(context, **kwargs):
25202524
context=sushi_context,
25212525
).run()
25222526
)
2527+
2528+
2529+
@pytest.mark.parametrize("is_error", [True, False])
2530+
def test_model_test_text_result_reporting_no_traceback(
2531+
sushi_context: Context, full_model_with_two_ctes: SqlModel, is_error: bool
2532+
) -> None:
2533+
test = _create_test(
2534+
body=load_yaml(
2535+
"""
2536+
test_foo:
2537+
model: sushi.foo
2538+
inputs:
2539+
raw:
2540+
- id: 1
2541+
outputs:
2542+
ctes:
2543+
source:
2544+
- id: 1
2545+
renamed:
2546+
- fid: 1
2547+
vars:
2548+
start: 2022-01-01
2549+
end: 2022-01-01
2550+
"""
2551+
),
2552+
test_name="test_foo",
2553+
model=sushi_context.upsert_model(full_model_with_two_ctes),
2554+
context=sushi_context,
2555+
)
2556+
stream = io.StringIO()
2557+
result = ModelTextTestResult(
2558+
stream=unittest.runner._WritelnDecorator(stream), # type: ignore
2559+
verbosity=1,
2560+
descriptions=True,
2561+
)
2562+
2563+
try:
2564+
raise Exception("failure")
2565+
except Exception as e:
2566+
assert e.__traceback__ is not None
2567+
if is_error:
2568+
result.addError(test, (e.__class__, e, e.__traceback__))
2569+
else:
2570+
result.addFailure(test, (e.__class__, e, e.__traceback__))
2571+
2572+
result.log_test_report(0)
2573+
2574+
stream.seek(0)
2575+
output = stream.read()
2576+
2577+
# Make sure that the traceback is not printed
2578+
assert "Traceback" not in output
2579+
assert "File" not in output
2580+
assert "line" not in output
2581+
2582+
prefix = "ERROR" if is_error else "FAIL"
2583+
assert f"{prefix}: test_foo (None)" in output
2584+
assert "Exception: failure" in output

0 commit comments

Comments
 (0)