Skip to content

Fix: Unit test CTE failures not being captured #5081

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 1, 2025

Conversation

VaggelisD
Copy link
Contributor

Fixes #5067

In unittest internals TestCase::addSubTest appends the result of that subtest directly to self.errors or self.failures:

    def addSubTest(self, test, subtest, err):
        """Called at the end of a subtest.
        'err' is None if the subtest ended successfully, otherwise it's a
        tuple of values as returned by sys.exc_info().
        """
        # By default, we don't do anything with successful subtests, but
        # more sophisticated test results might want to record them.
        if err is not None:
            if getattr(self, 'failfast', False):
                self.stop()
            if issubclass(err[0], test.failureException):
                errors = self.failures
            else:
                errors = self.errors
            errors.append((subtest, self._exc_info_to_string(err, test)))
            self._mirrorOutput = True

However, our custom classes expected that this'd happen through addError(...) or addFailure(...) so that we can intercept these exceptions (since they get turned into strings before being stored in their parent classes).

To solve this regression, ModelTestTextResult::addSubTest now follows the parent implementation and instead calls the respective APIs which do fill our own original_failures & original_error structures.

The output for that looks like:

❯ sqlmesh test
F
======================================================================

----------------------------------------------------------------------
FAIL: test_example_full_model (/Users/vaggelisd/Desktop/tobiko/test_dir/tests/test.yaml)
----------------------------------------------------------------------
         Data mismatch (CTE "filtered_orders_cte")          
┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┓
┃   Row    ┃      id: Expected       ┃     id: Actual      ┃
┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━┩
│    0     │            5            │          1          │
└──────────┴─────────────────────────┴─────────────────────┘

----------------------------------------------------------------------
Test Failure Summary
======================================================================
Ran 1 tests against duckdb in 0.09 seconds. 

Failed tests (1):
 •<path>::test_example_full_model
======================================================================

Docs

unittest.TestResult | unittest.TestCase | unittest.runner

Comment on lines +321 to +325
failed_subtest = ""

if subtest := getattr(self, "_subtest", None):
if cte := subtest.params.get("cte"):
failed_subtest = f" (CTE {cte})"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to differentiate the CTE mismatch from the output one, e.g if both subtests fail in a single test that'd be the output:

FF
======================================================================

----------------------------------------------------------------------
FAIL: test_example_full_model (/Users/vaggelisd/Desktop/tobiko/test_dir/tests/test.yaml)
----------------------------------------------------------------------
         Data mismatch (CTE "filtered_orders_cte")          
┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┓
┃   Row    ┃      id: Expected       ┃     id: Actual      ┃
┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━┩
│    0     │            5            │          1          │
└──────────┴─────────────────────────┴─────────────────────┘

----------------------------------------------------------------------
FAIL: test_example_full_model (/Users/vaggelisd/Desktop/tobiko/test_dir/tests/test.yaml)
----------------------------------------------------------------------
                       Data mismatch                        
┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┓
┃   Row    ┃      id: Expected       ┃     id: Actual      ┃
┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━┩
│    0     │            2            │          1          │
└──────────┴─────────────────────────┴─────────────────────┘

----------------------------------------------------------------------
Test Failure Summary
======================================================================
Ran 1 tests against duckdb in 0.14 seconds. 

Failed tests (1):
 • /Users/vaggelisd/Desktop/tobiko/test_dir/tests/test.yaml::test_example_full_model
======================================================================

Comment on lines +127 to +135
def get_fail_and_error_tests(self) -> t.List[ModelTest]:
# If tests contain failed subtests (e.g testing CTE outputs) we don't want
# to report it as different test failures
test_name_to_test = {
test.test_name: test
for test, _ in self.failures + self.errors
if isinstance(test, ModelTest)
}
return list(test_name_to_test.values())
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simple abstraction to simplify console.py

Comment on lines +2418 to +2419
copy_test_file(original_test_file, tmp_path / "tests" / f"test_success_{i}.yaml", i)
copy_test_file(new_test_file, tmp_path / "tests" / f"test_failure_{i}.yaml", i)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously we copied these files directly but unit tests don't work well if the name is duplicated, I'll follow up with a UX fix

Comment on lines -2729 to -2739
errors = result.errors
failures = result.failures
skipped = result.skipped

infos = []
if failures:
infos.append(f"failures={len(failures)}")
if errors:
infos.append(f"errors={len(errors)}")
if skipped:
infos.append(f"skipped={skipped}")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dead code, infos is no longer used

@VaggelisD VaggelisD force-pushed the vaggelisd/fix_unit_test_cte branch from a726d55 to 681d328 Compare July 31, 2025 13:51

failed_subtest = ""

if subtest := getattr(self, "_subtest", None):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it even makes sense at this point to rely on subtests rather than write our own context manager which sets the current CTE and then resets it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any other benefits that subtests offer us here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had the same thought as I was working on this, I think it's merit is that it triggers the callback addSubTest when it finishes, otherwise we'd have to schedule that ourselves.

Not really a strong argument for it, but I'm not sure if there's a strong argument against it either. Happy to reconsider if you had a better thought with the custom CM

@VaggelisD VaggelisD merged commit a2f8a05 into main Aug 1, 2025
27 checks passed
@VaggelisD VaggelisD deleted the vaggelisd/fix_unit_test_cte branch August 1, 2025 12:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Cte Tests Not Working
2 participants