wup 0.2.29__tar.gz → 0.2.33__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.29/wup.egg-info → wup-0.2.33}/PKG-INFO +9 -7
- {wup-0.2.29 → wup-0.2.33}/README.md +6 -6
- {wup-0.2.29 → wup-0.2.33}/pyproject.toml +6 -1
- {wup-0.2.29 → wup-0.2.33}/tests/test_testql_watcher.py +86 -4
- {wup-0.2.29 → wup-0.2.33}/tests/test_wup.py +138 -6
- {wup-0.2.29 → wup-0.2.33}/wup/__init__.py +1 -1
- {wup-0.2.29 → wup-0.2.33}/wup/cli.py +115 -53
- {wup-0.2.29 → wup-0.2.33}/wup/config.py +31 -17
- {wup-0.2.29 → wup-0.2.33}/wup/core.py +29 -14
- {wup-0.2.29 → wup-0.2.33}/wup/monitoring_manifest.py +93 -59
- {wup-0.2.29 → wup-0.2.33}/wup/testql_monitor.py +131 -68
- {wup-0.2.29 → wup-0.2.33}/wup/testql_watcher.py +62 -27
- {wup-0.2.29 → wup-0.2.33}/wup/visual_diff.py +140 -33
- {wup-0.2.29 → wup-0.2.33/wup.egg-info}/PKG-INFO +9 -7
- {wup-0.2.29 → wup-0.2.33}/wup.egg-info/requires.txt +3 -0
- {wup-0.2.29 → wup-0.2.33}/LICENSE +0 -0
- {wup-0.2.29 → wup-0.2.33}/setup.cfg +0 -0
- {wup-0.2.29 → wup-0.2.33}/tests/test_e2e.py +0 -0
- {wup-0.2.29 → wup-0.2.33}/tests/test_monitoring_manifest.py +0 -0
- {wup-0.2.29 → wup-0.2.33}/tests/test_testql_monitor.py +0 -0
- {wup-0.2.29 → wup-0.2.33}/tests/test_web_client.py +0 -0
- {wup-0.2.29 → wup-0.2.33}/wup/_ast_detector.py +0 -0
- {wup-0.2.29 → wup-0.2.33}/wup/_hash_detector.py +0 -0
- {wup-0.2.29 → wup-0.2.33}/wup/_yaml_detector.py +0 -0
- {wup-0.2.29 → wup-0.2.33}/wup/anomaly_detector.py +0 -0
- {wup-0.2.29 → wup-0.2.33}/wup/anomaly_models.py +0 -0
- {wup-0.2.29 → wup-0.2.33}/wup/assistant.py +0 -0
- {wup-0.2.29 → wup-0.2.33}/wup/dependency_mapper.py +0 -0
- {wup-0.2.29 → wup-0.2.33}/wup/models/__init__.py +0 -0
- {wup-0.2.29 → wup-0.2.33}/wup/models/config.py +0 -0
- {wup-0.2.29 → wup-0.2.33}/wup/testql_discovery.py +0 -0
- {wup-0.2.29 → wup-0.2.33}/wup/web_client.py +0 -0
- {wup-0.2.29 → wup-0.2.33}/wup.egg-info/SOURCES.txt +0 -0
- {wup-0.2.29 → wup-0.2.33}/wup.egg-info/dependency_links.txt +0 -0
- {wup-0.2.29 → wup-0.2.33}/wup.egg-info/entry_points.txt +0 -0
- {wup-0.2.29 → wup-0.2.33}/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.33
|
|
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
|
|
@@ -22,6 +22,8 @@ Requires-Dist: psutil>=5.9.0
|
|
|
22
22
|
Requires-Dist: rich>=13.0.0
|
|
23
23
|
Requires-Dist: typer>=0.9.0
|
|
24
24
|
Requires-Dist: pyyaml>=6.0
|
|
25
|
+
Provides-Extra: visual
|
|
26
|
+
Requires-Dist: playwright<2,>=1.40; extra == "visual"
|
|
25
27
|
Dynamic: license-file
|
|
26
28
|
|
|
27
29
|
# WUP (What's Up)
|
|
@@ -29,17 +31,17 @@ Dynamic: license-file
|
|
|
29
31
|
|
|
30
32
|
## AI Cost Tracking
|
|
31
33
|
|
|
32
|
-
    
|
|
35
|
+
  
