Skip to content

Commit 4cb8804

Browse files
Improve snow dcm test and refresh output
- refresh: add a green/red summary line ("N Dynamic Tables refreshed successfully." / "N failures.") and append the === divider after output - test: switch summary wording to "Ran T data quality tests: P passed, F failed." with correct pluralisation - base.py: append the === divider after every DCM reporter output so command results are visually separated in the terminal - Consolidate shared kind-table helpers and success-contract logic across reporters (test + refresh) Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent db1e79e commit 4cb8804

8 files changed

Lines changed: 196 additions & 55 deletions

File tree

src/snowflake/cli/_plugins/dcm/reporters/base.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@
3434

3535
T = TypeVar("T")
3636

37+
# Visual divider printed after every DCM command's reporter output so the
38+
# command's final line is clearly separated from the next shell prompt
39+
# (or from a CliError box on failure).
40+
_REPORT_SEPARATOR = "=" * 80
41+
3742

3843
class Reporter(ABC, Generic[T]):
3944
def __init__(self, save_output: bool = False) -> None:
@@ -78,6 +83,13 @@ def print_summary(self) -> None:
7883
cli_console.styled_message(renderable.plain, style=renderable.style)
7984
cli_console.styled_message("\n")
8085

86+
def print_separator(self) -> None:
87+
"""Print a divider line that marks the end of the command's output.
88+
89+
Automatically muted in JSON/CSV output formats (cf. `cli_console`)."""
90+
cli_console.styled_message(_REPORT_SEPARATOR, style="dim")
91+
cli_console.styled_message("\n")
92+
8193
def _try_save_response(self, result_json: Dict[str, Any]) -> None:
8294
"""Save raw JSON response if save_output is enabled and raw data is available."""
8395
if self.save_output:
@@ -96,10 +108,15 @@ def process_payload(self, result_json: Dict[str, Any]) -> None:
96108
self.print_renderables(parsed_data)
97109
if self._is_success():
98110
self.print_summary()
111+
self.print_separator()
99112
else:
100113
message = "".join(
101114
renderable.plain for renderable in self._generate_summary_renderables()
102115
)
116+
# Print the separator before raising so the user still sees a
117+
# clean divider between the partial body output and the error
118+
# box that the CLI framework renders for the CliError.
119+
self.print_separator()
103120
raise CliError(message)
104121

105122
@staticmethod

src/snowflake/cli/_plugins/dcm/reporters/refresh.py

Lines changed: 46 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from enum import Enum
1717
from typing import Any, Dict, Iterator, List, Optional
1818

19+
import typer
1920
from pydantic import BaseModel, Field, ValidationError
2021
from rich.text import Text
2122
from snowflake.cli._plugins.dcm import styles
@@ -152,11 +153,16 @@ class RefreshReporter(Reporter[RefreshRow]):
152153
class Summary:
153154
up_to_date: int = 0
154155
refreshed: int = 0
155-
unknown: int = 0
156+
failed: int = 0
157+
158+
@property
159+
def successful(self) -> int:
160+
"""Tables that completed without error (refreshed or already up-to-date)."""
161+
return self.up_to_date + self.refreshed
156162

157163
@property
158164
def total(self):
159-
return self.up_to_date + self.refreshed + self.unknown
165+
return self.successful + self.failed
160166

161167
def __init__(self, save_output: bool = False):
162168
super().__init__(save_output=save_output)
@@ -187,7 +193,7 @@ def parse_data(self, data: List[RefreshTableResult]) -> Iterator[RefreshRow]:
187193
elif parsed.status == RefreshStatus.REFRESHED:
188194
self._summary.refreshed += 1
189195
else:
190-
self._summary.unknown += 1
196+
self._summary.failed += 1
191197
yield parsed
192198

193199
def print_renderables(self, data: Iterator[RefreshRow]) -> None:
@@ -208,25 +214,44 @@ def print_renderables(self, data: Iterator[RefreshRow]) -> None:
208214
cli_console.styled_message("\n")
209215

