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
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
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 @@ def log_destructive_change(
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 @@ def plan(
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 @@ def plan(
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 @@ def _prompt_promote(self, plan_builder: PlanBuilder) -> None:
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 @@ def log_test_results(
1972
1975
)
1973
1976
for test , _ in result .failures + result .errors :
1974
1977
if isinstance (test , ModelTest ):
1975
- self ._print (f"Failure Test: { test .model . name } { test .test_name } " )
1978
+ self ._print (f"Failure Test: { test .path } :: { test .test_name } " )
1976
1979
self ._print ("=" * divider_length )
1977
- self ._print (output )
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 @@ def show_linter_violations(
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"\n Ran { 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 @@ def radio_button_selected(change: t.Dict[str, t.Any]) -> None:
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 @@ def log_test_results(
2781
2843
h (
2782
2844
"span" ,
2783
2845
{"style" : {** shared_style , ** success_color }},
2784
- f"Successfully Ran { str (result .testsRun )} Tests Against { target_dialect } " ,
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 @@ def stop_promotion_progress(self, success: bool = True) -> None:
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 @@ def show_model_difference_summary(
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 :
0 commit comments