|
|
34
36
|
|
|
35
|
-
- 🤖 **LLM usage:** $2.
|
|
36
|
-
- 👤 **Human dev:** ~$
|
|
37
|
+
- 🤖 **LLM usage:** $2.5582 (43 commits)
|
|
38
|
+
- 👤 **Human dev:** ~$1832 (18.3h @ $100/h, 30min dedup)
|
|
37
39
|
|
|
38
|
-
Generated on 2026-05-
|
|
40
|
+
Generated on 2026-05-21 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
39
41
|
|
|
40
42
|
---
|
|
41
43
|
|
|
42
|
-
    
|
|
43
45
|
|
|
44
46
|
**WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
|
|
45
47
|
|
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
|
|
4
4
|
## AI Cost Tracking
|
|
5
5
|
|
|
6
|
-
    
|
|
7
|
+
  
|
|
8
8
|
|
|
9
|
-
- 🤖 **LLM usage:** $2.
|
|
10
|
-
- 👤 **Human dev:** ~$
|
|
9
|
+
- 🤖 **LLM usage:** $2.5582 (43 commits)
|
|
10
|
+
- 👤 **Human dev:** ~$1832 (18.3h @ $100/h, 30min dedup)
|
|
11
11
|
|
|
12
|
-
Generated on 2026-05-
|
|
12
|
+
Generated on 2026-05-21 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
16
|
-
    