210216
def _generate_summary_renderables(self) -> List[Text]:
211-
total = self._summary.total
212-
if total == 0:
217+
if self._summary.total == 0:
213218
return [Text("No dynamic tables found in the project.")]
214219

215-
parts = []
216-
if (refreshed := self._summary.refreshed) > 0:
217-
parts.append(f"{refreshed} refreshed")
218-
if (up_to_date := self._summary.up_to_date) > 0:
219-
parts.append(f"{up_to_date} up-to-date")
220-
if (unknown := self._summary.unknown) > 0:
221-
parts.append(f"{unknown} unknown")
222-
223-
summary = ""
224-
for i, part in enumerate(parts):
225-
if i > 0:
226-
summary += ", "
227-
summary += part
228-
summary += "."
229-
return [Text(summary)]
220+
renderables: List[Text] = []
221+
successful = self._summary.successful
222+
failed = self._summary.failed
223+
224+
if successful > 0:
225+
tables_word = "Dynamic Table" if successful == 1 else "Dynamic Tables"
226+
renderables.append(
227+
Text(
228+
f"{successful} {tables_word} refreshed successfully.",
229+
styles.PASS_STYLE,
230+
)
231+
)
232+
233+
if failed > 0:
234+
refreshes_word = "Refresh" if failed == 1 else "Refreshes"
235+
if renderables:
236+
renderables.append(Text("\n"))
237+
renderables.append(
238+
Text(f"{failed} {refreshes_word} failed.", styles.FAIL_STYLE)
239+
)
240+
241+
return renderables
230242

231243
def _is_success(self) -> bool:
232-
return self._summary.unknown == 0
244+
# Always treat the run as "success" from the base reporter's perspective
245+
# so that `print_summary()` is always called — we want the styled
246+
# green/red summary rendered above the divider on every run (cf.
247+
# AnalyzeErrorsReporter). Exit code is set separately in
248+
# ``process_payload``.
249+
return True
250+
251+
def process_payload(self, result_json: Dict[str, Any]) -> None:
252+
super().process_payload(result_json)
253+
if self._summary.failed > 0:
254+
# Failures fail the command, but the styled summary is already on
255+
# screen — exit silently so we don't double-render the message
256+
# inside an "Error" box.
257+
raise typer.Exit(code=1)

src/snowflake/cli/_plugins/dcm/reporters/test.py

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from enum import Enum
1717
from typing import Any, Dict, Iterator, List, Optional
1818

19+
import typer
1920
from rich.text import Text
2021
from snowflake.cli._plugins.dcm import styles
2122
from snowflake.cli._plugins.dcm.reporters.base import Reporter, cli_console
@@ -151,18 +152,47 @@ def _generate_summary_renderables(self) -> List[Text]:
151152
if total == 0:
152153
return [Text("No expectations found in the project.")]
153154

