Skip to content

Commit 05b238c

Browse files
author
ci bot
committed
Merge branch 'aarthy/qa-fixes' into 'enterprise'
fix: bugs in test results diff and table freshness See merge request dkinternal/testgen/dataops-testgen!360
2 parents 47ea799 + a98d0b5 commit 05b238c

7 files changed

Lines changed: 133 additions & 64 deletions

File tree

testgen/commands/run_test_execution.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ def run_test_execution(test_suite_id: str | UUID, username: str | None = None, r
8282

8383
sql_generator = TestExecutionSQL(connection, table_group, test_run)
8484

85+
# Update the thresholds before retrieving the test definitions in the next steps
86+
LOG.info("Updating historic test thresholds")
87+
execute_db_queries([sql_generator.update_historic_thresholds()])
88+
8589
LOG.info("Retrieving active test definitions in test suite")
8690
test_defs = fetch_dict_from_db(*sql_generator.get_active_test_definitions())
8791
test_defs = [TestExecutionDef(**item) for item in test_defs]
@@ -100,9 +104,6 @@ def run_test_execution(test_suite_id: str | UUID, username: str | None = None, r
100104
)
101105

102106
if valid_test_defs:
103-
LOG.info("Updating historic test thresholds")
104-
execute_db_queries([sql_generator.update_historic_thresholds()])
105-
106107
column_types = {(col.schema_name, col.table_name, col.column_name): col.column_type for col in data_chars}
107108
for td in valid_test_defs:
108109
td.column_type = column_types.get((td.schema_name, td.table_name, td.column_name))

testgen/common/models/test_result.py

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@
22
from collections import defaultdict
33
from uuid import UUID, uuid4
44

5-
from sqlalchemy import Column, Enum, ForeignKey, String, or_, select
5+
from sqlalchemy import Boolean, Column, Enum, ForeignKey, String, and_, or_, select
66
from sqlalchemy.dialects import postgresql
77
from sqlalchemy.orm import aliased
8-
from sqlalchemy.sql.functions import coalesce
98

109
from testgen.common.models import get_current_session
1110
from testgen.common.models.entity import Entity
@@ -29,13 +28,17 @@ class TestResult(Entity):
2928

3029
test_suite_id: UUID = Column(postgresql.UUID(as_uuid=True), ForeignKey("test_suites.id"), nullable=False)
3130
test_run_id: UUID = Column(postgresql.UUID(as_uuid=True), ForeignKey("test_runs.id"), nullable=False)
31+
3232
test_definition_id: UUID = Column(postgresql.UUID(as_uuid=True), ForeignKey("test_definitions.id"), nullable=False)
3333
test_type: str = Column(String, ForeignKey("test_types.test_type"), nullable=False)
34+
auto_gen: bool = Column(Boolean)
35+
36+
schema_name: str = Column(String, nullable=False)
37+
table_name: str = Column(String)
38+
column_names: str = Column(String)
3439

3540
status: TestResultStatus = Column("result_status", Enum(TestResultStatus))
36-
message: str = Column("result_message", String, nullable=False)
37-
table_name: str = Column(String, nullable=False)
38-
column_names: str = Column(String, nullable=False)
41+
message: str = Column("result_message", String)
3942

4043
# Note: not all table columns are implemented by this entity
4144

@@ -44,9 +47,27 @@ def diff(cls, test_run_id_a: UUID, test_run_id_b: UUID) -> list[TestResultDiffTy
4447
alias_a = aliased(cls)
4548
alias_b = aliased(cls)
4649
query = select(
47-
alias_a.status, alias_b.status, coalesce(alias_a.test_definition_id, alias_b.test_definition_id),
50+
alias_a.status, alias_b.status, alias_b.test_definition_id,
4851
).join(
49-
alias_b, (alias_a.test_definition_id == alias_b.test_definition_id), isouter=True, full=True,
52+
alias_b,
53+
or_(
54+
and_(
55+
alias_a.auto_gen.is_(True),
56+
alias_b.auto_gen.is_(True),
57+
alias_a.test_suite_id == alias_b.test_suite_id,
58+
alias_a.schema_name == alias_b.schema_name,
59+
alias_a.table_name.isnot_distinct_from(alias_b.table_name),
60+
alias_a.column_names.isnot_distinct_from(alias_b.column_names),
61+
alias_a.test_type == alias_b.test_type,
62+
),
63+
and_(
64+
alias_a.auto_gen.isnot(True),
65+
alias_b.auto_gen.isnot(True),
66+
alias_a.test_definition_id == alias_b.test_definition_id,
67+
),
68+
),
69+
isouter=True,
70+
full=True,
5071
).where(
5172
or_(alias_a.test_run_id == test_run_id_a, alias_a.test_run_id.is_(None)),
5273
or_(alias_b.test_run_id == test_run_id_b, alias_b.test_run_id.is_(None)),

testgen/template/dbsetup_test_types/test_types_CUSTOM.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ test_types:
2323
default_parm_prompts: |-
2424
Custom SQL Query Returning Error Records
2525
default_parm_help: |-
26-
Query should return records indicating one or more errors. The test passes if no records are returned. Results of the query will be shown when you click `Review Source Data` for a failed test, so be sure to include enough data in your results to follow-up. \n\nA query can refer to any tables in the database. You must hard-code the schema or use `{DATA_SCHEMA}` to represent the schema defined for the Table Group.
26+
Query should return records indicating one or more errors. The test passes if no records are returned. Results of the query will be shown when you click `Review Source Data` for a failed test, so be sure to include enough data in your results to follow-up. A query can refer to any tables in the database. You must hard-code the schema or use `{DATA_SCHEMA}` to represent the schema defined for the Table Group.
2727
default_severity: Fail
2828
run_type: QUERY
2929
test_scope: custom
Lines changed: 68 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,71 @@
1-
WITH stats AS (
2-
SELECT
3-
d.id AS test_definition_id,
4-
COALESCE(
5-
MIN(r.result_signal) FILTER (WHERE d.history_calculation = 'Value'),
6-
MIN(r.result_signal::NUMERIC) FILTER (WHERE d.history_calculation = 'Minimum')::VARCHAR,
7-
MAX(r.result_signal::NUMERIC) FILTER (WHERE d.history_calculation = 'Maximum')::VARCHAR,
8-
SUM(r.result_signal::NUMERIC) FILTER (WHERE d.history_calculation = 'Sum')::VARCHAR,
9-
AVG(r.result_signal::NUMERIC) FILTER (WHERE d.history_calculation = 'Average')::VARCHAR
10-
) as calc_signal
11-
FROM test_definitions d
12-
INNER JOIN LATERAL (
13-
SELECT result_signal
14-
FROM test_results tr
15-
WHERE tr.test_definition_id = d.id
16-
ORDER BY tr.test_time DESC
17-
LIMIT CASE WHEN d.history_calculation = 'Value' THEN 1 ELSE d.history_lookback END
18-
) AS r ON TRUE
19-
WHERE d.test_suite_id = :TEST_SUITE_ID
20-
AND d.test_active = 'Y'
21-
AND d.history_lookback IS NOT NULL
22-
GROUP BY d.id, d.history_calculation, d.history_lookback
1+
WITH filtered_defs AS (
2+
-- Step 1: Filter definitions first to minimize join surface area
3+
SELECT id,
4+
test_suite_id,
5+
schema_name,
6+
table_name,
7+
column_name,
8+
test_type,
9+
history_calculation,
10+
CASE WHEN history_calculation = 'Value' THEN 1 ELSE COALESCE(history_lookback, 1) END AS lookback
11+
FROM test_definitions
12+
WHERE test_suite_id = :TEST_SUITE_ID
13+
AND test_active = 'Y'
14+
AND history_calculation IS NOT NULL
15+
AND history_lookback IS NOT NULL
16+
),
17+
normalized_results AS (
18+
-- Step 2: Normalize definition IDs for autogenerated tests
19+
SELECT CASE
20+
WHEN r.auto_gen THEN d.id
21+
ELSE r.test_definition_id
22+
END AS test_definition_id,
23+
r.test_time,
24+
r.result_signal
25+
FROM test_results r
26+
LEFT JOIN filtered_defs d ON r.auto_gen = TRUE
27+
AND r.test_suite_id = d.test_suite_id
28+
AND r.schema_name = d.schema_name
29+
AND r.table_name IS NOT DISTINCT FROM d.table_name
30+
AND r.column_names IS NOT DISTINCT FROM d.column_name
31+
AND r.test_type = d.test_type
32+
WHERE r.test_suite_id = :TEST_SUITE_ID
33+
),
34+
ranked_results AS (
35+
-- Step 3: Use a Window Function to get the N most recent results
36+
SELECT n.test_definition_id,
37+
n.result_signal,
38+
CASE
39+
WHEN n.result_signal ~ '^-?[0-9]*\.?[0-9]+$' THEN n.result_signal::NUMERIC
40+
ELSE NULL
41+
END AS signal_numeric,
42+
ROW_NUMBER() OVER (PARTITION BY n.test_definition_id ORDER BY n.test_time DESC) AS rank
43+
FROM normalized_results n
44+
WHERE n.test_definition_id IN (SELECT id FROM filtered_defs)
45+
),
46+
stats AS (
47+
-- Step 4: Aggregate only the rows within the lookback range
48+
SELECT d.id AS test_definition_id,
49+
d.history_calculation,
50+
MAX(CASE WHEN rr.rank = 1 THEN rr.result_signal END) AS val,
51+
MIN(rr.signal_numeric) AS min,
52+
MAX(rr.signal_numeric) AS max,
53+
SUM(rr.signal_numeric) AS sum,
54+
AVG(rr.signal_numeric) AS avg
55+
FROM filtered_defs d
56+
JOIN ranked_results rr ON d.id = rr.test_definition_id
57+
WHERE rr.rank <= d.lookback
58+
GROUP BY d.id,
59+
d.history_calculation
2360
)
2461
UPDATE test_definitions t
25-
SET baseline_value = s.calc_signal
62+
SET baseline_value = CASE
63+
WHEN s.history_calculation = 'Value' THEN s.val
64+
WHEN s.history_calculation = 'Minimum' THEN s.min::VARCHAR
65+
WHEN s.history_calculation = 'Maximum' THEN s.max::VARCHAR
66+
WHEN s.history_calculation = 'Sum' THEN s.sum::VARCHAR
67+
WHEN s.history_calculation = 'Average' THEN s.avg::VARCHAR
68+
ELSE NULL
69+
END
2670
FROM stats s
27-
WHERE t.id = s.test_definition_id;
71+
WHERE t.id = s.test_definition_id;

testgen/ui/components/frontend/js/pages/profiling_runs.js

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ const ProfilingRuns = (/** @type Properties */ props) => {
7272
Streamlit.setFrameHeight(1);
7373
window.testgen.isPage = true;
7474

75-
const columns = ['5%', '15%', '20%', '20%', '30%', '10%'];
75+
const columns = ['5%', '20%', '15%', '20%', '30%', '10%'];
7676
const userCanEdit = getValue(props.permissions)?.can_edit ?? false;
7777

7878
const pageIndex = van.state(0);
@@ -118,7 +118,7 @@ const ProfilingRuns = (/** @type Properties */ props) => {
118118
() => profilingRuns.val.length
119119
? div(
120120
div(
121-
{ class: 'table pb-0' },
121+
{ class: 'table pb-0', style: 'overflow-y: auto;' },
122122
() => {
123123
const selectedItems = profilingRuns.val.filter(i => selectedRuns[i.id]?.val ?? false);
124124
const someRunSelected = selectedItems.length > 0;
@@ -157,7 +157,7 @@ const ProfilingRuns = (/** @type Properties */ props) => {
157157
}
158158

159159
return span(
160-
{ style: `flex: ${columns[0]}` },
160+
{ style: `flex: 0 0 ${columns[0]}` },
161161
userCanEdit
162162
? Checkbox({
163163
checked: allSelected,
@@ -169,23 +169,23 @@ const ProfilingRuns = (/** @type Properties */ props) => {
169169
);
170170
},
171171
span(
172-
{ style: `flex: ${columns[1]}` },
172+
{ style: `flex: 0 0 ${columns[1]}` },
173173
'Start Time | Table Group',
174174
),
175175
span(
176-
{ style: `flex: ${columns[2]}` },
176+
{ style: `flex: 0 0 ${columns[2]}` },
177177
'Status | Duration',
178178
),
179179
span(
180-
{ style: `flex: ${columns[3]}` },
180+
{ style: `flex: 0 0 ${columns[3]}` },
181181
'Schema',
182182
),
183183
span(
184-
{ style: `flex: ${columns[4]}`, class: 'tg-profiling-runs--issues' },
184+
{ style: `flex: 0 0 ${columns[4]}`, class: 'tg-profiling-runs--issues' },
185185
'Hygiene Issues',
186186
),
187187
span(
188-
{ style: `flex: ${columns[5]}` },
188+
{ style: `flex: 0 0 ${columns[5]}` },
189189
'Profiling Score',
190190
),
191191
),
@@ -287,7 +287,7 @@ const ProfilingRunItem = (
287287
{ class: 'table-row flex-row', 'data-testid': 'profiling-run-item' },
288288
userCanEdit
289289
? div(
290-
{ style: `flex: ${columns[0]}; font-size: 16px;` },
290+
{ style: `flex: 0 0 ${columns[0]}; font-size: 16px;` },
291291
Checkbox({
292292
checked: selected,
293293
onChange: (checked) => selected.val = checked,
@@ -296,15 +296,15 @@ const ProfilingRunItem = (
296296
)
297297
: '',
298298
div(
299-
{ style: `flex: ${columns[1]}` },
299+
{ style: `flex: 0 0 ${columns[1]}; max-width: ${columns[1]}; word-wrap: break-word;` },
300300
div({ 'data-testid': 'profiling-run-item-starttime' }, formatTimestamp(item.profiling_starttime)),
301301
div(
302302
{ class: 'text-caption mt-1', 'data-testid': 'profiling-run-item-tablegroup' },
303303
item.table_groups_name,
304304
),
305305
),
306306
div(
307-
{ style: `flex: ${columns[2]}` },
307+
{ style: `flex: 0 0 ${columns[2]}; max-width: ${columns[2]};` },
308308
div(
309309
{ class: 'flex-row' },
310310
ProfilingRunStatus(item),
@@ -337,7 +337,7 @@ const ProfilingRunItem = (
337337
),
338338
),
339339
div(
340-
{ style: `flex: ${columns[3]}` },
340+
{ style: `flex: 0 0 ${columns[3]}; max-width: ${columns[3]};` },
341341
div({ 'data-testid': 'profiling-run-item-schema' }, item.table_group_schema),
342342
div(
343343
{
@@ -369,7 +369,7 @@ const ProfilingRunItem = (
369369
}) : null,
370370
),
371371
div(
372-
{ class: 'pr-3 tg-profiling-runs--issues', style: `flex: ${columns[4]}` },
372+
{ class: 'pr-3 tg-profiling-runs--issues', style: `flex: 0 0 ${columns[4]}; max-width: ${columns[4]};` },
373373
item.anomaly_ct ? SummaryCounts({
374374
items: [
375375
{ label: 'Definite', value: item.anomalies_definite_ct, color: 'red' },
@@ -389,7 +389,7 @@ const ProfilingRunItem = (
389389
}) : null,
390390
),
391391
div(
392-
{ style: `flex: ${columns[5]}; font-size: 16px;` },
392+
{ style: `flex: 0 0 ${columns[5]}; max-width: ${columns[5]}; font-size: 16px;` },
393393
item.column_ct && item.dq_score_profiling
394394
? item.dq_score_profiling
395395
: '--',

testgen/ui/views/test_definitions.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -483,11 +483,12 @@ def show_test_form(
483483
if dynamic_attributes_labels_raw:
484484
dynamic_attributes_labels = dynamic_attributes_labels_raw.split(",")
485485

486-
dynamic_attributes_help_raw = selected_test_type_row["default_parm_help"]
487-
if not dynamic_attributes_help_raw:
488-
dynamic_attributes_help_raw = "No help is available"
489486
# Split on pipe -- could contain commas
490-
dynamic_attributes_help = dynamic_attributes_help_raw.split("|")
487+
dynamic_attributes_help = (
488+
selected_test_type_row["default_parm_help"].split("|")
489+
if selected_test_type_row["default_parm_help"]
490+
else None
491+
)
491492

492493
if mode == "edit":
493494
st.text_input(label="Test Type", value=test_type_display, disabled=True),
@@ -700,7 +701,7 @@ def render_dynamic_attribute(attribute: str, container: DeltaGenerator):
700701
help_text = (
701702
dynamic_attributes_help[index]
702703
if dynamic_attributes_help and len(dynamic_attributes_help) > index
703-
else "Help text is not available."
704+
else None
704705
)
705706

706707
if attribute == "custom_query":
@@ -710,7 +711,7 @@ def render_dynamic_attribute(attribute: str, container: DeltaGenerator):
710711
elif test_type == "CUSTOM":
711712
custom_query_placeholder = "EXAMPLE: SELECT product, SUM(qty_sold) as sum_sold, SUM(qty_shipped) as qty_shipped \n FROM {DATA_SCHEMA}.sales_history \n GROUP BY product \n HAVING SUM(qty_shipped) > SUM(qty_sold)"
712713

713-
test_definition[attribute] = st.text_area(
714+
test_definition[attribute] = container.text_area(
714715
label=label_text,
715716
value=custom_query,
716717
placeholder=custom_query_placeholder,
@@ -766,6 +767,7 @@ def render_dynamic_attribute(attribute: str, container: DeltaGenerator):
766767
if test_scope != "tablegroup":
767768
st.divider()
768769

770+
mid_container = st.container()
769771
mid_left_column, mid_right_column = st.columns([0.5, 0.5])
770772

771773
if has_match_attributes:
@@ -775,7 +777,7 @@ def render_dynamic_attribute(attribute: str, container: DeltaGenerator):
775777
render_dynamic_attribute(f"match_{attribute}", mid_right_column)
776778

777779
if "custom_query" in dynamic_attributes:
778-
render_dynamic_attribute("custom_query", mid_left_column)
780+
render_dynamic_attribute("custom_query", mid_container)
779781

780782
total_length = len(leftover_attributes)
781783
half_length = round(total_length / 2)
@@ -953,13 +955,13 @@ def validate_form(test_scope, test_definition, column_name_label):
953955

954956
def prompt_for_test_type():
955957

956-
col0, col1, col2, col3, col4, col5 = st.columns([0.1, 0.2, 0.2, 0.2, 0.2, 0.1])
958+
col0, col1, col2, col3, col4 = st.columns([0.2, 0.2, 0.2, 0.2, 0.2])
957959
col0.write("Show Types")
958960

959-
include_referential=col1.checkbox(":green[⧉] Referential", True),
960-
include_table=col2.checkbox(":green[⊞] Table", True),
961-
include_column=col3.checkbox(":green[≣] Column", True),
962-
include_custom=col4.checkbox(":green[⛭] Custom", True),
961+
include_referential=col1.checkbox(":green[⧉] Referential", True)
962+
include_table=col2.checkbox(":green[⊞] Table", True)
963+
include_column=col3.checkbox(":green[≣] Column", True)
964+
include_custom=col4.checkbox(":green[⛭] Custom", True)
963965
# always exclude tablegroup scopes from showing
964966
include_all = not any([include_referential, include_table, include_column, include_custom])
965967

testgen/ui/views/test_results.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -600,7 +600,7 @@ def render_selected_details(
600600
if selected_item["measure_uom_description"]:
601601
st.caption(selected_item["measure_uom_description"])
602602
if selected_item["result_message"]:
603-
st.caption(selected_item["result_message"])
603+
st.caption(selected_item["result_message"].replace("*", "\\*"))
604604
fm.render_grid_select(dfh, show_hist_columns, selection_mode="disabled", key="test_history")
605605
with pg_col2:
606606
ut_tab1, ut_tab2 = st.tabs(["History", "Test Definition"])
@@ -809,8 +809,9 @@ def source_data_dialog(selected_row):
809809
st.markdown("#### Test Parameters")
810810
testgen.caption(selected_row["input_parameters"], styles="max-height: 75px; overflow: auto;")
811811

812-
st.markdown("#### Result Detail")
813-
st.caption(selected_row["result_message"])
812+
if selected_row["result_message"]:
813+
st.markdown("#### Result Detail")
814+
st.caption(selected_row["result_message"].replace("*", "\\*"))
814815

815816
st.markdown("#### SQL Query")
816817
if selected_row["test_type"] == "CUSTOM":

0 commit comments

Comments
 (0)