|
|
17
17
|
|
|
18
18
|
**WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
|
|
19
19
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "wup"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.33"
|
|
8
8
|
description = "WUP (What's Up) - Intelligent file watcher for regression testing in large projects"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -30,6 +30,11 @@ classifiers = [
|
|
|
30
30
|
]
|
|
31
31
|
license = "Apache-2.0"
|
|
32
32
|
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
visual = [
|
|
35
|
+
"playwright>=1.40,<2",
|
|
36
|
+
]
|
|
37
|
+
|
|
33
38
|
[tool.setuptools.packages.find]
|
|
34
39
|
include = ["wup*"]
|
|
35
40
|
|
|
@@ -9,6 +9,7 @@ from wup.testql_watcher import TestQLWatcher
|
|
|
9
9
|
from wup.models.config import (
|
|
10
10
|
ProjectConfig,
|
|
11
11
|
ServiceConfig,
|
|
12
|
+
TestStrategyConfig,
|
|
12
13
|
TestQLConfig,
|
|
13
14
|
VisualDiffConfig,
|
|
14
15
|
WatchConfig,
|
|
@@ -25,14 +26,15 @@ def test_process_changed_file_creates_track_on_failure():
|
|
|
25
26
|
|
|
26
27
|
scenario_dir = root / "testql-scenarios"
|
|
27
28
|
scenario_dir.mkdir(parents=True, exist_ok=True)
|
|
28
|
-
failing_scenario = scenario_dir / "
|
|
29
|
+
failing_scenario = scenario_dir / "api-users-smoke.testql.toon.yaml"
|
|
29
30
|
failing_scenario.write_text("name: failing\n", encoding="utf-8")
|
|
30
31
|
|
|
31
|
-
# Pass
|
|
32
|
-
from wup.models.config import TestQLConfig, WatchConfig
|
|
32
|
+
# Pass config with service to prevent loading from temp dir
|
|
33
|
+
from wup.models.config import TestQLConfig, WatchConfig, ServiceConfig
|
|
34
|
+
service_config = ServiceConfig(name="app/users", paths=["app/users"])
|
|
33
35
|
empty_config = WupConfig(
|
|
34
36
|
project=ProjectConfig(name="test"),
|
|
35
|
-
services=[],
|
|
37
|
+
services=[service_config],
|
|
36
38
|
test_strategy=None,
|
|
37
39
|
watch=WatchConfig(), # Add watch config to avoid file filtering issues
|
|
38
40
|
testql=TestQLConfig(scenario_dir="testql-scenarios")
|
|
@@ -53,6 +55,9 @@ def test_process_changed_file_creates_track_on_failure():
|
|
|
53
55
|
return CompletedProcess(args=args, returncode=0, stdout="{}", stderr="")
|
|
54
56
|
|
|
55
57
|
watcher._run_testql = fake_run_testql # type: ignore[method-assign]
|
|
58
|
+
|
|
59
|
+
# Mock scenario selection to return our failing scenario
|
|
60
|
+
watcher._select_scenarios_for_service = lambda service: [failing_scenario] # type: ignore[method-assign]
|
|
56
61
|
|
|
57
62
|
result = asyncio.run(watcher.process_changed_file_once(str(app_file)))
|
|
58
63
|
|
|
@@ -332,3 +337,80 @@ def test_visual_differ_initialized_when_enabled():
|
|
|
332
337
|
assert isinstance(watcher.visual_differ, VisualDiffer)
|
|
333
338
|
assert watcher.visual_differ.cfg.enabled is True
|
|
334
339
|
assert watcher.visual_differ.base_url == "http://localhost:9000"
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def test_get_config_endpoints_for_service_keeps_connect_pages_on_frontend():
|
|
343
|
+
"""Frontend page routes from explicit_endpoints must not be rebound to backend/api_base_url."""
|
|
344
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
345
|
+
config = WupConfig(
|
|
346
|
+
project=ProjectConfig(name="c2004"),
|
|
347
|
+
watch=WatchConfig(),
|
|
348
|
+
services=[
|
|
349
|
+
ServiceConfig(name="frontend", type="web", paths=["frontend/**"]),
|
|
350
|
+
ServiceConfig(name="backend", type="web", paths=["backend/**"]),
|
|
351
|
+
],
|
|
352
|
+
test_strategy=TestStrategyConfig(),
|
|
353
|
+
testql=TestQLConfig(
|
|
354
|
+
base_url="http://localhost:8100",
|
|
355
|
+
api_base_url="http://localhost:8101",
|
|
356
|
+
explicit_endpoints=["/connect-config-sitemap", "/connect-data"],
|
|
357
|
+
),
|
|
358
|
+
visual_diff=VisualDiffConfig(enabled=True),
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
watcher = TestQLWatcher(tmpdir, config=config)
|
|
362
|
+
|
|
363
|
+
frontend_endpoints = watcher._get_config_endpoints_for_service("frontend")
|
|
364
|
+
backend_endpoints = watcher._get_config_endpoints_for_service("backend")
|
|
365
|
+
|
|
366
|
+
assert "http://localhost:8100/connect-config-sitemap" in frontend_endpoints
|
|
367
|
+
assert "http://localhost:8100/connect-data" in frontend_endpoints
|
|
368
|
+
assert "http://localhost:8101/connect-config-sitemap" not in backend_endpoints
|
|
369
|
+
assert "http://localhost:8101/connect-data" not in backend_endpoints
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def test_quick_pass_actions_prefer_config_endpoints_for_visual_diff():
|
|
373
|
+
"""Visual diff should prefer config endpoints over merged mapper endpoints."""
|
|
374
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
375
|
+
config = WupConfig(
|
|
376
|
+
project=ProjectConfig(name="c2004"),
|
|
377
|
+
watch=WatchConfig(),
|
|
378
|
+
services=[
|
|
379
|
+
ServiceConfig(name="frontend", type="web", paths=["frontend/**"]),
|
|
380
|
+
ServiceConfig(name="backend", type="web", paths=["backend/**"]),
|
|
381
|
+
],
|
|
382
|
+
test_strategy=TestStrategyConfig(),
|
|
383
|
+
testql=TestQLConfig(
|
|
384
|
+
base_url="http://localhost:8100",
|
|
385
|
+
api_base_url="http://localhost:8101",
|
|
386
|
+
endpoints_by_service={"backend": ["http://localhost:8101/api/v3/health"]},
|
|
387
|
+
explicit_endpoints=["/connect-config-sitemap", "/connect-data"],
|
|
388
|
+
),
|
|
389
|
+
visual_diff=VisualDiffConfig(enabled=True),
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
watcher = TestQLWatcher(tmpdir, config=config)
|
|
393
|
+
|
|
394
|
+
class RecordingDiffer:
|
|
395
|
+
def __init__(self):
|
|
396
|
+
self.cfg = VisualDiffConfig(enabled=True)
|
|
397
|
+
self.calls = []
|
|
398
|
+
|
|
399
|
+
async def run_for_service(self, service, endpoints):
|
|
400
|
+
self.calls.append((service, list(endpoints)))
|
|
401
|
+
return []
|
|
402
|
+
|
|
403
|
+
differ = RecordingDiffer()
|
|
404
|
+
watcher.visual_differ = differ
|
|
405
|
+
|
|
406
|
+
asyncio.run(
|
|
407
|
+
watcher._quick_pass_actions(
|
|
408
|
+
"backend",
|
|
409
|
+
[
|
|
410
|
+
"http://localhost:8101/connect-config-sitemap",
|
|
411
|
+
"http://localhost:8101/connect-data",
|
|
412
|
+
],
|
|
413
|
+
)
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
assert differ.calls == [("backend", ["http://localhost:8101/api/v3/health"])]
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"""Tests for WUP (What's Up) - Intelligent file watcher for regression testing."""
|
|
2
2
|
|
|
3
|
+
import errno
|
|
3
4
|
import json
|
|
4
5
|
import tempfile
|
|
6
|
+
from unittest.mock import MagicMock, patch
|
|
5
7
|
from pathlib import Path
|
|
6
8
|
|
|
7
9
|
import pytest
|
|
@@ -21,7 +23,13 @@ from wup.models.config import (
|
|
|
21
23
|
VisualDiffConfig,
|
|
22
24
|
)
|
|
23
25
|
from wup.testql_watcher import TestQLWatcher
|
|
24
|
-
from wup.visual_diff import
|
|
26
|
+
from wup.visual_diff import (
|
|
27
|
+
VisualDiffer,
|
|
28
|
+
_diff_snapshots,
|
|
29
|
+
_looks_like_visual_page,
|
|
30
|
+
_page_slug,
|
|
31
|
+
_resolve_base_url,
|
|
32
|
+
)
|
|
25
33
|
|
|
26
34
|
|
|
27
35
|
class TestDependencyMapper:
|
|
@@ -603,6 +611,50 @@ class TestWupWatcher:
|
|
|
603
611
|
|
|
604
612
|
# No filtering should occur
|
|
605
613
|
|
|
614
|
+
def test_create_and_start_observer_fallback_on_enospc(self):
|
|
615
|
+
"""Fallback to PollingObserver when inotify watch limit is reached."""
|
|
616
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
617
|
+
watcher = WupWatcher(tmpdir)
|
|
618
|
+
event_handler = MagicMock()
|
|
619
|
+
fake_observer = MagicMock()
|
|
620
|
+
fake_observer.start.side_effect = OSError(errno.ENOSPC, "inotify watch limit reached")
|
|
621
|
+
|
|
622
|
+
with patch("wup.core.Observer", return_value=fake_observer):
|
|
623
|
+
with patch("wup.core.PollingObserver") as mock_polling:
|
|
624
|
+
fake_polling = MagicMock()
|
|
625
|
+
mock_polling.return_value = fake_polling
|
|
626
|
+
result = watcher._create_and_start_observer(event_handler, [tmpdir])
|
|
627
|
+
assert result is fake_polling
|
|
628
|
+
fake_polling.start.assert_called_once()
|
|
629
|
+
|
|
630
|
+
def test_create_and_start_observer_fallback_on_emfile(self):
|
|
631
|
+
"""Fallback to PollingObserver when inotify instance limit is reached."""
|
|
632
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
633
|
+
watcher = WupWatcher(tmpdir)
|
|
634
|
+
event_handler = MagicMock()
|
|
635
|
+
fake_observer = MagicMock()
|
|
636
|
+
fake_observer.start.side_effect = OSError(errno.EMFILE, "inotify instance limit reached")
|
|
637
|
+
|
|
638
|
+
with patch("wup.core.Observer", return_value=fake_observer):
|
|
639
|
+
with patch("wup.core.PollingObserver") as mock_polling:
|
|
640
|
+
fake_polling = MagicMock()
|
|
641
|
+
mock_polling.return_value = fake_polling
|
|
642
|
+
result = watcher._create_and_start_observer(event_handler, [tmpdir])
|
|
643
|
+
assert result is fake_polling
|
|
644
|
+
fake_polling.start.assert_called_once()
|
|
645
|
+
|
|
646
|
+
def test_create_and_start_observer_reraises_other_oserror(self):
|
|
647
|
+
"""Re-raise OSError that is not ENOSPC or EMFILE."""
|
|
648
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
649
|
+
watcher = WupWatcher(tmpdir)
|
|
650
|
+
event_handler = MagicMock()
|
|
651
|
+
fake_observer = MagicMock()
|
|
652
|
+
fake_observer.start.side_effect = OSError(errno.EACCES, "permission denied")
|
|
653
|
+
|
|
654
|
+
with patch("wup.core.Observer", return_value=fake_observer):
|
|
655
|
+
with pytest.raises(OSError, match="permission denied"):
|
|
656
|
+
watcher._create_and_start_observer(event_handler, [tmpdir])
|
|
657
|
+
|
|
606
658
|
|
|
607
659
|
class TestIntegrationWorkflow:
|
|
608
660
|
"""Integration tests for complete workflows."""
|
|
@@ -994,9 +1046,29 @@ class TestVisualDiffer:
|
|
|
994
1046
|
pages_from_endpoints=True,
|
|
995
1047
|
)
|
|
996
1048
|
differ = VisualDiffer(tmpdir, cfg)
|
|
997
|
-
pages = differ._pages_for_service("users", ["/
|
|
998
|
-
assert "http://localhost:8080/
|
|
999
|
-
assert "http://localhost:8080/
|
|
1049
|
+
pages = differ._pages_for_service("users", ["/connect-users", "/connect-users/1"])
|
|
1050
|
+
assert "http://localhost:8080/connect-users" in pages
|
|
1051
|
+
assert "http://localhost:8080/connect-users/1" in pages
|
|
1052
|
+
|
|
1053
|
+
def test_looks_like_visual_page_skips_api_health_routes(self):
|
|
1054
|
+
assert _looks_like_visual_page("http://localhost:8101/api/v3/health") is False
|
|
1055
|
+
assert _looks_like_visual_page("http://localhost:8100/firmware/api/v1/execution/status") is False
|
|
1056
|
+
assert _looks_like_visual_page("http://localhost:8100/connect-config") is True
|
|
1057
|
+
|
|
1058
|
+
def test_pages_for_service_from_endpoints_skips_non_html_probes(self):
|
|
1059
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1060
|
+
cfg = VisualDiffConfig(
|
|
1061
|
+
base_url="http://localhost:8080",
|
|
1062
|
+
pages_from_endpoints=True,
|
|
1063
|
+
)
|
|
1064
|
+
differ = VisualDiffer(tmpdir, cfg)
|
|
1065
|
+
pages = differ._pages_for_service(
|
|
1066
|
+
"backend",
|
|
1067
|
+
["/api/v3/health", "/connect-config", "http://localhost:8080/connect-data"],
|
|
1068
|
+
)
|
|
1069
|
+
assert "http://localhost:8080/api/v3/health" not in pages
|
|
1070
|
+
assert "http://localhost:8080/connect-config" in pages
|
|
1071
|
+
assert "http://localhost:8080/connect-data" in pages
|
|
1000
1072
|
|
|
1001
1073
|
def test_pages_for_service_fallback(self):
|
|
1002
1074
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
@@ -1012,12 +1084,12 @@ class TestVisualDiffer:
|
|
|
1012
1084
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1013
1085
|
cfg = VisualDiffConfig(
|
|
1014
1086
|
base_url="http://localhost:8080",
|
|
1015
|
-
pages=["https://example.com/
|
|
1087
|
+
pages=["https://example.com/connect-dashboard"],
|
|
1016
1088
|
pages_from_endpoints=False,
|
|
1017
1089
|
)
|
|
1018
1090
|
differ = VisualDiffer(tmpdir, cfg)
|
|
1019
1091
|
pages = differ._pages_for_service("svc", [])
|
|
1020
|
-
assert pages == ["https://example.com/
|
|
1092
|
+
assert pages == ["https://example.com/connect-dashboard"]
|
|
1021
1093
|
|
|
1022
1094
|
def test_diff_snapshots_baseline(self):
|
|
1023
1095
|
new = {"tag": "HTML", "children": [{"tag": "BODY"}]}
|
|
@@ -1055,6 +1127,45 @@ class TestVisualDiffer:
|
|
|
1055
1127
|
results = asyncio.run(differ.run_for_service("svc", ["/x"]))
|
|
1056
1128
|
assert results == []
|
|
1057
1129
|
|
|
1130
|
+
def test_run_for_service_summarizes_fetch_errors(self, monkeypatch):
|
|
1131
|
+
"""Fetch failures should be reported once per service, not once per page."""
|
|
1132
|
+
import asyncio
|
|
1133
|
+
from wup import visual_diff as visual_diff_module
|
|
1134
|
+
|
|
1135
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1136
|
+
cfg = VisualDiffConfig(
|
|
1137
|
+
enabled=True,
|
|
1138
|
+
base_url="http://localhost:8100",
|
|
1139
|
+
pages_from_endpoints=True,
|
|
1140
|
+
delay_seconds=0,
|
|
1141
|
+
)
|
|
1142
|
+
differ = VisualDiffer(tmpdir, cfg)
|
|
1143
|
+
|
|
1144
|
+
async def fake_check_page(service, url):
|
|
1145
|
+
return {
|
|
1146
|
+
"url": url,
|
|
1147
|
+
"diff": {
|
|
1148
|
+
"status": "error",
|
|
1149
|
+
"message": "BrowserType.launch: Executable doesn't exist at /tmp/chrome",
|
|
1150
|
+
},
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
printed = []
|
|
1154
|
+
|
|
1155
|
+
monkeypatch.setattr(visual_diff_module, "_playwright_available", lambda: True)
|
|
1156
|
+
monkeypatch.setattr(differ, "_check_page", fake_check_page)
|
|
1157
|
+
monkeypatch.setattr(visual_diff_module.console, "print", lambda message: printed.append(str(message)))
|
|
1158
|
+
|
|
1159
|
+
results = asyncio.run(
|
|
1160
|
+
differ.run_for_service("frontend", ["/a", "/b", "/c", "/d"])
|
|
1161
|
+
)
|
|
1162
|
+
|
|
1163
|
+
assert len(results) == 4
|
|
1164
|
+
assert len(printed) == 1
|
|
1165
|
+
assert "Visual diff skipped for frontend: 4 page(s) failed to fetch" in printed[0]
|
|
1166
|
+
assert "/a, /b, /c (+1 more)" in printed[0]
|
|
1167
|
+
assert "4x BrowserType.launch: Executable doesn't exist at /tmp/chrome" in printed[0]
|
|
1168
|
+
|
|
1058
1169
|
def test_get_recent_diffs_empty(self):
|
|
1059
1170
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1060
1171
|
cfg = VisualDiffConfig()
|
|
@@ -1315,8 +1426,29 @@ visual_diff:
|
|
|
1315
1426
|
assert vd.base_url == ""
|
|
1316
1427
|
assert vd.max_depth == 10
|
|
1317
1428
|
assert vd.pages == []
|
|
1429
|
+
assert vd.pages_from_endpoints is True
|
|
1318
1430
|
assert vd.headless is True
|
|
1319
1431
|
|
|
1432
|
+
def test_load_config_visual_diff_env_overrides_page_discovery(self, monkeypatch):
|
|
1433
|
+
"""Env can widen visual page discovery for large frontend apps."""
|
|
1434
|
+
monkeypatch.setenv("WUP_VISUAL_DIFF_MAX_PAGES", "200")
|
|
1435
|
+
monkeypatch.setenv("WUP_VISUAL_DIFF_PAGES_FROM_ENDPOINTS", "true")
|
|
1436
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1437
|
+
config_path = Path(tmpdir) / "wup.yaml"
|
|
1438
|
+
config_path.write_text(
|
|
1439
|
+
"project:\n"
|
|
1440
|
+
" name: x\n"
|
|
1441
|
+
"visual_diff:\n"
|
|
1442
|
+
" enabled: true\n"
|
|
1443
|
+
" pages_from_endpoints: false\n"
|
|
1444
|
+
" max_pages: 5\n",
|
|
1445
|
+
encoding="utf-8",
|
|
1446
|
+
)
|
|
1447
|
+
config = load_config(Path(tmpdir), config_path)
|
|
1448
|
+
vd = config.visual_diff
|
|
1449
|
+
assert vd.pages_from_endpoints is True
|
|
1450
|
+
assert vd.max_pages == 200
|
|
1451
|
+
|
|
1320
1452
|
def test_load_dotenv_sets_env_var(self):
|
|
1321
1453
|
"""_load_dotenv should load .wup.env into os.environ."""
|
|
1322
1454
|
import os
|
|
@@ -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.33"
|
|
11
11
|
__author__ = "Tom Sapletta"
|
|
12
12
|
|
|
13
13
|
from .config import load_config, save_config, get_default_config
|
|
@@ -23,6 +23,100 @@ app = typer.Typer(
|
|
|
23
23
|
console = Console()
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
def _load_watch_config(
|
|
27
|
+
project_path: Path,
|
|
28
|
+
config_path: Optional[Path],
|
|
29
|
+
probe_interval: Optional[int],
|
|
30
|
+
mode: str,
|
|
31
|
+
) -> WupConfig:
|
|
32
|
+
"""Load wup.yaml config and apply CLI probe_interval override."""
|
|
33
|
+
wup_config = load_config(project_path, config_path)
|
|
34
|
+
if probe_interval is not None:
|
|
35
|
+
wup_config.testql.probe_interval_s = int(probe_interval)
|
|
36
|
+
elif mode.lower() == "testql" and not wup_config.testql.probe_interval_s:
|
|
37
|
+
wup_config.testql.probe_interval_s = 60
|
|
38
|
+
return wup_config
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _print_watch_header(
|
|
42
|
+
wup_config: WupConfig,
|
|
43
|
+
cpu_throttle: float,
|
|
44
|
+
debounce: int,
|
|
45
|
+
cooldown: int,
|
|
46
|
+
config_path: Optional[Path],
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Print watcher startup banner."""
|
|
49
|
+
console.print(f"[bold cyan]🚀 WUP Watcher[/bold cyan]")
|
|
50
|
+
console.print(f"[dim]Project: {wup_config.project.name}[/dim]")
|
|
51
|
+
console.print(f"[dim]Description: {wup_config.project.description}[/dim]")
|
|
52
|
+
console.print(f"[dim]CPU Throttle: {cpu_throttle * 100}%[/dim]")
|
|
53
|
+
console.print(f"[dim]Debounce: {debounce}s[/dim]")
|
|
54
|
+
console.print(f"[dim]Cooldown: {cooldown}s[/dim]")
|
|
55
|
+
if wup_config.testql.probe_interval_s:
|
|
56
|
+
console.print(f"[dim]Live probes: every {wup_config.testql.probe_interval_s}s[/dim]")
|
|
57
|
+
console.print(f"[dim]Config: {config_path or 'auto-detected'}[/dim]")
|
|
58
|
+
console.print()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _refresh_monitoring_manifest(
|
|
62
|
+
project_path: Path,
|
|
63
|
+
wup_config: WupConfig,
|
|
64
|
+
cfg_path: Optional[Path],
|
|
65
|
+
) -> None:
|
|
66
|
+
"""Rebuild and patch monitoring manifest into wup.yaml when possible."""
|
|
67
|
+
if not cfg_path:
|
|
68
|
+
return
|
|
69
|
+
from .monitoring_manifest import build_monitoring_manifest, patch_wup_yaml_monitoring
|
|
70
|
+
try:
|
|
71
|
+
manifest = build_monitoring_manifest(project_path, wup_config)
|
|
72
|
+
patch_wup_yaml_monitoring(cfg_path, manifest)
|
|
73
|
+
console.print("[dim]Refreshed monitoring manifest in wup.yaml[/dim]")
|
|
74
|
+
except OSError as exc:
|
|
75
|
+
console.print(f"[yellow]Could not refresh monitoring manifest: {exc}[/yellow]")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _create_watcher(
|
|
79
|
+
mode: str,
|
|
80
|
+
project_path: Path,
|
|
81
|
+
deps_file: str,
|
|
82
|
+
cpu_throttle: float,
|
|
83
|
+
debounce: int,
|
|
84
|
+
cooldown: int,
|
|
85
|
+
scenarios_dir: Optional[str],
|
|
86
|
+
testql_bin: str,
|
|
87
|
+
browser_service_url: Optional[str],
|
|
88
|
+
track_dir: str,
|
|
89
|
+
quick_limit: int,
|
|
90
|
+
config: WupConfig,
|
|
91
|
+
) -> WupWatcher:
|
|
92
|
+
"""Instantiate the correct watcher class for the chosen mode."""
|
|
93
|
+
if mode.lower() == "testql":
|
|
94
|
+
watcher = TestQLWatcher(
|
|
95
|
+
project_root=str(project_path),
|
|
96
|
+
deps_file=deps_file,
|
|
97
|
+
cpu_throttle=cpu_throttle,
|
|
98
|
+
debounce_seconds=debounce,
|
|
99
|
+
test_cooldown_seconds=cooldown,
|
|
100
|
+
scenarios_dir=scenarios_dir,
|
|
101
|
+
testql_bin=testql_bin,
|
|
102
|
+
browser_service_url=browser_service_url,
|
|
103
|
+
track_dir=track_dir,
|
|
104
|
+
quick_limit=quick_limit,
|
|
105
|
+
config=config,
|
|
106
|
+
)
|
|
107
|
+
console.print("[green]TestQL mode enabled[/green]")
|
|
108
|
+
return watcher
|
|
109
|
+
|
|
110
|
+
return WupWatcher(
|
|
111
|
+
project_root=str(project_path),
|
|
112
|
+
deps_file=deps_file,
|
|
113
|
+
cpu_throttle=cpu_throttle,
|
|
114
|
+
debounce_seconds=debounce,
|
|
115
|
+
test_cooldown_seconds=cooldown,
|
|
116
|
+
config=config,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
26
120
|
@app.command()
|
|
27
121
|
def watch(
|
|
28
122
|
project: str = typer.Argument(".", help="Path to the project root directory"),
|
|
@@ -60,67 +154,35 @@ def watch(
|
|
|
60
154
|
``--mode default`` for the legacy HTTP-only watcher without TestQL.
|
|
61
155
|
"""
|
|
62
156
|
project_path = Path(project).resolve()
|
|
63
|
-
|
|
157
|
+
|
|
64
158
|
if not project_path.exists():
|
|
65
159
|
console.print(f"[red]Error: Project path '{project}' does not exist[/red]")
|
|
66
160
|
raise typer.Exit(1)
|
|
67
|
-
|
|
68
|
-
# Load configuration
|
|
69
|
-
config_path = Path(config) if config else None
|
|
70
|
-
wup_config = load_config(project_path, config_path)
|
|
71
|
-
if probe_interval is not None:
|
|
72
|
-
wup_config.testql.probe_interval_s = int(probe_interval)
|
|
73
|
-
elif mode.lower() == "testql" and not wup_config.testql.probe_interval_s:
|
|
74
|
-
wup_config.testql.probe_interval_s = 60
|
|
75
161
|
|
|
162
|
+
config_path = Path(config) if config else None
|
|
163
|
+
wup_config = _load_watch_config(project_path, config_path, probe_interval, mode)
|
|
76
164
|
effective_scenarios_dir = scenarios_dir or wup_config.testql.scenario_dir
|
|
77
165
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
console.print(f"[dim]Description: {wup_config.project.description}[/dim]")
|
|
81
|
-
console.print(f"[dim]CPU Throttle: {cpu_throttle * 100}%[/dim]")
|
|
82
|
-
console.print(f"[dim]Debounce: {debounce}s[/dim]")
|
|
83
|
-
console.print(f"[dim]Cooldown: {cooldown}s[/dim]")
|
|
84
|
-
if wup_config.testql.probe_interval_s:
|
|
85
|
-
console.print(f"[dim]Live probes: every {wup_config.testql.probe_interval_s}s[/dim]")
|
|
86
|
-
console.print(f"[dim]Config: {config_path or 'auto-detected'}[/dim]")
|
|
87
|
-
console.print()
|
|
88
|
-
|
|
166
|
+
_print_watch_header(wup_config, cpu_throttle, debounce, cooldown, config_path)
|
|
167
|
+
|
|
89
168
|
cfg_path = config_path if config_path and config_path.exists() else find_config_file(project_path)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
169
|
+
_refresh_monitoring_manifest(project_path, wup_config, cfg_path)
|
|
170
|
+
|
|
171
|
+
watcher = _create_watcher(
|
|
172
|
+
mode=mode,
|
|
173
|
+
project_path=project_path,
|
|
174
|
+
deps_file=deps_file,
|
|
175
|
+
cpu_throttle=cpu_throttle,
|
|
176
|
+
debounce=debounce,
|
|
177
|
+
cooldown=cooldown,
|
|
178
|
+
scenarios_dir=effective_scenarios_dir,
|
|
179
|
+
testql_bin=testql_bin,
|
|
180
|
+
browser_service_url=browser_service_url,
|
|
181
|
+
track_dir=track_dir,
|
|
182
|
+
quick_limit=quick_limit,
|
|
183
|
+
config=wup_config,
|
|
184
|
+
)
|
|
98
185
|
|
|
99
|
-
if mode.lower() == "testql":
|
|
100
|
-
watcher = TestQLWatcher(
|
|
101
|
-
project_root=str(project_path),
|
|
102
|
-
deps_file=deps_file,
|
|
103
|
-
cpu_throttle=cpu_throttle,
|
|
104
|
-
debounce_seconds=debounce,
|
|
105
|
-
test_cooldown_seconds=cooldown,
|
|
106
|
-
scenarios_dir=effective_scenarios_dir,
|
|
107
|
-
testql_bin=testql_bin,
|
|
108
|
-
browser_service_url=browser_service_url,
|
|
109
|
-
track_dir=track_dir,
|
|
110
|
-
quick_limit=quick_limit,
|
|
111
|
-
config=wup_config,
|
|
112
|
-
)
|
|
113
|
-
console.print("[green]TestQL mode enabled[/green]")
|
|
114
|
-
else:
|
|
115
|
-
watcher = WupWatcher(
|
|
116
|
-
project_root=str(project_path),
|
|
117
|
-
deps_file=deps_file,
|
|
118
|
-
cpu_throttle=cpu_throttle,
|
|
119
|
-
debounce_seconds=debounce,
|
|
120
|
-
test_cooldown_seconds=cooldown,
|
|
121
|
-
config=wup_config,
|
|
122
|
-
)
|
|
123
|
-
|
|
124
186
|
if dashboard:
|
|
125
187
|
console.print("[green]Starting watcher with live dashboard...[/green]")
|
|
126
188
|
asyncio.run(watcher.run_with_dashboard())
|