154-
result = [
155-
(Text(f"{self._summary.passed} passed", styles.PASS_STYLE)),
156-
(Text(", ")),
157-
(Text(f"{self._summary.failed} failed", styles.FAIL_STYLE)),
155+
test_word = "test" if total == 1 else "tests"
156+
# The trailing noun (``expectation`` / ``expectations``) attaches to
157+
# the last visible bucket — ``unknown`` if present, otherwise
158+
# ``failed`` — and pluralizes with that bucket's count.
159+
trailing_count = (
160+
self._summary.unknown if self._summary.unknown > 0 else self._summary.failed
161+
)
162+
noun = "expectation" if trailing_count == 1 else "expectations"
163+
164+
result: List[Text] = [
165+
Text("Ran "),
166+
Text(f"{total}", styles.BOLD_STYLE),
167+
Text(f" data quality {test_word}: "),
168+
Text(f"{self._summary.passed} passed", styles.PASS_STYLE),
169+
Text(", "),
158170
]
159171
if self._summary.unknown > 0:
172+
result.append(Text(f"{self._summary.failed} failed", styles.FAIL_STYLE))
160173
result.append(Text(", "))
161-
result.append(Text(f"{self._summary.unknown} unknown", styles.FAIL_STYLE))
162-
result.append(Text(" out of "))
163-
result.append(Text(f"{total}", styles.BOLD_STYLE))
164-
result.append(Text(" total."))
174+
result.append(
175+
Text(f"{self._summary.unknown} unknown {noun}", styles.FAIL_STYLE)
176+
)
177+
else:
178+
result.append(
179+
Text(f"{self._summary.failed} failed {noun}", styles.FAIL_STYLE)
180+
)
181+
result.append(Text("."))
165182
return result
166183

167184
def _is_success(self) -> bool:
168-
return self._summary.failed + self._summary.unknown == 0
185+
# Always treat the run as "success" from the base reporter's
186+
# perspective so that ``print_summary()`` is always called — the
187+
# styled "✓ N passed, ✗ M failed" summary should render on every run
188+
# (cf. ``AnalyzeErrorsReporter`` / ``RefreshReporter``). Exit code is
189+
# set separately by ``process_payload`` below.
190+
return True
191+
192+
def process_payload(self, result_json: Dict[str, Any]) -> None:
193+
super().process_payload(result_json)
194+
if self._summary.failed + self._summary.unknown > 0:
195+
# Failures fail the command, but the styled summary is already
196+
# on screen — exit silently so we don't double-render the
197+
# message inside an "Error" box.
198+
raise typer.Exit(code=1)

tests/dcm/__snapshots__/test_commands.ambr

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,56 +2,67 @@
22
# name: TestDCMRefresh.test_refresh_with_fresh_tables
33
'''
44

5+
56
UP-TO-DATE 0 0 JW_DCM_TESTALL.ANALYTICS.DYNAMIC_EMPLOYEES
67

7-
1 up-to-date.
8+
1 Dynamic Table refreshed successfully.
9+
================================================================================
810

911
'''
1012
# ---
1113
# name: TestDCMRefresh.test_refresh_with_no_dynamic_tables
1214
'''
1315

1416

17+
1518
No dynamic tables found in the project.
19+
================================================================================
1620

1721
'''
1822
# ---
1923
# name: TestDCMRefresh.test_refresh_with_outdated_tables
2024
'''
2125

26+
2227
REFRESHED +12.3k -999B JW_DCM_TESTALL.ANALYTICS.DYNAMIC_EMPLOYEES
2328

24-
1 refreshed.
29+
1 Dynamic Table refreshed successfully.
30+
================================================================================
2531

2632
'''
2733
# ---
2834
# name: TestDCMTest.test_test_all_passing
2935
'''
3036

37+
3138
✓ PASS DB.SCHEMA.EMPLOYEES (ROW_COUNT_CHECK)
3239
✓ PASS DB.SCHEMA.ORDERS (NULL_CHECK)
3340

34-
2 passed, 0 failed out of 2 total.
41+
Ran 2 data quality tests: 2 passed, 0 failed expectations.
42+
================================================================================
3543

3644
'''
3745
# ---
3846
# name: TestDCMTest.test_test_no_expectations
3947
'''
4048

4149

50+
4251
No expectations found in the project.
52+
================================================================================
4353

4454
'''
4555
# ---
4656
# name: TestDCMTest.test_test_with_failures
4757
'''
4858

59+
4960
✓ PASS DB.SCHEMA.EMPLOYEES (ROW_COUNT_CHECK)
5061
✗ FAIL DB.SCHEMA.ORDERS (NULL_CHECK)
5162
└─ Expected: = 0, Got: 15 (Metric: null_count)
52-
+- Error ----------------------------------------------------------------------+
53-
| 1 passed, 1 failed out of 2 total. |
54-
+------------------------------------------------------------------------------+
63+
64+
Ran 2 data quality tests: 1 passed, 1 failed expectation.
65+
================================================================================
5566

5667
'''
5768
# ---

tests/dcm/test_reporters/__snapshots__/test_refresh_reporter.ambr

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
'''
44
UP-TO-DATE 0 0 DB.SCHEMA.RED_TABLE
55

6-
1 up-to-date.
6+
1 Dynamic Table refreshed successfully.
7+
================================================================================
78

89
'''
910
# ---
@@ -18,23 +19,38 @@
1819
REFRESHED +1.5B -999M DB.SCHEMA.BILLIONS
1920
REFRESHED +2.5T -100B DB.SCHEMA.TRILLIONS
2021

21-
2 refreshed.
22+
2 Dynamic Tables refreshed successfully.
23+
================================================================================
2224

2325
'''
2426
# ---
2527
# name: TestRefreshReporter.test_missing_statistics
2628
'''
2729
UNKNOWN DB.SCHEMA.NO_STATS
2830

29-
1 unknown.
31+
1 Refresh failed.
32+
================================================================================
3033

3134
'''
3235
# ---
3336
# name: TestRefreshReporter.test_missing_table_name
3437
'''
3538
REFRESHED +100 0 UNKNOWN
3639

37-
1 refreshed.
40+
1 Dynamic Table refreshed successfully.
41+
================================================================================
42+
43+
'''
44+
# ---
45+
# name: TestRefreshReporter.test_mixed_success_and_failure
46+
'''
47+
REFRESHED +10 0 DB.SCHEMA.OK_REFRESHED
48+
UP-TO-DATE 0 0 DB.SCHEMA.OK_UP_TO_DATE
49+
UNKNOWN DB.SCHEMA.FAILED
50+
51+
2 Dynamic Tables refreshed successfully.
52+
1 Refresh failed.
53+
================================================================================
3854

3955
'''
4056
# ---
@@ -45,46 +61,52 @@
4561
UP-TO-DATE 0 0 DB.SCHEMA.TABLE_C
4662
REFRESHED +999k -500 DB.SCHEMA.TABLE_D
4763

48-
2 refreshed, 2 up-to-date.
64+
4 Dynamic Tables refreshed successfully.
65+
================================================================================
4966

5067
'''
5168
# ---
5269
# name: TestRefreshReporter.test_no_dynamic_tables
5370
'''
5471

5572
No dynamic tables found in the project.
73+
================================================================================
5674

5775
'''
5876
# ---
5977
# name: TestRefreshReporter.test_only_deletions
6078
'''
6179
REFRESHED 0 -3k DB.SCHEMA.DELETES_ONLY
6280

63-
1 refreshed.
81+
1 Dynamic Table refreshed successfully.
82+
================================================================================
6483

6584
'''
6685
# ---
6786
# name: TestRefreshReporter.test_only_insertions
6887
'''
6988
REFRESHED +5k 0 DB.SCHEMA.INSERTS_ONLY
7089

71-
1 refreshed.
90+
1 Dynamic Table refreshed successfully.
91+
================================================================================
7292

7393
'''
7494
# ---
7595
# name: TestRefreshReporter.test_single_refreshed_table
7696
'''
7797
REFRESHED +1.5k -200 DB.SCHEMA.CUSTOMERS
7898

79-
1 refreshed.
99+
1 Dynamic Table refreshed successfully.
100+
================================================================================
80101

81102
'''
82103
# ---
83104
# name: TestRefreshReporter.test_single_up_to_date_table
84105
'''
85106
UP-TO-DATE 0 0 DB.SCHEMA.ORDERS
86107

87-
1 up-to-date.
108+
1 Dynamic Table refreshed successfully.
109+
================================================================================
88110

89111
'''
90112
# ---

0 commit comments

Comments
 (0)