Spaces:
Running
Running
mike boone commited on
Commit Β·
ef5b239
1
Parent(s): 7a5c5fe
fix: identify e2e runs from final model URL
Browse files- tests/e2e_quality.py +69 -90
tests/e2e_quality.py
CHANGED
|
@@ -437,8 +437,8 @@ def submit_job(page: Page, test_case: dict):
|
|
| 437 |
url_el.fill(test_case["company_url"])
|
| 438 |
page.wait_for_timeout(300)
|
| 439 |
|
| 440 |
-
# Generate a unique tag for this test case
|
| 441 |
-
#
|
| 442 |
test_tag = f"TR-{uuid.uuid4().hex[:8].upper()}"
|
| 443 |
test_case["_test_tag"] = test_tag # store so run_single_test can pass it to diagnostics
|
| 444 |
|
|
@@ -504,17 +504,28 @@ def pipeline_finished(stages: dict) -> bool:
|
|
| 504 |
def extract_run_context(page: Page) -> dict:
|
| 505 |
"""
|
| 506 |
After completion, find model and liveboard URLs in the page.
|
| 507 |
-
Uses full HTML
|
|
|
|
| 508 |
"""
|
| 509 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 510 |
|
| 511 |
model_match = re.search(r'(https://[^\s"\'<>#]+)/#/data/tables/([a-f0-9-]{36})', body)
|
| 512 |
lb_match = re.search(r'(https://[^\s"\'<>#]+)/#/pinboard/([a-f0-9-]{36})', body)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 513 |
|
| 514 |
return {
|
| 515 |
-
"ts_base_url":
|
| 516 |
"model_guid": model_match.group(2) if model_match else None,
|
| 517 |
"liveboard_guid": lb_match.group(2) if lb_match else None,
|
|
|
|
| 518 |
}
|
| 519 |
|
| 520 |
|
|
@@ -610,6 +621,26 @@ def extract_db_schema(model_tml_str: str) -> tuple:
|
|
| 610 |
return "", ""
|
| 611 |
|
| 612 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 613 |
def get_snowflake_sample(db: str, schema: str) -> str:
|
| 614 |
try:
|
| 615 |
from snowflake_auth import get_snowflake_connection
|
|
@@ -804,7 +835,7 @@ def run_ai_grading(run_context: dict, company: str, vertical: str, line: str, fu
|
|
| 804 |
db, schema = extract_db_schema(model_tml)
|
| 805 |
if (not db or not schema) and schema_override:
|
| 806 |
db, schema = "DEMOBUILD", schema_override
|
| 807 |
-
print(f" βΉοΈ Using schema from
|
| 808 |
sample = get_snowflake_sample(db, schema) if db and schema else "Could not determine db/schema"
|
| 809 |
print(" π€ Grading data quality...")
|
| 810 |
dg = grade_data_quality(company, vertical, line, function, model_tml, sample)
|
|
@@ -1140,8 +1171,8 @@ def compute_grade(score: float, config: dict) -> str:
|
|
| 1140 |
def fetch_run_diagnostics(start_time: float, company: str = "", test_tag: str = "") -> dict:
|
| 1141 |
"""
|
| 1142 |
Query session_logs for entries by testrunner after start_time.
|
| 1143 |
-
|
| 1144 |
-
|
| 1145 |
"""
|
| 1146 |
try:
|
| 1147 |
from supabase_client import SupabaseSettings
|
|
@@ -1169,57 +1200,16 @@ def fetch_run_diagnostics(start_time: float, company: str = "", test_tag: str =
|
|
| 1169 |
sid = log["session_id"]
|
| 1170 |
sessions.setdefault(sid, []).append(log)
|
| 1171 |
|
| 1172 |
-
# Derive expected 3-letter company prefix from company name/URL
|
| 1173 |
-
# e.g. "delta.com" β "DEL", "Wells Fargo" β "WEL"
|
| 1174 |
-
company_prefix = ""
|
| 1175 |
-
if company:
|
| 1176 |
-
name = company.lower().replace(".com", "").replace(".", "").replace(" ", "")
|
| 1177 |
-
company_prefix = name[:3].upper()
|
| 1178 |
-
|
| 1179 |
-
def schema_for_session(session_entries):
|
| 1180 |
-
for l in session_entries:
|
| 1181 |
-
schema = (l.get("meta") or {}).get("schema", "")
|
| 1182 |
-
if schema:
|
| 1183 |
-
return schema
|
| 1184 |
-
return ""
|
| 1185 |
-
|
| 1186 |
-
# 1. Exact tag match β most reliable, no guessing
|
| 1187 |
if test_tag:
|
| 1188 |
tag_matching = {
|
| 1189 |
sid: entries for sid, entries in sessions.items()
|
| 1190 |
if any((l.get("meta") or {}).get("test_tag") == test_tag for l in entries)
|
| 1191 |
}
|
| 1192 |
-
if tag_matching:
|
| 1193 |
-
|
| 1194 |
-
|
| 1195 |
-
# Tag not found yet (pipeline may still be starting) β fall through to prefix
|
| 1196 |
-
if company_prefix:
|
| 1197 |
-
matching = {
|
| 1198 |
-
sid: entries for sid, entries in sessions.items()
|
| 1199 |
-
if company_prefix in schema_for_session(entries).upper()
|
| 1200 |
-
}
|
| 1201 |
-
if matching:
|
| 1202 |
-
session_logs = max(matching.values(), key=len)
|
| 1203 |
-
else:
|
| 1204 |
-
session_logs = max(sessions.values(), key=len)
|
| 1205 |
-
schema = schema_for_session(session_logs)
|
| 1206 |
-
print(f" β οΈ Tag '{test_tag}' not found, prefix '{company_prefix}' no match β using best guess (schema: {schema})")
|
| 1207 |
-
else:
|
| 1208 |
-
session_logs = max(sessions.values(), key=len)
|
| 1209 |
-
# 2. Company prefix match β fallback when no tag
|
| 1210 |
-
elif company_prefix:
|
| 1211 |
-
matching = {
|
| 1212 |
-
sid: entries for sid, entries in sessions.items()
|
| 1213 |
-
if company_prefix in schema_for_session(entries).upper()
|
| 1214 |
-
}
|
| 1215 |
-
if matching:
|
| 1216 |
-
session_logs = max(matching.values(), key=len)
|
| 1217 |
-
else:
|
| 1218 |
-
session_logs = max(sessions.values(), key=len)
|
| 1219 |
-
schema = schema_for_session(session_logs)
|
| 1220 |
-
print(f" β οΈ No session matched company prefix '{company_prefix}' β using best guess (schema: {schema})")
|
| 1221 |
else:
|
| 1222 |
-
|
| 1223 |
|
| 1224 |
# Summarise
|
| 1225 |
completed = [l["stage"] for l in session_logs if "completed" in (l.get("event") or "")]
|
|
@@ -1277,29 +1267,18 @@ def fetch_run_diagnostics(start_time: float, company: str = "", test_tag: str =
|
|
| 1277 |
def check_snowflake_schema(company: str, start_time: float, schema_override: str = None) -> dict:
|
| 1278 |
"""
|
| 1279 |
Get row counts for the Snowflake schema created during this test run.
|
| 1280 |
-
|
| 1281 |
-
|
| 1282 |
"""
|
| 1283 |
try:
|
| 1284 |
from snowflake_auth import get_snowflake_connection
|
| 1285 |
-
from datetime import datetime
|
| 1286 |
|
|
|
|
|
|
|
| 1287 |
conn = get_snowflake_connection()
|
| 1288 |
cursor = conn.cursor()
|
| 1289 |
|
| 1290 |
-
|
| 1291 |
-
schema = schema_override
|
| 1292 |
-
else:
|
| 1293 |
-
# Fall back: guess from company prefix + date
|
| 1294 |
-
prefix = company[:3].upper()
|
| 1295 |
-
date_str = datetime.fromtimestamp(start_time).strftime("%m%d")
|
| 1296 |
-
cursor.execute("SHOW SCHEMAS IN DATABASE DEMOBUILD")
|
| 1297 |
-
all_schemas = [row[1] for row in cursor.fetchall()]
|
| 1298 |
-
matching = [s for s in all_schemas if s.startswith(prefix) and date_str in s]
|
| 1299 |
-
if not matching:
|
| 1300 |
-
cursor.close(); conn.close()
|
| 1301 |
-
return {"found": False, "prefix": prefix, "date": date_str}
|
| 1302 |
-
schema = sorted(matching)[-1] # most recent
|
| 1303 |
cursor.execute(f'SHOW TABLES IN SCHEMA DEMOBUILD."{schema}"')
|
| 1304 |
tables = cursor.fetchall()
|
| 1305 |
|
|
@@ -1344,7 +1323,7 @@ def print_diagnostics(diag: dict, sf: dict):
|
|
| 1344 |
for t in sf["tables"]:
|
| 1345 |
print(f" {t['table']}: {t['rows']} rows")
|
| 1346 |
elif sf:
|
| 1347 |
-
print(f" Snowflake:
|
| 1348 |
print(" βββββββββββββββββββββββββββββββββββββββββββββββββββββ")
|
| 1349 |
|
| 1350 |
|
|
@@ -1432,30 +1411,29 @@ def run_single_test(page: Page, test_case: dict, config: dict) -> dict:
|
|
| 1432 |
|
| 1433 |
result["duration_seconds"] = round(time.time() - start)
|
| 1434 |
|
| 1435 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1436 |
diag = fetch_run_diagnostics(start, company=result.get("company", ""),
|
| 1437 |
test_tag=test_case.get("_test_tag", ""))
|
| 1438 |
result["diagnostics"] = diag
|
|
|
|
|
|
|
| 1439 |
|
| 1440 |
-
|
| 1441 |
-
|
| 1442 |
-
|
| 1443 |
-
"
|
| 1444 |
-
"liveboard_guid": diag.get("liveboard_guid"),
|
| 1445 |
-
}
|
| 1446 |
-
if log_ctx["model_guid"]:
|
| 1447 |
-
result["run_context"] = log_ctx
|
| 1448 |
-
print(f" π GUIDs from session_logs: model={log_ctx['model_guid'][:8]}β¦ lb={log_ctx['liveboard_guid'] and log_ctx['liveboard_guid'][:8] or 'none'}β¦")
|
| 1449 |
else:
|
| 1450 |
-
|
| 1451 |
-
result["run_context"] = page_ctx
|
| 1452 |
-
if page_ctx.get("model_guid"):
|
| 1453 |
-
print(f" π GUIDs from page HTML (log miss): model={page_ctx['model_guid'][:8]}β¦")
|
| 1454 |
-
else:
|
| 1455 |
-
print(" β οΈ No model GUID found in logs or page β AI grading will be skipped")
|
| 1456 |
-
|
| 1457 |
-
# Use the real schema name from session_logs; fall back to prefix-guessing only if missing
|
| 1458 |
-
known_schema = diag.get("snowflake_schema") if diag.get("found") else None
|
| 1459 |
sf = check_snowflake_schema(result["company"], start, schema_override=known_schema)
|
| 1460 |
result["snowflake_check"] = sf
|
| 1461 |
if sf.get("found"):
|
|
@@ -1463,7 +1441,7 @@ def run_single_test(page: Page, test_case: dict, config: dict) -> dict:
|
|
| 1463 |
for t in sf["tables"]:
|
| 1464 |
print(f" {t['table']}: {t['rows']} rows")
|
| 1465 |
else:
|
| 1466 |
-
print(f" π¦ Snowflake: schema not
|
| 1467 |
|
| 1468 |
# Reconcile stages with session_logs β catches cases where the monitor bailed early
|
| 1469 |
# but the pipeline actually completed (e.g. slow DDL that took >20 min)
|
|
@@ -1488,6 +1466,7 @@ def run_single_test(page: Page, test_case: dict, config: dict) -> dict:
|
|
| 1488 |
ag = run_ai_grading(
|
| 1489 |
result["run_context"],
|
| 1490 |
result["company"], result["vertical"], result["line"], result["function"],
|
|
|
|
| 1491 |
)
|
| 1492 |
else:
|
| 1493 |
ag["grading_errors"].append("Skipped β no model GUID (pipeline did not complete)")
|
|
|
|
| 437 |
url_el.fill(test_case["company_url"])
|
| 438 |
page.wait_for_timeout(300)
|
| 439 |
|
| 440 |
+
# Generate a unique tag for this test case. Logs are diagnostic-only; the
|
| 441 |
+
# completed page's model/liveboard URLs identify the run under test.
|
| 442 |
test_tag = f"TR-{uuid.uuid4().hex[:8].upper()}"
|
| 443 |
test_case["_test_tag"] = test_tag # store so run_single_test can pass it to diagnostics
|
| 444 |
|
|
|
|
| 504 |
def extract_run_context(page: Page) -> dict:
|
| 505 |
"""
|
| 506 |
After completion, find model and liveboard URLs in the page.
|
| 507 |
+
Uses visible text plus full HTML so GUIDs in href attributes are also
|
| 508 |
+
matched. This is the source of truth for the run under test.
|
| 509 |
"""
|
| 510 |
+
try:
|
| 511 |
+
visible = page.inner_text("body", timeout=5000)
|
| 512 |
+
except Exception:
|
| 513 |
+
visible = ""
|
| 514 |
+
body = f"{visible}\n{page.content()}" # full HTML catches GUIDs in href attrs too
|
| 515 |
|
| 516 |
model_match = re.search(r'(https://[^\s"\'<>#]+)/#/data/tables/([a-f0-9-]{36})', body)
|
| 517 |
lb_match = re.search(r'(https://[^\s"\'<>#]+)/#/pinboard/([a-f0-9-]{36})', body)
|
| 518 |
+
ts_base = None
|
| 519 |
+
if model_match:
|
| 520 |
+
ts_base = model_match.group(1)
|
| 521 |
+
elif lb_match:
|
| 522 |
+
ts_base = lb_match.group(1)
|
| 523 |
|
| 524 |
return {
|
| 525 |
+
"ts_base_url": ts_base,
|
| 526 |
"model_guid": model_match.group(2) if model_match else None,
|
| 527 |
"liveboard_guid": lb_match.group(2) if lb_match else None,
|
| 528 |
+
"source": "page",
|
| 529 |
}
|
| 530 |
|
| 531 |
|
|
|
|
| 621 |
return "", ""
|
| 622 |
|
| 623 |
|
| 624 |
+
def resolve_schema_from_model(run_context: dict) -> dict:
|
| 625 |
+
"""
|
| 626 |
+
Resolve the Snowflake schema from the exact ThoughtSpot model printed by
|
| 627 |
+
the app. No prefix/date guessing.
|
| 628 |
+
"""
|
| 629 |
+
ts_base = run_context.get("ts_base_url")
|
| 630 |
+
model_guid = run_context.get("model_guid")
|
| 631 |
+
if not ts_base or not model_guid:
|
| 632 |
+
return {"found": False, "reason": "missing model URL"}
|
| 633 |
+
try:
|
| 634 |
+
session = ts_authenticate(ts_base)
|
| 635 |
+
model_tml = export_model_tml(ts_base, session, model_guid)
|
| 636 |
+
db, schema = extract_db_schema(model_tml)
|
| 637 |
+
if not schema:
|
| 638 |
+
return {"found": False, "reason": "model TML did not expose schema"}
|
| 639 |
+
return {"found": True, "database": db or "DEMOBUILD", "schema": schema}
|
| 640 |
+
except Exception as e:
|
| 641 |
+
return {"found": False, "reason": str(e)}
|
| 642 |
+
|
| 643 |
+
|
| 644 |
def get_snowflake_sample(db: str, schema: str) -> str:
|
| 645 |
try:
|
| 646 |
from snowflake_auth import get_snowflake_connection
|
|
|
|
| 835 |
db, schema = extract_db_schema(model_tml)
|
| 836 |
if (not db or not schema) and schema_override:
|
| 837 |
db, schema = "DEMOBUILD", schema_override
|
| 838 |
+
print(f" βΉοΈ Using schema resolved from model: {schema_override}")
|
| 839 |
sample = get_snowflake_sample(db, schema) if db and schema else "Could not determine db/schema"
|
| 840 |
print(" π€ Grading data quality...")
|
| 841 |
dg = grade_data_quality(company, vertical, line, function, model_tml, sample)
|
|
|
|
| 1171 |
def fetch_run_diagnostics(start_time: float, company: str = "", test_tag: str = "") -> dict:
|
| 1172 |
"""
|
| 1173 |
Query session_logs for entries by testrunner after start_time.
|
| 1174 |
+
Matches only by exact test tag. This is intentionally not used to identify
|
| 1175 |
+
the model/schema under test because concurrent runs can contaminate logs.
|
| 1176 |
"""
|
| 1177 |
try:
|
| 1178 |
from supabase_client import SupabaseSettings
|
|
|
|
| 1200 |
sid = log["session_id"]
|
| 1201 |
sessions.setdefault(sid, []).append(log)
|
| 1202 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1203 |
if test_tag:
|
| 1204 |
tag_matching = {
|
| 1205 |
sid: entries for sid, entries in sessions.items()
|
| 1206 |
if any((l.get("meta") or {}).get("test_tag") == test_tag for l in entries)
|
| 1207 |
}
|
| 1208 |
+
if not tag_matching:
|
| 1209 |
+
return {"found": False, "reason": f"Exact test tag not found in session_logs: {test_tag}"}
|
| 1210 |
+
session_logs = max(tag_matching.values(), key=len)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1211 |
else:
|
| 1212 |
+
return {"found": False, "reason": "No test tag provided; refusing to guess session"}
|
| 1213 |
|
| 1214 |
# Summarise
|
| 1215 |
completed = [l["stage"] for l in session_logs if "completed" in (l.get("event") or "")]
|
|
|
|
| 1267 |
def check_snowflake_schema(company: str, start_time: float, schema_override: str = None) -> dict:
|
| 1268 |
"""
|
| 1269 |
Get row counts for the Snowflake schema created during this test run.
|
| 1270 |
+
Requires an explicit schema resolved from the exact ThoughtSpot model.
|
| 1271 |
+
Never guesses by company/date prefix.
|
| 1272 |
"""
|
| 1273 |
try:
|
| 1274 |
from snowflake_auth import get_snowflake_connection
|
|
|
|
| 1275 |
|
| 1276 |
+
if not schema_override:
|
| 1277 |
+
return {"found": False, "reason": "No explicit schema provided; refusing to guess"}
|
| 1278 |
conn = get_snowflake_connection()
|
| 1279 |
cursor = conn.cursor()
|
| 1280 |
|
| 1281 |
+
schema = schema_override
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1282 |
cursor.execute(f'SHOW TABLES IN SCHEMA DEMOBUILD."{schema}"')
|
| 1283 |
tables = cursor.fetchall()
|
| 1284 |
|
|
|
|
| 1323 |
for t in sf["tables"]:
|
| 1324 |
print(f" {t['table']}: {t['rows']} rows")
|
| 1325 |
elif sf:
|
| 1326 |
+
print(f" Snowflake: {sf.get('reason') or sf.get('error') or 'schema not found'}")
|
| 1327 |
print(" βββββββββββββββββββββββββββββββββββββββββββββββββββββ")
|
| 1328 |
|
| 1329 |
|
|
|
|
| 1411 |
|
| 1412 |
result["duration_seconds"] = round(time.time() - start)
|
| 1413 |
|
| 1414 |
+
# The completed page prints the exact model/liveboard URLs for this run.
|
| 1415 |
+
# Treat that as authoritative; session_logs are diagnostics only.
|
| 1416 |
+
page_ctx = extract_run_context(page)
|
| 1417 |
+
result["run_context"] = page_ctx
|
| 1418 |
+
if page_ctx.get("model_guid"):
|
| 1419 |
+
lb_note = page_ctx.get("liveboard_guid", "")
|
| 1420 |
+
print(f" π GUIDs from page: model={page_ctx['model_guid'][:8]}β¦ lb={lb_note[:8] if lb_note else 'none'}β¦")
|
| 1421 |
+
else:
|
| 1422 |
+
print(" β οΈ No model GUID found on final page β AI grading will be skipped")
|
| 1423 |
+
|
| 1424 |
+
# Fetch exact-tag diagnostics only for supplemental errors/stage reconciliation.
|
| 1425 |
diag = fetch_run_diagnostics(start, company=result.get("company", ""),
|
| 1426 |
test_tag=test_case.get("_test_tag", ""))
|
| 1427 |
result["diagnostics"] = diag
|
| 1428 |
+
if not diag.get("found"):
|
| 1429 |
+
print(f" βΉοΈ Session logs not used for identity: {diag.get('reason')}")
|
| 1430 |
|
| 1431 |
+
schema_resolution = resolve_schema_from_model(result["run_context"])
|
| 1432 |
+
known_schema = schema_resolution.get("schema") if schema_resolution.get("found") else None
|
| 1433 |
+
if known_schema:
|
| 1434 |
+
print(f" π§ Schema from ThoughtSpot model: {known_schema}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1435 |
else:
|
| 1436 |
+
print(f" β οΈ Could not resolve schema from model: {schema_resolution.get('reason')}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1437 |
sf = check_snowflake_schema(result["company"], start, schema_override=known_schema)
|
| 1438 |
result["snowflake_check"] = sf
|
| 1439 |
if sf.get("found"):
|
|
|
|
| 1441 |
for t in sf["tables"]:
|
| 1442 |
print(f" {t['table']}: {t['rows']} rows")
|
| 1443 |
else:
|
| 1444 |
+
print(f" π¦ Snowflake: schema not checked ({sf.get('reason') or sf.get('error') or 'unknown'})")
|
| 1445 |
|
| 1446 |
# Reconcile stages with session_logs β catches cases where the monitor bailed early
|
| 1447 |
# but the pipeline actually completed (e.g. slow DDL that took >20 min)
|
|
|
|
| 1466 |
ag = run_ai_grading(
|
| 1467 |
result["run_context"],
|
| 1468 |
result["company"], result["vertical"], result["line"], result["function"],
|
| 1469 |
+
schema_override=known_schema,
|
| 1470 |
)
|
| 1471 |
else:
|
| 1472 |
ag["grading_errors"].append("Skipped β no model GUID (pipeline did not complete)")
|