wup 0.2.67__tar.gz → 0.2.68__tar.gz
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.
- {wup-0.2.67/wup.egg-info → wup-0.2.68}/PKG-INFO +7 -5
- {wup-0.2.67 → wup-0.2.68}/README.md +6 -4
- {wup-0.2.67 → wup-0.2.68}/pyproject.toml +1 -1
- {wup-0.2.67 → wup-0.2.68}/tests/test_testql_watcher.py +28 -0
- {wup-0.2.67 → wup-0.2.68}/tests/test_wup.py +81 -0
- {wup-0.2.67 → wup-0.2.68}/wup/__init__.py +1 -1
- {wup-0.2.67 → wup-0.2.68}/wup/config.py +6 -0
- {wup-0.2.67 → wup-0.2.68}/wup/models/config.py +3 -0
- {wup-0.2.67 → wup-0.2.68}/wup/planfile_reporter.py +20 -0
- {wup-0.2.67 → wup-0.2.68}/wup/testql_watcher.py +11 -5
- {wup-0.2.67 → wup-0.2.68}/wup/visual_diff.py +54 -1
- {wup-0.2.67 → wup-0.2.68/wup.egg-info}/PKG-INFO +7 -5
- {wup-0.2.67 → wup-0.2.68}/LICENSE +0 -0
- {wup-0.2.67 → wup-0.2.68}/setup.cfg +0 -0
- {wup-0.2.67 → wup-0.2.68}/tests/test_assistant.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/tests/test_auto_detection.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/tests/test_cli_bridge.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/tests/test_cli_filtering.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/tests/test_control.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/tests/test_e2e.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/tests/test_endpoints_init_cli.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/tests/test_health_summary_passed.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/tests/test_monitoring_manifest.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/tests/test_probe_mutex.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/tests/test_service_inference.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/tests/test_status_data.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/tests/test_sync.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/tests/test_testql_monitor.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/tests/test_visual_diff_periodic_skip.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/tests/test_visual_diff_progress.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/tests/test_watch_exclude.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/tests/test_web_client.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/tests/test_wup_generate.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/wup/_ast_detector.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/wup/_base_detector.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/wup/_hash_detector.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/wup/_yaml_detector.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/wup/anomaly_detector.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/wup/anomaly_models.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/wup/assistant.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/wup/assistant_discovery.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/wup/assistant_validator.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/wup/bus.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/wup/cli.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/wup/cli_bridge.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/wup/cli_config_generator.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/wup/cli_scanner.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/wup/control.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/wup/core.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/wup/dependency_mapper.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/wup/endpoints.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/wup/event_store.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/wup/file_watcher/events/file_events.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/wup/generate.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/wup/init_cli.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/wup/models/__init__.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/wup/monitoring_manifest.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/wup/paths.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/wup/status_data.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/wup/sync.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/wup/testing/events/health_events.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/wup/testing/events/test_results.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/wup/testing/handlers/event_handlers.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/wup/testing/handlers/health_handlers.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/wup/testing/queries/health_queries.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/wup/testql_cli_generator.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/wup/testql_discovery.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/wup/testql_monitor.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/wup/validate.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/wup/web_client.py +0 -0
- {wup-0.2.67 → wup-0.2.68}/wup.egg-info/SOURCES.txt +0 -0
- {wup-0.2.67 → wup-0.2.68}/wup.egg-info/dependency_links.txt +0 -0
- {wup-0.2.67 → wup-0.2.68}/wup.egg-info/entry_points.txt +0 -0
- {wup-0.2.67 → wup-0.2.68}/wup.egg-info/requires.txt +0 -0
- {wup-0.2.67 → wup-0.2.68}/wup.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wup
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.68
|
|
4
4
|
Summary: WUP (What's Up) - Intelligent file watcher for regression testing in large projects
|
|
5
5
|
Author-email: Tom Sapletta <tom@sapletta.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -42,15 +42,17 @@ Dynamic: license-file
|
|
|
42
42
|
|
|
43
43
|
## AI Cost Tracking
|
|
44
44
|
|
|
45
|
-
   
|
|
46
|
+
  
|
|
46
47
|
|
|
47
|
-
|
|
48
|
+
- 🤖 **LLM usage:** $3.8816 (82 commits)
|
|
49
|
+
- 👤 **Human dev:** ~$3968 (39.7h @ $100/h, 30min dedup)
|
|
48
50
|
|
|
49
|
-
Generated on 2026-
|
|
51
|
+
Generated on 2026-07-03 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
50
52
|
|
|
51
53
|
---
|
|
52
54
|
|
|
53
|
-
    
|
|
54
56
|
|
|
55
57
|
**WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
|
|
56
58
|
|
|
@@ -3,15 +3,17 @@
|
|
|
3
3
|
|
|
4
4
|
## AI Cost Tracking
|
|
5
5
|
|
|
6
|
-
   
|
|
7
|
+
  
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
- 🤖 **LLM usage:** $3.8816 (82 commits)
|
|
10
|
+
- 👤 **Human dev:** ~$3968 (39.7h @ $100/h, 30min dedup)
|
|
9
11
|
|
|
10
|
-
Generated on 2026-
|
|
12
|
+
Generated on 2026-07-03 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
11
13
|
|
|
12
14
|
---
|
|
13
15
|
|
|
14
|
-
    
|
|
15
17
|
|
|
16
18
|
**WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
|
|
17
19
|
|
|
@@ -283,6 +283,34 @@ def test_planfile_reporter_clears_dedupe_after_recovery(monkeypatch):
|
|
|
283
283
|
|
|
284
284
|
assert first == "PLF-1000"
|
|
285
285
|
assert second == "PLF-1001"
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def test_planfile_reporter_retries_without_files_for_old_planfile_cli(monkeypatch):
|
|
289
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
290
|
+
root = Path(tmpdir)
|
|
291
|
+
calls = []
|
|
292
|
+
|
|
293
|
+
def fake_run(cmd, **kwargs):
|
|
294
|
+
calls.append(cmd)
|
|
295
|
+
if "--files" in cmd:
|
|
296
|
+
return CompletedProcess(cmd, returncode=2, stdout="", stderr="Error: No such option: --files")
|
|
297
|
+
return CompletedProcess(cmd, returncode=0, stdout="Created PLF-998\n", stderr="")
|
|
298
|
+
|
|
299
|
+
monkeypatch.setattr("wup.planfile_reporter.subprocess.run", fake_run)
|
|
300
|
+
reporter = PlanfileReporter(root, PlanfileConfig(enabled=True))
|
|
301
|
+
|
|
302
|
+
ticket_id = reporter.report_failure(
|
|
303
|
+
service="firmware",
|
|
304
|
+
status="failed",
|
|
305
|
+
stage="detail",
|
|
306
|
+
message="detail failed",
|
|
307
|
+
track_file=".wup/tracks/firmware_detail.json",
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
assert ticket_id == "PLF-998"
|
|
311
|
+
assert len(calls) == 2
|
|
312
|
+
assert "--files" in calls[0]
|
|
313
|
+
assert "--files" not in calls[1]
|
|
286
314
|
assert len(calls) == 2
|
|
287
315
|
|
|
288
316
|
|
|
@@ -26,6 +26,7 @@ from wup.models.config import (
|
|
|
26
26
|
from wup.testql_watcher import TestQLWatcher
|
|
27
27
|
from wup.visual_diff import (
|
|
28
28
|
VisualDiffer,
|
|
29
|
+
_chromium_launch_options,
|
|
29
30
|
_diff_snapshots,
|
|
30
31
|
_looks_like_visual_page,
|
|
31
32
|
_page_slug,
|
|
@@ -994,6 +995,9 @@ class TestConfigModels:
|
|
|
994
995
|
assert cfg.pages == []
|
|
995
996
|
assert cfg.pages_from_endpoints is True
|
|
996
997
|
assert cfg.headless is True
|
|
998
|
+
assert cfg.page_settle_ms == 750
|
|
999
|
+
assert cfg.issue_retry_count == 0
|
|
1000
|
+
assert cfg.issue_retry_delay_seconds == 2.0
|
|
997
1001
|
|
|
998
1002
|
def test_visual_diff_config_custom(self):
|
|
999
1003
|
"""Test VisualDiffConfig with custom values."""
|
|
@@ -1002,11 +1006,17 @@ class TestConfigModels:
|
|
|
1002
1006
|
base_url="http://localhost:9000",
|
|
1003
1007
|
pages=["/dashboard", "/users"],
|
|
1004
1008
|
threshold_added=10,
|
|
1009
|
+
page_settle_ms=1500,
|
|
1010
|
+
issue_retry_count=2,
|
|
1011
|
+
issue_retry_delay_seconds=3.0,
|
|
1005
1012
|
)
|
|
1006
1013
|
assert cfg.enabled is True
|
|
1007
1014
|
assert cfg.base_url == "http://localhost:9000"
|
|
1008
1015
|
assert cfg.pages == ["/dashboard", "/users"]
|
|
1009
1016
|
assert cfg.threshold_added == 10
|
|
1017
|
+
assert cfg.page_settle_ms == 1500
|
|
1018
|
+
assert cfg.issue_retry_count == 2
|
|
1019
|
+
assert cfg.issue_retry_delay_seconds == 3.0
|
|
1010
1020
|
|
|
1011
1021
|
|
|
1012
1022
|
class TestVisualDiffer:
|
|
@@ -1026,6 +1036,14 @@ class TestVisualDiffer:
|
|
|
1026
1036
|
monkeypatch.delenv("WUP_VD_NONE", raising=False)
|
|
1027
1037
|
assert _resolve_base_url(cfg) == ""
|
|
1028
1038
|
|
|
1039
|
+
def test_chromium_launch_options_uses_env_executable(self, tmp_path, monkeypatch):
|
|
1040
|
+
chrome = tmp_path / "chrome"
|
|
1041
|
+
chrome.write_text("#!/bin/sh\n", encoding="utf-8")
|
|
1042
|
+
monkeypatch.setenv("WUP_CHROMIUM_EXECUTABLE_PATH", str(chrome))
|
|
1043
|
+
options = _chromium_launch_options(True)
|
|
1044
|
+
assert options["headless"] is True
|
|
1045
|
+
assert options["executable_path"] == str(chrome)
|
|
1046
|
+
|
|
1029
1047
|
def test_page_slug(self):
|
|
1030
1048
|
assert _page_slug("http://x/api/v1/users") == "api_v1_users"
|
|
1031
1049
|
assert _page_slug("http://x/") == "root"
|
|
@@ -1168,6 +1186,57 @@ class TestVisualDiffer:
|
|
|
1168
1186
|
assert "/a, /b, /c (+1 more)" in printed[0]
|
|
1169
1187
|
assert "4x BrowserType.launch: Executable doesn't exist at /tmp/chrome" in printed[0]
|
|
1170
1188
|
|
|
1189
|
+
def test_check_page_retries_transient_visual_issue(self, monkeypatch):
|
|
1190
|
+
"""A transient Vite overlay/tiny shell should be retried before reporting an issue."""
|
|
1191
|
+
import asyncio
|
|
1192
|
+
from wup import visual_diff as visual_diff_module
|
|
1193
|
+
|
|
1194
|
+
bad_snapshot = {
|
|
1195
|
+
"tag": "HTML",
|
|
1196
|
+
"meta": {
|
|
1197
|
+
"text_length": 33,
|
|
1198
|
+
"dom_nodes": 8,
|
|
1199
|
+
"matched_error_selectors": ["vite-error-overlay"],
|
|
1200
|
+
},
|
|
1201
|
+
}
|
|
1202
|
+
good_snapshot = {
|
|
1203
|
+
"tag": "HTML",
|
|
1204
|
+
"children": [{"tag": "BODY", "children": [{"tag": "MAIN"}]}],
|
|
1205
|
+
"meta": {
|
|
1206
|
+
"text_length": 500,
|
|
1207
|
+
"dom_nodes": 50,
|
|
1208
|
+
"matched_error_selectors": [],
|
|
1209
|
+
},
|
|
1210
|
+
}
|
|
1211
|
+
snapshots = [bad_snapshot, good_snapshot]
|
|
1212
|
+
calls = []
|
|
1213
|
+
|
|
1214
|
+
async def fake_fetch(*args):
|
|
1215
|
+
calls.append(args)
|
|
1216
|
+
return snapshots.pop(0), None
|
|
1217
|
+
|
|
1218
|
+
async def fake_sleep(_seconds):
|
|
1219
|
+
return None
|
|
1220
|
+
|
|
1221
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1222
|
+
cfg = VisualDiffConfig(
|
|
1223
|
+
enabled=True,
|
|
1224
|
+
issue_retry_count=1,
|
|
1225
|
+
issue_retry_delay_seconds=0,
|
|
1226
|
+
page_settle_ms=1234,
|
|
1227
|
+
)
|
|
1228
|
+
differ = VisualDiffer(tmpdir, cfg)
|
|
1229
|
+
|
|
1230
|
+
monkeypatch.setattr(visual_diff_module, "_fetch_dom_snapshot", fake_fetch)
|
|
1231
|
+
monkeypatch.setattr(visual_diff_module.asyncio, "sleep", fake_sleep)
|
|
1232
|
+
|
|
1233
|
+
result = asyncio.run(differ._check_page("frontend", "http://localhost/page"))
|
|
1234
|
+
|
|
1235
|
+
assert len(calls) == 2
|
|
1236
|
+
assert calls[0][-1] == 1234
|
|
1237
|
+
assert result["diff"]["status"] in {"new", "ok", "changed"}
|
|
1238
|
+
assert "issues" not in result["diff"]
|
|
1239
|
+
|
|
1171
1240
|
def test_get_recent_diffs_empty(self):
|
|
1172
1241
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1173
1242
|
cfg = VisualDiffConfig()
|
|
@@ -1376,6 +1445,9 @@ testql:
|
|
|
1376
1445
|
threshold_added=7,
|
|
1377
1446
|
threshold_removed=4,
|
|
1378
1447
|
threshold_changed=8,
|
|
1448
|
+
page_settle_ms=1600,
|
|
1449
|
+
issue_retry_count=3,
|
|
1450
|
+
issue_retry_delay_seconds=4.5,
|
|
1379
1451
|
headless=False,
|
|
1380
1452
|
),
|
|
1381
1453
|
)
|
|
@@ -1392,6 +1464,9 @@ testql:
|
|
|
1392
1464
|
assert vd.threshold_added == 7
|
|
1393
1465
|
assert vd.threshold_removed == 4
|
|
1394
1466
|
assert vd.threshold_changed == 8
|
|
1467
|
+
assert vd.page_settle_ms == 1600
|
|
1468
|
+
assert vd.issue_retry_count == 3
|
|
1469
|
+
assert vd.issue_retry_delay_seconds == 4.5
|
|
1395
1470
|
assert vd.headless is False
|
|
1396
1471
|
|
|
1397
1472
|
def test_load_config_visual_diff_from_yaml(self):
|
|
@@ -1415,6 +1490,9 @@ visual_diff:
|
|
|
1415
1490
|
threshold_added: 5
|
|
1416
1491
|
threshold_removed: 2
|
|
1417
1492
|
threshold_changed: 9
|
|
1493
|
+
page_settle_ms: 1500
|
|
1494
|
+
issue_retry_count: 2
|
|
1495
|
+
issue_retry_delay_seconds: 3
|
|
1418
1496
|
headless: false
|
|
1419
1497
|
"""
|
|
1420
1498
|
config_path = Path(tmpdir) / "wup.yaml"
|
|
@@ -1433,6 +1511,9 @@ visual_diff:
|
|
|
1433
1511
|
assert vd.threshold_added == 5
|
|
1434
1512
|
assert vd.threshold_removed == 2
|
|
1435
1513
|
assert vd.threshold_changed == 9
|
|
1514
|
+
assert vd.page_settle_ms == 1500
|
|
1515
|
+
assert vd.issue_retry_count == 2
|
|
1516
|
+
assert vd.issue_retry_delay_seconds == 3.0
|
|
1436
1517
|
assert vd.headless is False
|
|
1437
1518
|
|
|
1438
1519
|
def test_load_config_visual_diff_defaults_when_section_absent(self):
|
|
@@ -7,7 +7,7 @@ WUP monitors file changes and runs intelligent regression tests using a 3-layer
|
|
|
7
7
|
3. Detail Layer: Full tests with blame reports (only on failure)
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
-
__version__ = "0.2.
|
|
10
|
+
__version__ = "0.2.68"
|
|
11
11
|
__author__ = "Tom Sapletta"
|
|
12
12
|
|
|
13
13
|
from .config import load_config, save_config, get_default_config
|
|
@@ -292,6 +292,9 @@ def _parse_visual_diff_config(raw: dict) -> VisualDiffConfig:
|
|
|
292
292
|
threshold_changed=int(vd_raw.get("threshold_changed", 5)),
|
|
293
293
|
min_text_length=int(vd_raw.get("min_text_length", 200)),
|
|
294
294
|
min_dom_nodes=int(vd_raw.get("min_dom_nodes", 20)),
|
|
295
|
+
page_settle_ms=int(vd_raw.get("page_settle_ms", 750)),
|
|
296
|
+
issue_retry_count=int(vd_raw.get("issue_retry_count", 0)),
|
|
297
|
+
issue_retry_delay_seconds=float(vd_raw.get("issue_retry_delay_seconds", 2.0)),
|
|
295
298
|
error_selectors=vd_raw.get("error_selectors", [
|
|
296
299
|
"#error-container",
|
|
297
300
|
".error-container",
|
|
@@ -494,6 +497,9 @@ def save_config(config: WupConfig, output_path: Path):
|
|
|
494
497
|
"threshold_changed": config.visual_diff.threshold_changed,
|
|
495
498
|
"min_text_length": config.visual_diff.min_text_length,
|
|
496
499
|
"min_dom_nodes": config.visual_diff.min_dom_nodes,
|
|
500
|
+
"page_settle_ms": config.visual_diff.page_settle_ms,
|
|
501
|
+
"issue_retry_count": config.visual_diff.issue_retry_count,
|
|
502
|
+
"issue_retry_delay_seconds": config.visual_diff.issue_retry_delay_seconds,
|
|
497
503
|
"error_selectors": config.visual_diff.error_selectors,
|
|
498
504
|
"headless": config.visual_diff.headless,
|
|
499
505
|
},
|
|
@@ -97,6 +97,9 @@ class VisualDiffConfig:
|
|
|
97
97
|
threshold_changed: int = 5 # min changed attrs to report
|
|
98
98
|
min_text_length: int = 200 # anomaly if rendered text is too short
|
|
99
99
|
min_dom_nodes: int = 20 # anomaly if DOM is suspiciously tiny
|
|
100
|
+
page_settle_ms: int = 750 # wait after networkidle before snapshotting SPA DOM
|
|
101
|
+
issue_retry_count: int = 0 # retry transient Vite/HMR/page-shell issues
|
|
102
|
+
issue_retry_delay_seconds: float = 2.0
|
|
100
103
|
error_selectors: List[str] = field(default_factory=lambda: [
|
|
101
104
|
"#error-container",
|
|
102
105
|
".error-container",
|
|
@@ -123,6 +123,21 @@ class PlanfileReporter:
|
|
|
123
123
|
|
|
124
124
|
stdout = (result.stdout or "").strip()
|
|
125
125
|
stderr = (result.stderr or "").strip()
|
|
126
|
+
if result.returncode != 0 and track_file and self._files_option_unsupported(stderr or stdout):
|
|
127
|
+
cmd = [part for index, part in enumerate(cmd) if not (part == "--files" or (index > 0 and cmd[index - 1] == "--files"))]
|
|
128
|
+
try:
|
|
129
|
+
result = subprocess.run(
|
|
130
|
+
cmd,
|
|
131
|
+
cwd=str(self.project_root),
|
|
132
|
+
capture_output=True,
|
|
133
|
+
text=True,
|
|
134
|
+
timeout=30,
|
|
135
|
+
)
|
|
136
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
137
|
+
self.console.print(f"[yellow]planfile ticket creation skipped: {exc}[/yellow]")
|
|
138
|
+
return None
|
|
139
|
+
stdout = (result.stdout or "").strip()
|
|
140
|
+
stderr = (result.stderr or "").strip()
|
|
126
141
|
if result.returncode != 0:
|
|
127
142
|
detail = stderr or stdout or f"rc={result.returncode}"
|
|
128
143
|
self.console.print(f"[yellow]planfile ticket creation failed: {detail}[/yellow]")
|
|
@@ -187,6 +202,11 @@ class PlanfileReporter:
|
|
|
187
202
|
match = re.search(r"\bPLF-\d+\b", text)
|
|
188
203
|
return match.group(0) if match else None
|
|
189
204
|
|
|
205
|
+
@staticmethod
|
|
206
|
+
def _files_option_unsupported(text: str) -> bool:
|
|
207
|
+
lowered = text.lower()
|
|
208
|
+
return "--files" in lowered and ("no such option" in lowered or "unknown option" in lowered)
|
|
209
|
+
|
|
190
210
|
@staticmethod
|
|
191
211
|
def _ticket_name(*, service: str, stage: str, status: str) -> str:
|
|
192
212
|
return f"[AUTO-DIAG] wup-{service} {stage} {status}"
|
|
@@ -361,16 +361,22 @@ class TestQLWatcher(WupWatcher):
|
|
|
361
361
|
passed, total, failed = (int(match.group(i)) for i in range(1, 4))
|
|
362
362
|
return failed == 0 and passed >= total and total > 0
|
|
363
363
|
|
|
364
|
-
def _select_scenarios_for_service(self, service: str) -> List[Path]:
|
|
364
|
+
def _select_scenarios_for_service(self, service: str, *, stage: str = "quick") -> List[Path]:
|
|
365
365
|
all_scenarios = self._discover_scenarios()
|
|
366
366
|
if not all_scenarios:
|
|
367
367
|
return []
|
|
368
368
|
|
|
369
369
|
svc_config = self.get_service_config(service)
|
|
370
|
-
|
|
371
|
-
|
|
370
|
+
if stage == "detail":
|
|
371
|
+
test_cfg = svc_config.detail_tests if svc_config else None
|
|
372
|
+
default_limit = 10
|
|
373
|
+
else:
|
|
374
|
+
test_cfg = svc_config.quick_tests if svc_config else None
|
|
375
|
+
default_limit = self.quick_limit
|
|
372
376
|
|
|
373
|
-
|
|
377
|
+
limit = test_cfg.max_endpoints if test_cfg else default_limit
|
|
378
|
+
|
|
379
|
+
pinned = test_cfg.scenario if test_cfg else ""
|
|
374
380
|
if pinned:
|
|
375
381
|
resolved = self._resolve_scenario_path(pinned)
|
|
376
382
|
if resolved:
|
|
@@ -870,7 +876,7 @@ class TestQLWatcher(WupWatcher):
|
|
|
870
876
|
if configured_endpoint not in merged_endpoints:
|
|
871
877
|
merged_endpoints.append(configured_endpoint)
|
|
872
878
|
|
|
873
|
-
scenarios = self._select_scenarios_for_service(service)
|
|
879
|
+
scenarios = self._select_scenarios_for_service(service, stage="detail")
|
|
874
880
|
results = {
|
|
875
881
|
"service": service,
|
|
876
882
|
"total_scenarios": len(scenarios),
|
|
@@ -17,6 +17,7 @@ from __future__ import annotations
|
|
|
17
17
|
import asyncio
|
|
18
18
|
import json
|
|
19
19
|
import os
|
|
20
|
+
import shutil
|
|
20
21
|
import time
|
|
21
22
|
from collections import Counter
|
|
22
23
|
from pathlib import Path
|
|
@@ -97,11 +98,30 @@ _DOM_SNAPSHOT_JS = """
|
|
|
97
98
|
"""
|
|
98
99
|
|
|
99
100
|
|
|
101
|
+
def _chromium_launch_options(headless: bool) -> Dict[str, Any]:
|
|
102
|
+
options: Dict[str, Any] = {"headless": headless}
|
|
103
|
+
executable = os.environ.get("WUP_CHROMIUM_EXECUTABLE_PATH") or os.environ.get("CHROME_BIN")
|
|
104
|
+
if not executable:
|
|
105
|
+
for candidate in (
|
|
106
|
+
"google-chrome-stable",
|
|
107
|
+
"google-chrome",
|
|
108
|
+
"chromium",
|
|
109
|
+
"chromium-browser",
|
|
110
|
+
):
|
|
111
|
+
executable = shutil.which(candidate)
|
|
112
|
+
if executable:
|
|
113
|
+
break
|
|
114
|
+
if executable and Path(executable).exists():
|
|
115
|
+
options["executable_path"] = executable
|
|
116
|
+
return options
|
|
117
|
+
|
|
118
|
+
|
|
100
119
|
async def _fetch_dom_snapshot(
|
|
101
120
|
url: str,
|
|
102
121
|
max_depth: int,
|
|
103
122
|
headless: bool,
|
|
104
123
|
error_selectors: List[str],
|
|
124
|
+
page_settle_ms: int = 750,
|
|
105
125
|
) -> Tuple[Optional[Dict], Optional[str]]:
|
|
106
126
|
"""Return a DOM structure dict for *url* using Playwright."""
|
|
107
127
|
if not _playwright_available():
|
|
@@ -110,10 +130,12 @@ async def _fetch_dom_snapshot(
|
|
|
110
130
|
try:
|
|
111
131
|
from playwright.async_api import async_playwright
|
|
112
132
|
async with async_playwright() as pw:
|
|
113
|
-
browser = await pw.chromium.launch(headless
|
|
133
|
+
browser = await pw.chromium.launch(**_chromium_launch_options(headless))
|
|
114
134
|
page = await browser.new_page()
|
|
115
135
|
try:
|
|
116
136
|
await page.goto(url, wait_until="networkidle", timeout=15_000)
|
|
137
|
+
if page_settle_ms > 0:
|
|
138
|
+
await page.wait_for_timeout(page_settle_ms)
|
|
117
139
|
snapshot = await page.evaluate(_DOM_SNAPSHOT_JS, max_depth)
|
|
118
140
|
text_length = await page.evaluate("() => (document.body?.innerText || '').trim().length")
|
|
119
141
|
dom_nodes = await page.evaluate("() => document.querySelectorAll('*').length")
|
|
@@ -522,6 +544,7 @@ class VisualDiffer:
|
|
|
522
544
|
self.cfg.max_depth,
|
|
523
545
|
self.cfg.headless,
|
|
524
546
|
self.cfg.error_selectors,
|
|
547
|
+
self.cfg.page_settle_ms,
|
|
525
548
|
)
|
|
526
549
|
|
|
527
550
|
if new_snapshot is None:
|
|
@@ -543,6 +566,36 @@ class VisualDiffer:
|
|
|
543
566
|
)
|
|
544
567
|
|
|
545
568
|
issues = _detect_content_issues(new_snapshot, self.cfg)
|
|
569
|
+
retries_left = max(0, int(getattr(self.cfg, "issue_retry_count", 0) or 0))
|
|
570
|
+
while issues and retries_left > 0:
|
|
571
|
+
await asyncio.sleep(max(0.0, float(getattr(self.cfg, "issue_retry_delay_seconds", 2.0) or 0.0)))
|
|
572
|
+
retry_snapshot, retry_error = await _fetch_dom_snapshot(
|
|
573
|
+
url,
|
|
574
|
+
self.cfg.max_depth,
|
|
575
|
+
self.cfg.headless,
|
|
576
|
+
self.cfg.error_selectors,
|
|
577
|
+
self.cfg.page_settle_ms,
|
|
578
|
+
)
|
|
579
|
+
if retry_snapshot is None:
|
|
580
|
+
fetch_error = retry_error
|
|
581
|
+
retries_left -= 1
|
|
582
|
+
continue
|
|
583
|
+
retry_issues = _detect_content_issues(retry_snapshot, self.cfg)
|
|
584
|
+
if not retry_issues:
|
|
585
|
+
new_snapshot = retry_snapshot
|
|
586
|
+
issues = []
|
|
587
|
+
diff = _diff_snapshots(
|
|
588
|
+
old_snapshot,
|
|
589
|
+
new_snapshot,
|
|
590
|
+
self.cfg.max_depth,
|
|
591
|
+
self.cfg.threshold_added,
|
|
592
|
+
self.cfg.threshold_removed,
|
|
593
|
+
self.cfg.threshold_changed,
|
|
594
|
+
)
|
|
595
|
+
break
|
|
596
|
+
new_snapshot = retry_snapshot
|
|
597
|
+
issues = retry_issues
|
|
598
|
+
retries_left -= 1
|
|
546
599
|
if issues:
|
|
547
600
|
diff["status"] = "issue"
|
|
548
601
|
diff["issues"] = issues
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wup
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.68
|
|
4
4
|
Summary: WUP (What's Up) - Intelligent file watcher for regression testing in large projects
|
|
5
5
|
Author-email: Tom Sapletta <tom@sapletta.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -42,15 +42,17 @@ Dynamic: license-file
|
|
|
42
42
|
|
|
43
43
|
## AI Cost Tracking
|
|
44
44
|
|
|
45
|
-
   
|
|
46
|
+
  
|
|
46
47
|
|
|
47
|
-
|
|
48
|
+
- 🤖 **LLM usage:** $3.8816 (82 commits)
|
|
49
|
+
- 👤 **Human dev:** ~$3968 (39.7h @ $100/h, 30min dedup)
|
|
48
50
|
|
|
49
|
-
Generated on 2026-
|
|
51
|
+
Generated on 2026-07-03 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
50
52
|
|
|
51
53
|
---
|
|
52
54
|
|
|
53
|
-
    
|
|
54
56
|
|
|
55
57
|
**WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
|
|
56
58
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|