sqlmesh 0.193.1.dev3__py3-none-any.whl → 0.193.1.dev5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sqlmesh/_version.py +2 -2
- sqlmesh/core/console.py +94 -31
- sqlmesh/core/context.py +13 -15
- sqlmesh/core/test/definition.py +91 -5
- sqlmesh/core/test/result.py +42 -47
- sqlmesh/core/test/runner.py +8 -17
- sqlmesh/integrations/github/cicd/command.py +2 -3
- sqlmesh/integrations/github/cicd/controller.py +10 -9
- sqlmesh/magics.py +3 -0
- sqlmesh/utils/rich.py +56 -0
- {sqlmesh-0.193.1.dev3.dist-info → sqlmesh-0.193.1.dev5.dist-info}/METADATA +1 -1
- {sqlmesh-0.193.1.dev3.dist-info → sqlmesh-0.193.1.dev5.dist-info}/RECORD +18 -18
- web/server/api/endpoints/commands.py +1 -8
- web/server/console.py +4 -4
- {sqlmesh-0.193.1.dev3.dist-info → sqlmesh-0.193.1.dev5.dist-info}/WHEEL +0 -0
- {sqlmesh-0.193.1.dev3.dist-info → sqlmesh-0.193.1.dev5.dist-info}/entry_points.txt +0 -0
- {sqlmesh-0.193.1.dev3.dist-info → sqlmesh-0.193.1.dev5.dist-info}/licenses/LICENSE +0 -0
- {sqlmesh-0.193.1.dev3.dist-info → sqlmesh-0.193.1.dev5.dist-info}/top_level.txt +0 -0
sqlmesh/_version.py
CHANGED
|
@@ -17,5 +17,5 @@ __version__: str
|
|
|
17
17
|
__version_tuple__: VERSION_TUPLE
|
|
18
18
|
version_tuple: VERSION_TUPLE
|
|
19
19
|
|
|
20
|
-
__version__ = version = '0.193.1.
|
|
21
|
-
__version_tuple__ = version_tuple = (0, 193, 1, '
|
|
20
|
+
__version__ = version = '0.193.1.dev5'
|
|
21
|
+
__version_tuple__ = version_tuple = (0, 193, 1, 'dev5')
|
sqlmesh/core/console.py
CHANGED
|
@@ -7,6 +7,7 @@ import unittest
|
|
|
7
7
|
import uuid
|
|
8
8
|
import logging
|
|
9
9
|
import textwrap
|
|
10
|
+
from itertools import zip_longest
|
|
10
11
|
from pathlib import Path
|
|
11
12
|
from hyperscript import h
|
|
12
13
|
from rich.console import Console as RichConsole
|
|
@@ -26,6 +27,7 @@ from rich.table import Table
|
|
|
26
27
|
from rich.tree import Tree
|
|
27
28
|
from sqlglot import exp
|
|
28
29
|
|
|
30
|
+
from sqlmesh.core.test.result import ModelTextTestResult
|
|
29
31
|
from sqlmesh.core.environment import EnvironmentNamingInfo, EnvironmentSummary
|
|
30
32
|
from sqlmesh.core.linter.rule import RuleViolation
|
|
31
33
|
from sqlmesh.core.model import Model
|
|
@@ -46,6 +48,7 @@ from sqlmesh.utils.errors import (
|
|
|
46
48
|
NodeAuditsErrors,
|
|
47
49
|
format_destructive_change_msg,
|
|
48
50
|
)
|
|
51
|
+
from sqlmesh.utils.rich import strip_ansi_codes
|
|
49
52
|
|
|
50
53
|
if t.TYPE_CHECKING:
|
|
51
54
|
import ipywidgets as widgets
|
|
@@ -316,6 +319,17 @@ class PlanBuilderConsole(BaseConsole, abc.ABC):
|
|
|
316
319
|
"""Display a destructive change error or warning to the user."""
|
|
317
320
|
|
|
318
321
|
|
|
322
|
+
class UnitTestConsole(abc.ABC):
|
|
323
|
+
@abc.abstractmethod
|
|
324
|
+
def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> None:
|
|
325
|
+
"""Display the test result and output.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
result: The unittest test result that contains metrics like num success, fails, ect.
|
|
329
|
+
target_dialect: The dialect that tests were run against. Assumes all tests run against the same dialect.
|
|
330
|
+
"""
|
|
331
|
+
|
|
332
|
+
|
|
319
333
|
class Console(
|
|
320
334
|
PlanBuilderConsole,
|
|
321
335
|
LinterConsole,
|
|
@@ -327,6 +341,7 @@ class Console(
|
|
|
327
341
|
DifferenceConsole,
|
|
328
342
|
TableDiffConsole,
|
|
329
343
|
BaseConsole,
|
|
344
|
+
UnitTestConsole,
|
|
330
345
|
abc.ABC,
|
|
331
346
|
):
|
|
332
347
|
"""Abstract base class for defining classes used for displaying information to the user and also interact
|
|
@@ -460,18 +475,6 @@ class Console(
|
|
|
460
475
|
fail. Default: False
|
|
461
476
|
"""
|
|
462
477
|
|
|
463
|
-
@abc.abstractmethod
|
|
464
|
-
def log_test_results(
|
|
465
|
-
self, result: unittest.result.TestResult, output: t.Optional[str], target_dialect: str
|
|
466
|
-
) -> None:
|
|
467
|
-
"""Display the test result and output.
|
|
468
|
-
|
|
469
|
-
Args:
|
|
470
|
-
result: The unittest test result that contains metrics like num success, fails, ect.
|
|
471
|
-
output: The generated output from the unittest.
|
|
472
|
-
target_dialect: The dialect that tests were run against. Assumes all tests run against the same dialect.
|
|
473
|
-
"""
|
|
474
|
-
|
|
475
478
|
@abc.abstractmethod
|
|
476
479
|
def show_sql(self, sql: str) -> None:
|
|
477
480
|
"""Display to the user SQL."""
|
|
@@ -668,9 +671,7 @@ class NoopConsole(Console):
|
|
|
668
671
|
if auto_apply:
|
|
669
672
|
plan_builder.apply()
|
|
670
673
|
|
|
671
|
-
def log_test_results(
|
|
672
|
-
self, result: unittest.result.TestResult, output: t.Optional[str], target_dialect: str
|
|
673
|
-
) -> None:
|
|
674
|
+
def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> None:
|
|
674
675
|
pass
|
|
675
676
|
|
|
676
677
|
def show_sql(self, sql: str) -> None:
|
|
@@ -1952,10 +1953,12 @@ class TerminalConsole(Console):
|
|
|
1952
1953
|
):
|
|
1953
1954
|
plan_builder.apply()
|
|
1954
1955
|
|
|
1955
|
-
def log_test_results(
|
|
1956
|
-
self, result: unittest.result.TestResult, output: t.Optional[str], target_dialect: str
|
|
1957
|
-
) -> None:
|
|
1956
|
+
def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> None:
|
|
1958
1957
|
divider_length = 70
|
|
1958
|
+
|
|
1959
|
+
self._log_test_details(result)
|
|
1960
|
+
self._print("\n")
|
|
1961
|
+
|
|
1959
1962
|
if result.wasSuccessful():
|
|
1960
1963
|
self._print("=" * divider_length)
|
|
1961
1964
|
self._print(
|
|
@@ -1972,9 +1975,13 @@ class TerminalConsole(Console):
|
|
|
1972
1975
|
)
|
|
1973
1976
|
for test, _ in result.failures + result.errors:
|
|
1974
1977
|
if isinstance(test, ModelTest):
|
|
1975
|
-
self._print(f"Failure Test: {test.
|
|
1978
|
+
self._print(f"Failure Test: {test.path}::{test.test_name}")
|
|
1976
1979
|
self._print("=" * divider_length)
|
|
1977
|
-
|
|
1980
|
+
|
|
1981
|
+
def _captured_unit_test_results(self, result: ModelTextTestResult) -> str:
|
|
1982
|
+
with self.console.capture() as capture:
|
|
1983
|
+
self._log_test_details(result)
|
|
1984
|
+
return strip_ansi_codes(capture.get())
|
|
1978
1985
|
|
|
1979
1986
|
def show_sql(self, sql: str) -> None:
|
|
1980
1987
|
self._print(Syntax(sql, "sql", word_wrap=True), crop=False)
|
|
@@ -2492,6 +2499,63 @@ class TerminalConsole(Console):
|
|
|
2492
2499
|
else:
|
|
2493
2500
|
self.log_warning(msg)
|
|
2494
2501
|
|
|
2502
|
+
def _log_test_details(self, result: ModelTextTestResult) -> None:
|
|
2503
|
+
"""
|
|
2504
|
+
This is a helper method that encapsulates the logic for logging the relevant unittest for the result.
|
|
2505
|
+
The top level method (`log_test_results`) reuses `_log_test_details` differently based on the console.
|
|
2506
|
+
|
|
2507
|
+
Args:
|
|
2508
|
+
result: The unittest test result that contains metrics like num success, fails, ect.
|
|
2509
|
+
"""
|
|
2510
|
+
tests_run = result.testsRun
|
|
2511
|
+
errors = result.errors
|
|
2512
|
+
failures = result.failures
|
|
2513
|
+
skipped = result.skipped
|
|
2514
|
+
is_success = not (errors or failures)
|
|
2515
|
+
|
|
2516
|
+
infos = []
|
|
2517
|
+
if failures:
|
|
2518
|
+
infos.append(f"failures={len(failures)}")
|
|
2519
|
+
if errors:
|
|
2520
|
+
infos.append(f"errors={len(errors)}")
|
|
2521
|
+
if skipped:
|
|
2522
|
+
infos.append(f"skipped={skipped}")
|
|
2523
|
+
|
|
2524
|
+
self._print("\n", end="")
|
|
2525
|
+
|
|
2526
|
+
for (test_case, failure), test_failure_tables in zip_longest( # type: ignore
|
|
2527
|
+
failures, result.failure_tables
|
|
2528
|
+
):
|
|
2529
|
+
self._print(unittest.TextTestResult.separator1)
|
|
2530
|
+
self._print(f"FAIL: {test_case}")
|
|
2531
|
+
|
|
2532
|
+
if test_description := test_case.shortDescription():
|
|
2533
|
+
self._print(test_description)
|
|
2534
|
+
self._print(f"{unittest.TextTestResult.separator2}")
|
|
2535
|
+
|
|
2536
|
+
if not test_failure_tables:
|
|
2537
|
+
self._print(failure)
|
|
2538
|
+
else:
|
|
2539
|
+
for failure_table in test_failure_tables:
|
|
2540
|
+
self._print(failure_table)
|
|
2541
|
+
self._print("\n", end="")
|
|
2542
|
+
|
|
2543
|
+
for test_case, error in errors:
|
|
2544
|
+
self._print(unittest.TextTestResult.separator1)
|
|
2545
|
+
self._print(f"ERROR: {test_case}")
|
|
2546
|
+
self._print(f"{unittest.TextTestResult.separator2}")
|
|
2547
|
+
self._print(error)
|
|
2548
|
+
|
|
2549
|
+
# Output final report
|
|
2550
|
+
self._print(unittest.TextTestResult.separator2)
|
|
2551
|
+
test_duration_msg = f" in {result.duration:.3f}s" if result.duration else ""
|
|
2552
|
+
self._print(
|
|
2553
|
+
f"\nRan {tests_run} {'tests' if tests_run > 1 else 'test'}{test_duration_msg} \n"
|
|
2554
|
+
)
|
|
2555
|
+
self._print(
|
|
2556
|
+
f"{'OK' if is_success else 'FAILED'}{' (' + ', '.join(infos) + ')' if infos else ''}"
|
|
2557
|
+
)
|
|
2558
|
+
|
|
2495
2559
|
|
|
2496
2560
|
def _cells_match(x: t.Any, y: t.Any) -> bool:
|
|
2497
2561
|
"""Helper function to compare two cells and returns true if they're equal, handling array objects."""
|
|
@@ -2763,9 +2827,7 @@ class NotebookMagicConsole(TerminalConsole):
|
|
|
2763
2827
|
)
|
|
2764
2828
|
self.display(radio)
|
|
2765
2829
|
|
|
2766
|
-
def log_test_results(
|
|
2767
|
-
self, result: unittest.result.TestResult, output: t.Optional[str], target_dialect: str
|
|
2768
|
-
) -> None:
|
|
2830
|
+
def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> None:
|
|
2769
2831
|
import ipywidgets as widgets
|
|
2770
2832
|
|
|
2771
2833
|
divider_length = 70
|
|
@@ -2781,12 +2843,14 @@ class NotebookMagicConsole(TerminalConsole):
|
|
|
2781
2843
|
h(
|
|
2782
2844
|
"span",
|
|
2783
2845
|
{"style": {**shared_style, **success_color}},
|
|
2784
|
-
f"Successfully Ran {str(result.testsRun)}
|
|
2846
|
+
f"Successfully Ran {str(result.testsRun)} tests against {target_dialect}",
|
|
2785
2847
|
)
|
|
2786
2848
|
)
|
|
2787
2849
|
footer = str(h("span", {"style": shared_style}, "=" * divider_length))
|
|
2788
2850
|
self.display(widgets.HTML("<br>".join([header, message, footer])))
|
|
2789
2851
|
else:
|
|
2852
|
+
output = self._captured_unit_test_results(result)
|
|
2853
|
+
|
|
2790
2854
|
fail_color = {"color": "#db3737"}
|
|
2791
2855
|
fail_shared_style = {**shared_style, **fail_color}
|
|
2792
2856
|
header = str(h("span", {"style": fail_shared_style}, "-" * divider_length))
|
|
@@ -3137,21 +3201,22 @@ class MarkdownConsole(CaptureTerminalConsole):
|
|
|
3137
3201
|
def log_success(self, message: str) -> None:
|
|
3138
3202
|
self._print(message)
|
|
3139
3203
|
|
|
3140
|
-
def log_test_results(
|
|
3141
|
-
self, result: unittest.result.TestResult, output: t.Optional[str], target_dialect: str
|
|
3142
|
-
) -> None:
|
|
3204
|
+
def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> None:
|
|
3143
3205
|
if result.wasSuccessful():
|
|
3144
3206
|
self._print(
|
|
3145
3207
|
f"**Successfully Ran `{str(result.testsRun)}` Tests Against `{target_dialect}`**\n\n"
|
|
3146
3208
|
)
|
|
3147
3209
|
else:
|
|
3210
|
+
self._print("```")
|
|
3211
|
+
self._log_test_details(result)
|
|
3212
|
+
self._print("```\n\n")
|
|
3213
|
+
|
|
3148
3214
|
self._print(
|
|
3149
3215
|
f"**Num Successful Tests: {result.testsRun - len(result.failures) - len(result.errors)}**\n\n"
|
|
3150
3216
|
)
|
|
3151
3217
|
for test, _ in result.failures + result.errors:
|
|
3152
3218
|
if isinstance(test, ModelTest):
|
|
3153
3219
|
self._print(f"* Failure Test: `{test.model.name}` - `{test.test_name}`\n\n")
|
|
3154
|
-
self._print(f"```{output}```\n\n")
|
|
3155
3220
|
|
|
3156
3221
|
def log_skipped_models(self, snapshot_names: t.Set[str]) -> None:
|
|
3157
3222
|
if snapshot_names:
|
|
@@ -3530,9 +3595,7 @@ class DebuggerTerminalConsole(TerminalConsole):
|
|
|
3530
3595
|
for modified in context_diff.modified_snapshots:
|
|
3531
3596
|
self._write(f" Modified: {modified}")
|
|
3532
3597
|
|
|
3533
|
-
def log_test_results(
|
|
3534
|
-
self, result: unittest.result.TestResult, output: t.Optional[str], target_dialect: str
|
|
3535
|
-
) -> None:
|
|
3598
|
+
def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> None:
|
|
3536
3599
|
self._write("Test Results:", result)
|
|
3537
3600
|
|
|
3538
3601
|
def show_sql(self, sql: str) -> None:
|
sqlmesh/core/context.py
CHANGED
|
@@ -40,7 +40,6 @@ import sys
|
|
|
40
40
|
import time
|
|
41
41
|
import traceback
|
|
42
42
|
import typing as t
|
|
43
|
-
import unittest.result
|
|
44
43
|
from functools import cached_property
|
|
45
44
|
from io import StringIO
|
|
46
45
|
from itertools import chain
|
|
@@ -2061,7 +2060,7 @@ class GenericContext(BaseContext, t.Generic[C]):
|
|
|
2061
2060
|
|
|
2062
2061
|
test_meta = self.load_model_tests(tests=tests, patterns=match_patterns)
|
|
2063
2062
|
|
|
2064
|
-
|
|
2063
|
+
result = run_tests(
|
|
2065
2064
|
model_test_metadata=test_meta,
|
|
2066
2065
|
models=self._models,
|
|
2067
2066
|
config=self.config,
|
|
@@ -2074,6 +2073,13 @@ class GenericContext(BaseContext, t.Generic[C]):
|
|
|
2074
2073
|
default_catalog_dialect=self.config.dialect or "",
|
|
2075
2074
|
)
|
|
2076
2075
|
|
|
2076
|
+
self.console.log_test_results(
|
|
2077
|
+
result,
|
|
2078
|
+
self.test_connection_config._engine_adapter.DIALECT,
|
|
2079
|
+
)
|
|
2080
|
+
|
|
2081
|
+
return result
|
|
2082
|
+
|
|
2077
2083
|
@python_api_analytics
|
|
2078
2084
|
def audit(
|
|
2079
2085
|
self,
|
|
@@ -2496,28 +2502,20 @@ class GenericContext(BaseContext, t.Generic[C]):
|
|
|
2496
2502
|
|
|
2497
2503
|
def _run_tests(
|
|
2498
2504
|
self, verbosity: Verbosity = Verbosity.DEFAULT
|
|
2499
|
-
) -> t.Tuple[
|
|
2505
|
+
) -> t.Tuple[ModelTextTestResult, str]:
|
|
2500
2506
|
test_output_io = StringIO()
|
|
2501
2507
|
result = self.test(stream=test_output_io, verbosity=verbosity)
|
|
2502
2508
|
return result, test_output_io.getvalue()
|
|
2503
2509
|
|
|
2504
|
-
def _run_plan_tests(
|
|
2505
|
-
self, skip_tests: bool = False
|
|
2506
|
-
) -> t.Tuple[t.Optional[unittest.result.TestResult], t.Optional[str]]:
|
|
2510
|
+
def _run_plan_tests(self, skip_tests: bool = False) -> t.Optional[ModelTextTestResult]:
|
|
2507
2511
|
if not skip_tests:
|
|
2508
|
-
result
|
|
2509
|
-
if result.testsRun > 0:
|
|
2510
|
-
self.console.log_test_results(
|
|
2511
|
-
result,
|
|
2512
|
-
test_output,
|
|
2513
|
-
self.test_connection_config._engine_adapter.DIALECT,
|
|
2514
|
-
)
|
|
2512
|
+
result = self.test()
|
|
2515
2513
|
if not result.wasSuccessful():
|
|
2516
2514
|
raise PlanError(
|
|
2517
2515
|
"Cannot generate plan due to failing test(s). Fix test(s) and run again."
|
|
2518
2516
|
)
|
|
2519
|
-
return result
|
|
2520
|
-
return None
|
|
2517
|
+
return result
|
|
2518
|
+
return None
|
|
2521
2519
|
|
|
2522
2520
|
@property
|
|
2523
2521
|
def _model_tables(self) -> t.Dict[str, str]:
|
sqlmesh/core/test/definition.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import sys
|
|
4
|
+
|
|
3
5
|
import datetime
|
|
4
6
|
import threading
|
|
5
7
|
import typing as t
|
|
@@ -10,6 +12,7 @@ from itertools import chain
|
|
|
10
12
|
from pathlib import Path
|
|
11
13
|
from unittest.mock import patch
|
|
12
14
|
|
|
15
|
+
|
|
13
16
|
from io import StringIO
|
|
14
17
|
from sqlglot import Dialect, exp
|
|
15
18
|
from sqlglot.optimizer.annotate_types import annotate_types
|
|
@@ -24,6 +27,8 @@ from sqlmesh.utils import UniqueKeyDict, random_id, type_is_known, yaml
|
|
|
24
27
|
from sqlmesh.utils.date import date_dict, pandas_timestamp_to_pydatetime, to_datetime
|
|
25
28
|
from sqlmesh.utils.errors import ConfigError, TestError
|
|
26
29
|
from sqlmesh.utils.yaml import load as yaml_load
|
|
30
|
+
from sqlmesh.utils import Verbosity
|
|
31
|
+
from sqlmesh.utils.rich import df_to_table
|
|
27
32
|
|
|
28
33
|
if t.TYPE_CHECKING:
|
|
29
34
|
import pandas as pd
|
|
@@ -60,6 +65,7 @@ class ModelTest(unittest.TestCase):
|
|
|
60
65
|
preserve_fixtures: bool = False,
|
|
61
66
|
default_catalog: str | None = None,
|
|
62
67
|
concurrency: bool = False,
|
|
68
|
+
verbosity: Verbosity = Verbosity.DEFAULT,
|
|
63
69
|
) -> None:
|
|
64
70
|
"""ModelTest encapsulates a unit test for a model.
|
|
65
71
|
|
|
@@ -83,6 +89,7 @@ class ModelTest(unittest.TestCase):
|
|
|
83
89
|
self.default_catalog = default_catalog
|
|
84
90
|
self.dialect = dialect
|
|
85
91
|
self.concurrency = concurrency
|
|
92
|
+
self.verbosity = verbosity
|
|
86
93
|
|
|
87
94
|
self._fixture_table_cache: t.Dict[str, exp.Table] = {}
|
|
88
95
|
self._normalized_column_name_cache: t.Dict[str, str] = {}
|
|
@@ -134,6 +141,11 @@ class ModelTest(unittest.TestCase):
|
|
|
134
141
|
|
|
135
142
|
super().__init__()
|
|
136
143
|
|
|
144
|
+
def defaultTestResult(self) -> unittest.TestResult:
|
|
145
|
+
from sqlmesh.core.test.result import ModelTextTestResult
|
|
146
|
+
|
|
147
|
+
return ModelTextTestResult(stream=sys.stdout, descriptions=True, verbosity=self.verbosity)
|
|
148
|
+
|
|
137
149
|
def shortDescription(self) -> t.Optional[str]:
|
|
138
150
|
return self.body.get("description")
|
|
139
151
|
|
|
@@ -290,23 +302,53 @@ class ModelTest(unittest.TestCase):
|
|
|
290
302
|
check_like=True, # Ignore column order
|
|
291
303
|
)
|
|
292
304
|
except AssertionError as e:
|
|
305
|
+
# There are 2 concepts at play here:
|
|
306
|
+
# 1. The Exception args will contain the error message plus the diff dataframe table stringified
|
|
307
|
+
# (backwards compatibility with existing tests, possible to serialize/send over network etc)
|
|
308
|
+
# 2. Each test will also transform these diff dataframes into Rich tables, which will be the ones that'll
|
|
309
|
+
# be surfaced to the user through Console for better UX (versus stringified dataframes)
|
|
310
|
+
#
|
|
311
|
+
# This is a bit of a hack, but it's a way to get the best of both worlds.
|
|
312
|
+
args: t.List[t.Any] = []
|
|
293
313
|
if expected.shape != actual.shape:
|
|
294
314
|
_raise_if_unexpected_columns(expected.columns, actual.columns)
|
|
295
315
|
|
|
296
|
-
|
|
316
|
+
args.append("Data mismatch (rows are different)")
|
|
297
317
|
|
|
298
318
|
missing_rows = _row_difference(expected, actual)
|
|
299
319
|
if not missing_rows.empty:
|
|
300
|
-
|
|
320
|
+
args[0] += f"\n\nMissing rows:\n\n{missing_rows}"
|
|
321
|
+
args.append(df_to_table("Missing rows", missing_rows))
|
|
301
322
|
|
|
302
323
|
unexpected_rows = _row_difference(actual, expected)
|
|
324
|
+
|
|
303
325
|
if not unexpected_rows.empty:
|
|
304
|
-
|
|
326
|
+
args[0] += f"\n\nUnexpected rows:\n\n{unexpected_rows}"
|
|
327
|
+
args.append(df_to_table("Unexpected rows", unexpected_rows))
|
|
305
328
|
|
|
306
|
-
e.args = (error_msg,)
|
|
307
329
|
else:
|
|
308
330
|
diff = expected.compare(actual).rename(columns={"self": "exp", "other": "act"})
|
|
309
|
-
|
|
331
|
+
|
|
332
|
+
args.append(f"Data mismatch (exp: expected, act: actual)\n\n{diff}")
|
|
333
|
+
|
|
334
|
+
diff.rename(columns={"exp": "Expected", "act": "Actual"}, inplace=True)
|
|
335
|
+
if self.verbosity == Verbosity.DEFAULT:
|
|
336
|
+
args.extend(
|
|
337
|
+
df_to_table("Data mismatch", df) for df in _split_df_by_column_pairs(diff)
|
|
338
|
+
)
|
|
339
|
+
else:
|
|
340
|
+
from pandas import MultiIndex
|
|
341
|
+
|
|
342
|
+
levels = t.cast(MultiIndex, diff.columns).levels[0]
|
|
343
|
+
for col in levels:
|
|
344
|
+
col_diff = diff[col]
|
|
345
|
+
if not col_diff.empty:
|
|
346
|
+
table = df_to_table(
|
|
347
|
+
f"[bold red]Column '{col}' mismatch[/bold red]", col_diff
|
|
348
|
+
)
|
|
349
|
+
args.append(table)
|
|
350
|
+
|
|
351
|
+
e.args = (*args,)
|
|
310
352
|
|
|
311
353
|
raise e
|
|
312
354
|
|
|
@@ -328,6 +370,7 @@ class ModelTest(unittest.TestCase):
|
|
|
328
370
|
preserve_fixtures: bool = False,
|
|
329
371
|
default_catalog: str | None = None,
|
|
330
372
|
concurrency: bool = False,
|
|
373
|
+
verbosity: Verbosity = Verbosity.DEFAULT,
|
|
331
374
|
) -> t.Optional[ModelTest]:
|
|
332
375
|
"""Create a SqlModelTest or a PythonModelTest.
|
|
333
376
|
|
|
@@ -373,6 +416,7 @@ class ModelTest(unittest.TestCase):
|
|
|
373
416
|
preserve_fixtures,
|
|
374
417
|
default_catalog,
|
|
375
418
|
concurrency,
|
|
419
|
+
verbosity,
|
|
376
420
|
)
|
|
377
421
|
except Exception as e:
|
|
378
422
|
raise TestError(f"Failed to create test {test_name} ({path})\n{str(e)}")
|
|
@@ -692,6 +736,7 @@ class PythonModelTest(ModelTest):
|
|
|
692
736
|
preserve_fixtures: bool = False,
|
|
693
737
|
default_catalog: str | None = None,
|
|
694
738
|
concurrency: bool = False,
|
|
739
|
+
verbosity: Verbosity = Verbosity.DEFAULT,
|
|
695
740
|
) -> None:
|
|
696
741
|
"""PythonModelTest encapsulates a unit test for a Python model.
|
|
697
742
|
|
|
@@ -718,6 +763,7 @@ class PythonModelTest(ModelTest):
|
|
|
718
763
|
preserve_fixtures,
|
|
719
764
|
default_catalog,
|
|
720
765
|
concurrency,
|
|
766
|
+
verbosity,
|
|
721
767
|
)
|
|
722
768
|
|
|
723
769
|
self.context = TestExecutionContext(
|
|
@@ -951,3 +997,43 @@ def _normalize_df_value(value: t.Any) -> t.Any:
|
|
|
951
997
|
return {k: _normalize_df_value(v) for k, v in zip(value["key"], value["value"])}
|
|
952
998
|
return {k: _normalize_df_value(v) for k, v in value.items()}
|
|
953
999
|
return value
|
|
1000
|
+
|
|
1001
|
+
|
|
1002
|
+
def _split_df_by_column_pairs(df: pd.DataFrame, pairs_per_chunk: int = 4) -> t.List[pd.DataFrame]:
|
|
1003
|
+
"""Split a dataframe into chunks of column pairs.
|
|
1004
|
+
|
|
1005
|
+
Args:
|
|
1006
|
+
df: The dataframe to split
|
|
1007
|
+
pairs_per_chunk: Number of column pairs per chunk (default: 4)
|
|
1008
|
+
|
|
1009
|
+
Returns:
|
|
1010
|
+
List of dataframes, each containing an even number of columns
|
|
1011
|
+
"""
|
|
1012
|
+
total_columns = len(df.columns)
|
|
1013
|
+
|
|
1014
|
+
# If we have fewer columns than pairs_per_chunk * 2, return the original df
|
|
1015
|
+
if total_columns <= pairs_per_chunk * 2:
|
|
1016
|
+
return [df]
|
|
1017
|
+
|
|
1018
|
+
# Calculate number of chunks needed to split columns evenly
|
|
1019
|
+
num_chunks = (total_columns + (pairs_per_chunk * 2 - 1)) // (pairs_per_chunk * 2)
|
|
1020
|
+
|
|
1021
|
+
# Calculate columns per chunk to ensure equal distribution
|
|
1022
|
+
# We round down to nearest even number to ensure each chunk has even columns
|
|
1023
|
+
columns_per_chunk = (total_columns // num_chunks) & ~1 # Round down to nearest even number
|
|
1024
|
+
remainder = total_columns - (columns_per_chunk * num_chunks)
|
|
1025
|
+
|
|
1026
|
+
chunks = []
|
|
1027
|
+
start_idx = 0
|
|
1028
|
+
|
|
1029
|
+
# Distribute columns evenly across chunks
|
|
1030
|
+
for i in range(num_chunks):
|
|
1031
|
+
# Add 2 columns to early chunks if there's a remainder
|
|
1032
|
+
# This ensures we always add pairs of columns
|
|
1033
|
+
extra = 2 if i < remainder // 2 else 0
|
|
1034
|
+
end_idx = start_idx + columns_per_chunk + extra
|
|
1035
|
+
chunk = df.iloc[:, start_idx:end_idx]
|
|
1036
|
+
chunks.append(chunk)
|
|
1037
|
+
start_idx = end_idx
|
|
1038
|
+
|
|
1039
|
+
return chunks
|
sqlmesh/core/test/result.py
CHANGED
|
@@ -15,10 +15,13 @@ class ModelTextTestResult(unittest.TextTestResult):
|
|
|
15
15
|
successes: t.List[unittest.TestCase]
|
|
16
16
|
|
|
17
17
|
def __init__(self, *args: t.Any, **kwargs: t.Any):
|
|
18
|
+
self.console = kwargs.pop("console", None)
|
|
18
19
|
super().__init__(*args, **kwargs)
|
|
19
20
|
self.successes = []
|
|
20
21
|
self.original_failures: t.List[t.Tuple[unittest.TestCase, ErrorType]] = []
|
|
22
|
+
self.failure_tables: t.List[t.Tuple[t.Any, ...]] = []
|
|
21
23
|
self.original_errors: t.List[t.Tuple[unittest.TestCase, ErrorType]] = []
|
|
24
|
+
self.duration: t.Optional[float] = None
|
|
22
25
|
|
|
23
26
|
def addSubTest(
|
|
24
27
|
self,
|
|
@@ -41,6 +44,12 @@ class ModelTextTestResult(unittest.TextTestResult):
|
|
|
41
44
|
|
|
42
45
|
super().addSubTest(test, subtest, err)
|
|
43
46
|
|
|
47
|
+
def _print_char(self, char: str) -> None:
|
|
48
|
+
from sqlmesh.core.console import TerminalConsole
|
|
49
|
+
|
|
50
|
+
if isinstance(self.console, TerminalConsole):
|
|
51
|
+
self.console._print(char, end="")
|
|
52
|
+
|
|
44
53
|
def addFailure(self, test: unittest.TestCase, err: ErrorType) -> None:
|
|
45
54
|
"""Called when the test case test signals a failure.
|
|
46
55
|
|
|
@@ -51,7 +60,18 @@ class ModelTextTestResult(unittest.TextTestResult):
|
|
|
51
60
|
err: A tuple of the form returned by sys.exc_info(), i.e., (type, value, traceback).
|
|
52
61
|
"""
|
|
53
62
|
exctype, value, _ = err
|
|
63
|
+
|
|
64
|
+
if value and value.args:
|
|
65
|
+
exception_msg, rich_tables = value.args[:1], value.args[1:]
|
|
66
|
+
value.args = exception_msg
|
|
67
|
+
|
|
68
|
+
if rich_tables:
|
|
69
|
+
self.failure_tables.append(rich_tables)
|
|
70
|
+
|
|
71
|
+
self._print_char("F")
|
|
72
|
+
|
|
54
73
|
self.original_failures.append((test, err))
|
|
74
|
+
|
|
55
75
|
# Intentionally ignore the traceback to hide it from the user
|
|
56
76
|
return super().addFailure(test, (exctype, value, None)) # type: ignore
|
|
57
77
|
|
|
@@ -64,6 +84,9 @@ class ModelTextTestResult(unittest.TextTestResult):
|
|
|
64
84
|
"""
|
|
65
85
|
exctype, value, _ = err
|
|
66
86
|
self.original_errors.append((test, err))
|
|
87
|
+
|
|
88
|
+
self._print_char("E")
|
|
89
|
+
|
|
67
90
|
# Intentionally ignore the traceback to hide it from the user
|
|
68
91
|
return super().addError(test, (exctype, value, None)) # type: ignore
|
|
69
92
|
|
|
@@ -74,52 +97,24 @@ class ModelTextTestResult(unittest.TextTestResult):
|
|
|
74
97
|
test: The test case
|
|
75
98
|
"""
|
|
76
99
|
super().addSuccess(test)
|
|
77
|
-
self.successes.append(test)
|
|
78
100
|
|
|
79
|
-
|
|
80
|
-
"""
|
|
81
|
-
Log the test report following unittest's conventions.
|
|
101
|
+
self._print_char(".")
|
|
82
102
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
stream = self.stream
|
|
102
|
-
|
|
103
|
-
stream.write("\n")
|
|
104
|
-
|
|
105
|
-
for test_case, failure in failures:
|
|
106
|
-
stream.writeln(unittest.TextTestResult.separator1)
|
|
107
|
-
stream.writeln(f"FAIL: {test_case}")
|
|
108
|
-
if test_description := test_case.shortDescription():
|
|
109
|
-
stream.writeln(test_description)
|
|
110
|
-
stream.writeln(unittest.TextTestResult.separator2)
|
|
111
|
-
stream.writeln(failure)
|
|
112
|
-
|
|
113
|
-
for test_case, error in errors:
|
|
114
|
-
stream.writeln(unittest.TextTestResult.separator1)
|
|
115
|
-
stream.writeln(f"ERROR: {test_case}")
|
|
116
|
-
stream.writeln(error)
|
|
117
|
-
|
|
118
|
-
# Output final report
|
|
119
|
-
stream.writeln(unittest.TextTestResult.separator2)
|
|
120
|
-
stream.writeln(
|
|
121
|
-
f"Ran {tests_run} {'tests' if tests_run > 1 else 'test'} in {test_duration:.3f}s \n"
|
|
122
|
-
)
|
|
123
|
-
stream.writeln(
|
|
124
|
-
f"{'OK' if is_success else 'FAILED'}{' (' + ', '.join(infos) + ')' if infos else ''}"
|
|
125
|
-
)
|
|
103
|
+
self.successes.append(test)
|
|
104
|
+
|
|
105
|
+
def merge(self, other: ModelTextTestResult) -> None:
|
|
106
|
+
if other.successes:
|
|
107
|
+
self.addSuccess(other.successes[0])
|
|
108
|
+
elif other.errors:
|
|
109
|
+
for error_test, error in other.original_errors:
|
|
110
|
+
self.addError(error_test, error)
|
|
111
|
+
elif other.failures:
|
|
112
|
+
for failure_test, failure in other.original_failures:
|
|
113
|
+
self.addFailure(failure_test, failure)
|
|
114
|
+
|
|
115
|
+
self.failure_tables.extend(other.failure_tables)
|
|
116
|
+
elif other.skipped:
|
|
117
|
+
skipped_args = other.skipped[0]
|
|
118
|
+
self.addSkip(skipped_args[0], skipped_args[1])
|
|
119
|
+
|
|
120
|
+
self.testsRun += 1
|
sqlmesh/core/test/runner.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import sys
|
|
4
3
|
import time
|
|
5
4
|
import threading
|
|
6
5
|
import typing as t
|
|
7
6
|
import unittest
|
|
7
|
+
from io import StringIO
|
|
8
8
|
|
|
9
9
|
import concurrent
|
|
10
10
|
from concurrent.futures import ThreadPoolExecutor
|
|
@@ -16,7 +16,6 @@ from sqlmesh.core.test.discovery import (
|
|
|
16
16
|
ModelTestMetadata as ModelTestMetadata,
|
|
17
17
|
)
|
|
18
18
|
from sqlmesh.core.config.connection import BaseDuckDBConnectionConfig
|
|
19
|
-
|
|
20
19
|
from sqlmesh.core.test.result import ModelTextTestResult as ModelTextTestResult
|
|
21
20
|
from sqlmesh.utils import UniqueKeyDict, Verbosity
|
|
22
21
|
|
|
@@ -106,10 +105,13 @@ def run_tests(
|
|
|
106
105
|
|
|
107
106
|
lock = threading.Lock()
|
|
108
107
|
|
|
108
|
+
from sqlmesh.core.console import get_console
|
|
109
|
+
|
|
109
110
|
combined_results = ModelTextTestResult(
|
|
110
|
-
stream=unittest.runner._WritelnDecorator(stream or
|
|
111
|
+
stream=unittest.runner._WritelnDecorator(stream or StringIO()), # type: ignore
|
|
111
112
|
verbosity=2 if verbosity >= Verbosity.VERBOSE else 1,
|
|
112
113
|
descriptions=True,
|
|
114
|
+
console=get_console(),
|
|
113
115
|
)
|
|
114
116
|
|
|
115
117
|
metadata_to_adapter = create_testing_engine_adapters(
|
|
@@ -136,6 +138,7 @@ def run_tests(
|
|
|
136
138
|
default_catalog=default_catalog,
|
|
137
139
|
preserve_fixtures=preserve_fixtures,
|
|
138
140
|
concurrency=num_workers > 1,
|
|
141
|
+
verbosity=verbosity,
|
|
139
142
|
)
|
|
140
143
|
|
|
141
144
|
if not test:
|
|
@@ -147,19 +150,7 @@ def run_tests(
|
|
|
147
150
|
)
|
|
148
151
|
|
|
149
152
|
with lock:
|
|
150
|
-
|
|
151
|
-
combined_results.addSuccess(result.successes[0])
|
|
152
|
-
elif result.errors:
|
|
153
|
-
for error_test, error in result.original_errors:
|
|
154
|
-
combined_results.addError(error_test, error)
|
|
155
|
-
elif result.failures:
|
|
156
|
-
for failure_test, failure in result.original_failures:
|
|
157
|
-
combined_results.addFailure(failure_test, failure)
|
|
158
|
-
elif result.skipped:
|
|
159
|
-
skipped_args = result.skipped[0]
|
|
160
|
-
combined_results.addSkip(skipped_args[0], skipped_args[1])
|
|
161
|
-
|
|
162
|
-
combined_results.testsRun += 1
|
|
153
|
+
combined_results.merge(result)
|
|
163
154
|
|
|
164
155
|
return result
|
|
165
156
|
|
|
@@ -183,6 +174,6 @@ def run_tests(
|
|
|
183
174
|
|
|
184
175
|
end_time = time.perf_counter()
|
|
185
176
|
|
|
186
|
-
combined_results.
|
|
177
|
+
combined_results.duration = end_time - start_time
|
|
187
178
|
|
|
188
179
|
return combined_results
|
|
@@ -63,20 +63,19 @@ def check_required_approvers(ctx: click.Context) -> None:
|
|
|
63
63
|
def _run_tests(controller: GithubController) -> bool:
|
|
64
64
|
controller.update_test_check(status=GithubCheckStatus.IN_PROGRESS)
|
|
65
65
|
try:
|
|
66
|
-
result,
|
|
66
|
+
result, _ = controller.run_tests()
|
|
67
67
|
controller.update_test_check(
|
|
68
68
|
status=GithubCheckStatus.COMPLETED,
|
|
69
69
|
# Conclusion will be updated with final status based on test results
|
|
70
70
|
conclusion=GithubCheckConclusion.NEUTRAL,
|
|
71
71
|
result=result,
|
|
72
|
-
output=output,
|
|
73
72
|
)
|
|
74
73
|
return result.wasSuccessful()
|
|
75
74
|
except Exception:
|
|
76
75
|
controller.update_test_check(
|
|
77
76
|
status=GithubCheckStatus.COMPLETED,
|
|
78
77
|
conclusion=GithubCheckConclusion.FAILURE,
|
|
79
|
-
|
|
78
|
+
traceback=traceback.format_exc(),
|
|
80
79
|
)
|
|
81
80
|
return False
|
|
82
81
|
|
|
@@ -8,7 +8,6 @@ import pathlib
|
|
|
8
8
|
import re
|
|
9
9
|
import traceback
|
|
10
10
|
import typing as t
|
|
11
|
-
import unittest
|
|
12
11
|
from enum import Enum
|
|
13
12
|
from typing import List
|
|
14
13
|
from pathlib import Path
|
|
@@ -20,6 +19,7 @@ from sqlglot.helper import seq_get
|
|
|
20
19
|
from sqlmesh.core import constants as c
|
|
21
20
|
from sqlmesh.core.console import SNAPSHOT_CHANGE_CATEGORY_STR, get_console, MarkdownConsole
|
|
22
21
|
from sqlmesh.core.context import Context
|
|
22
|
+
from sqlmesh.core.test.result import ModelTextTestResult
|
|
23
23
|
from sqlmesh.core.environment import Environment
|
|
24
24
|
from sqlmesh.core.plan import Plan, PlanBuilder
|
|
25
25
|
from sqlmesh.core.snapshot.definition import (
|
|
@@ -494,7 +494,7 @@ class GithubController:
|
|
|
494
494
|
except PlanError as e:
|
|
495
495
|
return f"Plan failed to generate. Check for pending or unresolved changes. Error: {e}"
|
|
496
496
|
|
|
497
|
-
def run_tests(self) -> t.Tuple[
|
|
497
|
+
def run_tests(self) -> t.Tuple[ModelTextTestResult, str]:
|
|
498
498
|
"""
|
|
499
499
|
Run tests for the PR
|
|
500
500
|
"""
|
|
@@ -734,8 +734,8 @@ class GithubController:
|
|
|
734
734
|
self,
|
|
735
735
|
status: GithubCheckStatus,
|
|
736
736
|
conclusion: t.Optional[GithubCheckConclusion] = None,
|
|
737
|
-
result: t.Optional[
|
|
738
|
-
|
|
737
|
+
result: t.Optional[ModelTextTestResult] = None,
|
|
738
|
+
traceback: t.Optional[str] = None,
|
|
739
739
|
) -> None:
|
|
740
740
|
"""
|
|
741
741
|
Updates the status of tests for code in the PR
|
|
@@ -743,15 +743,13 @@ class GithubController:
|
|
|
743
743
|
|
|
744
744
|
def conclusion_handler(
|
|
745
745
|
conclusion: GithubCheckConclusion,
|
|
746
|
-
result: t.Optional[
|
|
747
|
-
output: t.Optional[str],
|
|
746
|
+
result: t.Optional[ModelTextTestResult],
|
|
748
747
|
) -> t.Tuple[GithubCheckConclusion, str, t.Optional[str]]:
|
|
749
748
|
if result:
|
|
750
749
|
# Clear out console
|
|
751
750
|
self._console.consume_captured_output()
|
|
752
751
|
self._console.log_test_results(
|
|
753
752
|
result,
|
|
754
|
-
output,
|
|
755
753
|
self._context.test_connection_config._engine_adapter.DIALECT,
|
|
756
754
|
)
|
|
757
755
|
test_summary = self._console.consume_captured_output()
|
|
@@ -762,8 +760,11 @@ class GithubController:
|
|
|
762
760
|
else GithubCheckConclusion.FAILURE
|
|
763
761
|
)
|
|
764
762
|
return test_conclusion, test_title, test_summary
|
|
763
|
+
if traceback:
|
|
764
|
+
self._console._print(traceback)
|
|
765
|
+
|
|
765
766
|
test_title = "Skipped Tests" if conclusion.is_skipped else "Tests Failed"
|
|
766
|
-
return conclusion, test_title,
|
|
767
|
+
return conclusion, test_title, traceback
|
|
767
768
|
|
|
768
769
|
self._update_check_handler(
|
|
769
770
|
check_name="SQLMesh - Run Unit Tests",
|
|
@@ -776,7 +777,7 @@ class GithubController:
|
|
|
776
777
|
}[status],
|
|
777
778
|
None,
|
|
778
779
|
),
|
|
779
|
-
conclusion_handler=functools.partial(conclusion_handler, result=result
|
|
780
|
+
conclusion_handler=functools.partial(conclusion_handler, result=result),
|
|
780
781
|
)
|
|
781
782
|
|
|
782
783
|
def update_required_approval_check(
|
sqlmesh/magics.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from io import StringIO
|
|
4
|
+
|
|
3
5
|
import functools
|
|
4
6
|
import logging
|
|
5
7
|
import typing as t
|
|
@@ -1032,6 +1034,7 @@ class SQLMeshMagics(Magics):
|
|
|
1032
1034
|
tests=args.tests,
|
|
1033
1035
|
verbosity=Verbosity(args.verbose),
|
|
1034
1036
|
preserve_fixtures=args.preserve_fixtures,
|
|
1037
|
+
stream=StringIO(), # consume the output instead of redirecting to stdout
|
|
1035
1038
|
)
|
|
1036
1039
|
|
|
1037
1040
|
@magic_arguments()
|
sqlmesh/utils/rich.py
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import typing as t
|
|
2
4
|
|
|
5
|
+
import re
|
|
6
|
+
|
|
3
7
|
from rich.console import Console
|
|
4
8
|
from rich.progress import Column, ProgressColumn, Task, Text
|
|
5
9
|
from rich.theme import Theme
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
from rich.align import Align
|
|
12
|
+
|
|
13
|
+
if t.TYPE_CHECKING:
|
|
14
|
+
import pandas as pd
|
|
6
15
|
|
|
7
16
|
theme = Theme(
|
|
8
17
|
{
|
|
@@ -46,3 +55,50 @@ class BatchColumn(ProgressColumn):
|
|
|
46
55
|
f"{completed:{total_width}d}{self.separator}{total}",
|
|
47
56
|
style="progress.download",
|
|
48
57
|
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def strip_ansi_codes(text: str) -> str:
|
|
61
|
+
"""Strip ANSI color codes and styling from text."""
|
|
62
|
+
ansi_escape = re.compile(r"\x1b\[[0-9;]*[a-zA-Z]")
|
|
63
|
+
return ansi_escape.sub("", text).strip()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def df_to_table(
|
|
67
|
+
header: str,
|
|
68
|
+
df: pd.DataFrame,
|
|
69
|
+
show_index: bool = True,
|
|
70
|
+
index_name: str = "Row",
|
|
71
|
+
) -> Table:
|
|
72
|
+
"""Convert a pandas.DataFrame obj into a rich.Table obj.
|
|
73
|
+
Args:
|
|
74
|
+
df (DataFrame): A Pandas DataFrame to be converted to a rich Table.
|
|
75
|
+
rich_table (Table): A rich Table that should be populated by the DataFrame values.
|
|
76
|
+
show_index (bool): Add a column with a row count to the table. Defaults to True.
|
|
77
|
+
index_name (str, optional): The column name to give to the index column. Defaults to None, showing no value.
|
|
78
|
+
Returns:
|
|
79
|
+
Table: The rich Table instance passed, populated with the DataFrame values."""
|
|
80
|
+
|
|
81
|
+
rich_table = Table(title=f"[bold red]{header}[/bold red]", show_lines=True, min_width=60)
|
|
82
|
+
if show_index:
|
|
83
|
+
index_name = str(index_name) if index_name else ""
|
|
84
|
+
rich_table.add_column(Align.center(index_name))
|
|
85
|
+
|
|
86
|
+
for column in df.columns:
|
|
87
|
+
column_name = column if isinstance(column, str) else ": ".join(str(col) for col in column)
|
|
88
|
+
|
|
89
|
+
# Color coding unit test columns (expected/actual), can be removed or refactored if df_to_table is used elswhere too
|
|
90
|
+
lower = column_name.lower()
|
|
91
|
+
if "expected" in lower:
|
|
92
|
+
column_name = f"[green]{column_name}[/green]"
|
|
93
|
+
elif "actual" in lower:
|
|
94
|
+
column_name = f"[red]{column_name}[/red]"
|
|
95
|
+
|
|
96
|
+
rich_table.add_column(Align.center(column_name))
|
|
97
|
+
|
|
98
|
+
for index, value_list in enumerate(df.values.tolist()):
|
|
99
|
+
row = [str(index)] if show_index else []
|
|
100
|
+
row += [str(x) for x in value_list]
|
|
101
|
+
center = [Align.center(x) for x in row]
|
|
102
|
+
rich_table.add_row(*center)
|
|
103
|
+
|
|
104
|
+
return rich_table
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
sqlmesh/__init__.py,sha256=QjwaamJIdW7voXoS880yAx7tHm-Im4_mqGA-XJgafos,7631
|
|
2
|
-
sqlmesh/_version.py,sha256
|
|
3
|
-
sqlmesh/magics.py,sha256=
|
|
2
|
+
sqlmesh/_version.py,sha256=-vQeNkqxpJKSmc2IWN3JZZ9rI-Wo6DrJsVeyc_gNCP4,528
|
|
3
|
+
sqlmesh/magics.py,sha256=ysPC5ohXpgQ8IHQPwtQtwGoVypmVq_YBlm1noUueDIw,40448
|
|
4
4
|
sqlmesh/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
5
|
sqlmesh/cicd/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
6
|
sqlmesh/cicd/bot.py,sha256=2zlbn-DXkqQzr3lA0__IGU4XaIfXBXBKLWXNI2DRJX8,759
|
|
@@ -11,9 +11,9 @@ sqlmesh/cli/main.py,sha256=CM97CwI_DkrcE6TfJOwpiAaheYDr8WE7Tl-8asgV-Yw,33845
|
|
|
11
11
|
sqlmesh/cli/options.py,sha256=aC_L5WnMScKzMU55BD1Y-8R-QzqCkPSYrlCI-payHRA,1333
|
|
12
12
|
sqlmesh/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
13
|
sqlmesh/core/_typing.py,sha256=e1f7xO2aZCy38M_3VoDy-XI3shyd99-IFctgIFSgXWM,497
|
|
14
|
-
sqlmesh/core/console.py,sha256=
|
|
14
|
+
sqlmesh/core/console.py,sha256=DO8Ixq6tR4H6EIhpZRvObMclt5qZN9_IMt7OKkWsOZA,153591
|
|
15
15
|
sqlmesh/core/constants.py,sha256=pnQXs5cS61zqjS8szbA1IRYTZ_G5jIt8Qrw-gneQoqE,2594
|
|
16
|
-
sqlmesh/core/context.py,sha256=
|
|
16
|
+
sqlmesh/core/context.py,sha256=kbFRzNM-5HxrgmMrr_dgOhhCryWbVan4ASkS3QAF0hw,118099
|
|
17
17
|
sqlmesh/core/context_diff.py,sha256=TCzM8egBE35DskpouOB-GQM2Qf5iUHWbRgiB5yZ6tzg,21552
|
|
18
18
|
sqlmesh/core/dialect.py,sha256=93cXYVuNoZ9CvJNTVn2Yz7WDOdHINwVXflbn0eZ-p14,49764
|
|
19
19
|
sqlmesh/core/environment.py,sha256=bNj-JCG_l13dwrp--YPIuGNsuWLTMLcFA5sY8IazVK8,12917
|
|
@@ -119,10 +119,10 @@ sqlmesh/core/state_sync/db/utils.py,sha256=204OpT3ISFPCwWP9Aqq8nr6ETl24sbG5uOyo8
|
|
|
119
119
|
sqlmesh/core/state_sync/db/version.py,sha256=fl5g1tNMYu8O6WTN4J3bciwjGq0c5nZlRDlxxRKGU78,2210
|
|
120
120
|
sqlmesh/core/test/__init__.py,sha256=e83TJPwPRR_rAG29Y0OVbZb-5oWVBzz-_wrcd22Qk10,418
|
|
121
121
|
sqlmesh/core/test/context.py,sha256=Shmft5gRWCsk8Hropfmd4otWkLmw43r_uzrr6Azu5Ns,2220
|
|
122
|
-
sqlmesh/core/test/definition.py,sha256=
|
|
122
|
+
sqlmesh/core/test/definition.py,sha256=1V6EoaEpAWwiMpJIV630h-HcSTTQQuTZVhua7VWL4So,41301
|
|
123
123
|
sqlmesh/core/test/discovery.py,sha256=WXIwPidLoacAGHoT_3TVFFEKamG89nHNtaMsZ01uGjs,1188
|
|
124
|
-
sqlmesh/core/test/result.py,sha256=
|
|
125
|
-
sqlmesh/core/test/runner.py,sha256=
|
|
124
|
+
sqlmesh/core/test/result.py,sha256=IIyR4uMDDFJ9Hf57iwrHgZ_W_tLwnr0KSWKeBJ0Pc6k,3961
|
|
125
|
+
sqlmesh/core/test/runner.py,sha256=etwyGICHdPunx5-dUkC8oAA6khSQK5VpAztM4Yz9UgA,6159
|
|
126
126
|
sqlmesh/dbt/__init__.py,sha256=KUv-lW5sG9D2ceXAIzA4MLcjyhzq3E-7qJP4P_PH2EU,144
|
|
127
127
|
sqlmesh/dbt/adapter.py,sha256=pV8b8m_wqK3YJo_6D5b6cZBwgewFAPvAlvLO51UCBl4,16948
|
|
128
128
|
sqlmesh/dbt/basemodel.py,sha256=XuiK9-r5JWzvZGEk98Jt4ukR2exfMDsotGZqYx5xluc,12561
|
|
@@ -162,9 +162,9 @@ sqlmesh/integrations/llm.py,sha256=URgp3JKr06HYv6hGlJ7DQ9m3QCc41xRWK23HzmGpbDI,1
|
|
|
162
162
|
sqlmesh/integrations/slack.py,sha256=nxLxu5WztGbZH3JdqnzyPqmJUMfRdJ_49LQ7zR-u39Q,6801
|
|
163
163
|
sqlmesh/integrations/github/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
164
164
|
sqlmesh/integrations/github/cicd/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
165
|
-
sqlmesh/integrations/github/cicd/command.py,sha256=
|
|
165
|
+
sqlmesh/integrations/github/cicd/command.py,sha256=bQCLLzWS3pZQpATWADPQE_TpDZTHOt0qYwxcNm3St0Q,11557
|
|
166
166
|
sqlmesh/integrations/github/cicd/config.py,sha256=tRhMZaLf1p6AWXvi67n85Q-lptJNGt1Wk_Jr61ytpiM,2104
|
|
167
|
-
sqlmesh/integrations/github/cicd/controller.py,sha256=
|
|
167
|
+
sqlmesh/integrations/github/cicd/controller.py,sha256=CvyAjyOTSt9H8ojDLfz_4GDdpHe1hK4DUT7RL_yiybc,43790
|
|
168
168
|
sqlmesh/lsp/api.py,sha256=_lA_buSPZFIoLJuINbS53LbNOOaHp3orb0uFTE3aCqI,2328
|
|
169
169
|
sqlmesh/lsp/completions.py,sha256=Nt1XpFL_-QAIRM8BBBC6Ia6YG0hMZriiNJra7nGfD4Q,6676
|
|
170
170
|
sqlmesh/lsp/context.py,sha256=QGbNL0sifxzVJ7T1U5oWcJ5FXrOrLrVKt1jtH1qM_50,6099
|
|
@@ -279,10 +279,10 @@ sqlmesh/utils/migration.py,sha256=R7Ck6FX-LfcujGy7sgS0kDWlK-xAEBODr3gqIQ8gC08,83
|
|
|
279
279
|
sqlmesh/utils/pandas.py,sha256=FytTSLcRNtO0-YMvpoDBkkut3yoQJR9mBSKpif8lxug,2497
|
|
280
280
|
sqlmesh/utils/process.py,sha256=vpXcpJ1eIbivs7gZdwTKSJI7OS6kKBebnaBzz75ykvs,2428
|
|
281
281
|
sqlmesh/utils/pydantic.py,sha256=I898jAFqa3qU6lG2uPpRebVFncOUo8ZrLWM_R3K3DHU,11685
|
|
282
|
-
sqlmesh/utils/rich.py,sha256=
|
|
282
|
+
sqlmesh/utils/rich.py,sha256=BpB47G0-JcU41LkGkeGKroexj4NOV5fox4-GOIXPgIc,3545
|
|
283
283
|
sqlmesh/utils/windows.py,sha256=r7Yin4WEwbvH7sKA3hOJtRsclmscNuBeCklCfVPvtQs,611
|
|
284
284
|
sqlmesh/utils/yaml.py,sha256=np-WVcwmwsKghCgAj1OBBMsK1SXSRn4wR-H37cGiFr8,2282
|
|
285
|
-
sqlmesh-0.193.1.
|
|
285
|
+
sqlmesh-0.193.1.dev5.dist-info/licenses/LICENSE,sha256=OlMefUjgWJdULtf84BLW0AZZcY8DwdgQqb_1j2862j8,11346
|
|
286
286
|
web/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
287
287
|
web/client/dist/index.html,sha256=6S_GSRtPwXyr17YSqAWyZ2Prb2puyUa-UdLiZpFtwe4,5472
|
|
288
288
|
web/client/dist/assets/Audits-Dym5PKHr.js,sha256=UZFlxjPaLxv7b4C494dCxXDNY7918lWEkn2FghwDd4E,1041
|
|
@@ -379,7 +379,7 @@ web/client/dist/fonts/Sohne/sohne-mono-kraftig.woff2,sha256=DGsWMljiFHztzyb0Gg_T
|
|
|
379
379
|
web/client/src/workers/sqlglot/sqlglot.py,sha256=XAMrvqcRBa7vy8bHVayYbZNyWsf1lNSjVk_odtif-YY,1566
|
|
380
380
|
web/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
381
381
|
web/server/app.py,sha256=b6qEJ4btjaSm2kWwQUGV--S3VE8QW0Um7f4rBdaoB_M,59
|
|
382
|
-
web/server/console.py,sha256=
|
|
382
|
+
web/server/console.py,sha256=CJlhoNBVUj_U5h_88gQgDJNzLOe2FAQ1DcFeMdO3H18,14583
|
|
383
383
|
web/server/exceptions.py,sha256=I2PPpChwhDWawQENr5kHg6DjyRGeiJ6OWZsCzEZhVzs,1533
|
|
384
384
|
web/server/main.py,sha256=9PaDa8sigo1z0AxtI3sQ3fHmDqs6uXwP-_IvoMwkznw,3199
|
|
385
385
|
web/server/models.py,sha256=NUI2xqDcav20uuC2BZ9xDL2x0v16-gpVT1Sb-WHfAcg,15518
|
|
@@ -389,7 +389,7 @@ web/server/utils.py,sha256=bb6AOM3Nga-zihPjZvJeDL6sHqTPNC_SYjklsy8tAQA,4013
|
|
|
389
389
|
web/server/watcher.py,sha256=5biJ8gbHmGNUrooMFWRmw00XDVg0bMx6Hub571cs6TI,5246
|
|
390
390
|
web/server/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
391
391
|
web/server/api/endpoints/__init__.py,sha256=FcXl56-kPqbKgCZKOx0kjE50hn37q1m-nwOCxI9tRcc,932
|
|
392
|
-
web/server/api/endpoints/commands.py,sha256
|
|
392
|
+
web/server/api/endpoints/commands.py,sha256=Fj7FUxOuageasij_7-dVvOlJMsBnRmFBWYk_-t91UQI,8242
|
|
393
393
|
web/server/api/endpoints/directories.py,sha256=Aa8U4pTs3Saj5DnJ9TDnX9mZois-EXgpcBm5GpNNM_s,1822
|
|
394
394
|
web/server/api/endpoints/environments.py,sha256=g3YC6j4LLlG_wjJqn-Hrz78BWmgS7vhUhzaycsiez00,2124
|
|
395
395
|
web/server/api/endpoints/events.py,sha256=tjhOhGe9jt7L1mOLLapl5qTvicDRnP5lPZHSq9OnSVI,932
|
|
@@ -400,8 +400,8 @@ web/server/api/endpoints/models.py,sha256=ud-lRwriyI_OYg9tZchrBgeiXELiRTLNEodraf
|
|
|
400
400
|
web/server/api/endpoints/modules.py,sha256=8hqqgonGay_mJmpCw0IdbjsPhWlQH2VLdKAqha-myac,468
|
|
401
401
|
web/server/api/endpoints/plan.py,sha256=OYuQ_4dgbhG9ibTcX1a8mvYgH3ANxw9Fhbb4wg6QWqc,9222
|
|
402
402
|
web/server/api/endpoints/table_diff.py,sha256=SCy3-pr5x6bEEiOaZGCDbiHQhpgxwmnz3Acqa2Ie6jI,2090
|
|
403
|
-
sqlmesh-0.193.1.
|
|
404
|
-
sqlmesh-0.193.1.
|
|
405
|
-
sqlmesh-0.193.1.
|
|
406
|
-
sqlmesh-0.193.1.
|
|
407
|
-
sqlmesh-0.193.1.
|
|
403
|
+
sqlmesh-0.193.1.dev5.dist-info/METADATA,sha256=QdC-XqtGabVA8cl1VBNe5pd7iDZzRjS99x-1rVSHZys,26690
|
|
404
|
+
sqlmesh-0.193.1.dev5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
405
|
+
sqlmesh-0.193.1.dev5.dist-info/entry_points.txt,sha256=pIYKtg-u-uO7BK_Y0qrdmfyj0bYrcMc3Xiqr5WE3VFk,121
|
|
406
|
+
sqlmesh-0.193.1.dev5.dist-info/top_level.txt,sha256=bspjo5vjELQkuN0qKuTFIbsrUt3Zeo8D2mlKeHG1LdE,12
|
|
407
|
+
sqlmesh-0.193.1.dev5.dist-info/RECORD,,
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
-
import io
|
|
5
4
|
import typing as t
|
|
6
5
|
|
|
7
6
|
from fastapi import APIRouter, Body, Depends, Request, Response
|
|
@@ -142,12 +141,10 @@ async def test(
|
|
|
142
141
|
context: Context = Depends(get_loaded_context),
|
|
143
142
|
) -> models.TestResult:
|
|
144
143
|
"""Run one or all model tests"""
|
|
145
|
-
test_output = io.StringIO()
|
|
146
144
|
try:
|
|
147
145
|
result = context.test(
|
|
148
146
|
tests=[str(context.path / Path(test))] if test else None,
|
|
149
147
|
verbosity=verbosity,
|
|
150
|
-
stream=test_output,
|
|
151
148
|
)
|
|
152
149
|
except Exception:
|
|
153
150
|
import traceback
|
|
@@ -157,11 +154,7 @@ async def test(
|
|
|
157
154
|
message="Unable to run tests",
|
|
158
155
|
origin="API -> commands -> test",
|
|
159
156
|
)
|
|
160
|
-
context.console.log_test_results(
|
|
161
|
-
result,
|
|
162
|
-
test_output.getvalue(),
|
|
163
|
-
context.test_connection_config._engine_adapter.DIALECT,
|
|
164
|
-
)
|
|
157
|
+
context.console.log_test_results(result, context.test_connection_config._engine_adapter.DIALECT)
|
|
165
158
|
|
|
166
159
|
def _test_path(test: ModelTest) -> t.Optional[str]:
|
|
167
160
|
if path := test.path_relative_to(context.path):
|
web/server/console.py
CHANGED
|
@@ -3,7 +3,6 @@ from __future__ import annotations
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import json
|
|
5
5
|
import typing as t
|
|
6
|
-
import unittest
|
|
7
6
|
from fastapi.encoders import jsonable_encoder
|
|
8
7
|
from sse_starlette.sse import ServerSentEvent
|
|
9
8
|
from sqlmesh.core.snapshot.definition import Interval, Intervals
|
|
@@ -12,6 +11,7 @@ from sqlmesh.core.environment import EnvironmentNamingInfo
|
|
|
12
11
|
from sqlmesh.core.plan.definition import EvaluatablePlan
|
|
13
12
|
from sqlmesh.core.snapshot import Snapshot, SnapshotInfoLike, SnapshotTableInfo
|
|
14
13
|
from sqlmesh.core.test import ModelTest
|
|
14
|
+
from sqlmesh.core.test.result import ModelTextTestResult
|
|
15
15
|
from sqlmesh.utils.date import now_timestamp
|
|
16
16
|
from web.server import models
|
|
17
17
|
from web.server.exceptions import ApiException
|
|
@@ -258,9 +258,7 @@ class ApiConsole(TerminalConsole):
|
|
|
258
258
|
)
|
|
259
259
|
)
|
|
260
260
|
|
|
261
|
-
def log_test_results(
|
|
262
|
-
self, result: unittest.result.TestResult, output: t.Optional[str], target_dialect: str
|
|
263
|
-
) -> None:
|
|
261
|
+
def log_test_results(self, result: ModelTextTestResult, target_dialect: str) -> None:
|
|
264
262
|
if result.wasSuccessful():
|
|
265
263
|
self.log_event(
|
|
266
264
|
event=models.EventName.TESTS,
|
|
@@ -279,6 +277,8 @@ class ApiConsole(TerminalConsole):
|
|
|
279
277
|
details=details,
|
|
280
278
|
)
|
|
281
279
|
)
|
|
280
|
+
|
|
281
|
+
output = self._captured_unit_test_results(result)
|
|
282
282
|
self.log_event(
|
|
283
283
|
event=models.EventName.TESTS,
|
|
284
284
|
data=models.ReportTestsFailure(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|