wup 0.2.46__tar.gz → 0.2.48__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.46/wup.egg-info → wup-0.2.48}/PKG-INFO +7 -7
- {wup-0.2.46 → wup-0.2.48}/README.md +6 -6
- {wup-0.2.46 → wup-0.2.48}/pyproject.toml +1 -1
- {wup-0.2.46 → wup-0.2.48}/tests/test_testql_watcher.py +38 -0
- wup-0.2.48/tests/test_visual_diff_periodic_skip.py +40 -0
- wup-0.2.48/tests/test_visual_diff_progress.py +39 -0
- {wup-0.2.46 → wup-0.2.48}/wup/__init__.py +1 -1
- {wup-0.2.46 → wup-0.2.48}/wup/models/config.py +3 -0
- {wup-0.2.46 → wup-0.2.48}/wup/testing/handlers/event_handlers.py +7 -1
- {wup-0.2.46 → wup-0.2.48}/wup/testql_watcher.py +51 -12
- {wup-0.2.46 → wup-0.2.48}/wup/visual_diff.py +42 -4
- {wup-0.2.46 → wup-0.2.48/wup.egg-info}/PKG-INFO +7 -7
- {wup-0.2.46 → wup-0.2.48}/wup.egg-info/SOURCES.txt +2 -0
- {wup-0.2.46 → wup-0.2.48}/LICENSE +0 -0
- {wup-0.2.46 → wup-0.2.48}/setup.cfg +0 -0
- {wup-0.2.46 → wup-0.2.48}/tests/test_auto_detection.py +0 -0
- {wup-0.2.46 → wup-0.2.48}/tests/test_cli_filtering.py +0 -0
- {wup-0.2.46 → wup-0.2.48}/tests/test_e2e.py +0 -0
- {wup-0.2.46 → wup-0.2.48}/tests/test_monitoring_manifest.py +0 -0
- {wup-0.2.46 → wup-0.2.48}/tests/test_service_inference.py +0 -0
- {wup-0.2.46 → wup-0.2.48}/tests/test_testql_monitor.py +0 -0
- {wup-0.2.46 → wup-0.2.48}/tests/test_web_client.py +0 -0
- {wup-0.2.46 → wup-0.2.48}/tests/test_wup.py +0 -0
- {wup-0.2.46 → wup-0.2.48}/wup/_ast_detector.py +0 -0
- {wup-0.2.46 → wup-0.2.48}/wup/_base_detector.py +0 -0
- {wup-0.2.46 → wup-0.2.48}/wup/_hash_detector.py +0 -0
- {wup-0.2.46 → wup-0.2.48}/wup/_yaml_detector.py +0 -0
- {wup-0.2.46 → wup-0.2.48}/wup/anomaly_detector.py +0 -0
- {wup-0.2.46 → wup-0.2.48}/wup/anomaly_models.py +0 -0
- {wup-0.2.46 → wup-0.2.48}/wup/assistant.py +0 -0
- {wup-0.2.46 → wup-0.2.48}/wup/bus.py +0 -0
- {wup-0.2.46 → wup-0.2.48}/wup/cli.py +0 -0
- {wup-0.2.46 → wup-0.2.48}/wup/cli_config_generator.py +0 -0
- {wup-0.2.46 → wup-0.2.48}/wup/cli_scanner.py +0 -0
- {wup-0.2.46 → wup-0.2.48}/wup/config.py +0 -0
- {wup-0.2.46 → wup-0.2.48}/wup/core.py +0 -0
- {wup-0.2.46 → wup-0.2.48}/wup/dependency_mapper.py +0 -0
- {wup-0.2.46 → wup-0.2.48}/wup/event_store.py +0 -0
- {wup-0.2.46 → wup-0.2.48}/wup/file_watcher/events/file_events.py +0 -0
- {wup-0.2.46 → wup-0.2.48}/wup/models/__init__.py +0 -0
- {wup-0.2.46 → wup-0.2.48}/wup/monitoring_manifest.py +0 -0
- {wup-0.2.46 → wup-0.2.48}/wup/planfile_reporter.py +0 -0
- {wup-0.2.46 → wup-0.2.48}/wup/testing/events/health_events.py +0 -0
- {wup-0.2.46 → wup-0.2.48}/wup/testing/events/test_results.py +0 -0
- {wup-0.2.46 → wup-0.2.48}/wup/testing/handlers/health_handlers.py +0 -0
- {wup-0.2.46 → wup-0.2.48}/wup/testing/queries/health_queries.py +0 -0
- {wup-0.2.46 → wup-0.2.48}/wup/testql_cli_generator.py +0 -0
- {wup-0.2.46 → wup-0.2.48}/wup/testql_discovery.py +0 -0
- {wup-0.2.46 → wup-0.2.48}/wup/testql_monitor.py +0 -0
- {wup-0.2.46 → wup-0.2.48}/wup/web_client.py +0 -0
- {wup-0.2.46 → wup-0.2.48}/wup.egg-info/dependency_links.txt +0 -0
- {wup-0.2.46 → wup-0.2.48}/wup.egg-info/entry_points.txt +0 -0
- {wup-0.2.46 → wup-0.2.48}/wup.egg-info/requires.txt +0 -0
- {wup-0.2.46 → wup-0.2.48}/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.48
|
|
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
|
|
@@ -31,17 +31,17 @@ Dynamic: license-file
|
|
|
31
31
|
|
|
32
32
|
## AI Cost Tracking
|
|
33
33
|
|
|
34
|
-
    
|
|
35
|
+
  
|
|
36
36
|
|
|
37
|
-
- 🤖 **LLM usage:** $3.
|
|
38
|
-
- 👤 **Human dev:** ~$
|
|
37
|
+
- 🤖 **LLM usage:** $3.3508 (60 commits)
|
|
38
|
+
- 👤 **Human dev:** ~$2573 (25.7h @ $100/h, 30min dedup)
|
|
39
39
|
|
|
40
|
-
Generated on 2026-05-
|
|
40
|
+
Generated on 2026-05-24 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
41
41
|
|
|
42
42
|
---
|
|
43
43
|
|
|
44
|
-
    
|
|
45
45
|
|
|
46
46
|
**WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
|
|
47
47
|
|
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
|
|
4
4
|
## AI Cost Tracking
|
|
5
5
|
|
|
6
|
-
    
|
|
7
|
+
  
|
|
8
8
|
|
|
9
|
-
- 🤖 **LLM usage:** $3.
|
|
10
|
-
- 👤 **Human dev:** ~$
|
|
9
|
+
- 🤖 **LLM usage:** $3.3508 (60 commits)
|
|
10
|
+
- 👤 **Human dev:** ~$2573 (25.7h @ $100/h, 30min dedup)
|
|
11
11
|
|
|
12
|
-
Generated on 2026-05-
|
|
12
|
+
Generated on 2026-05-24 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
|
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import json
|
|
3
3
|
import os
|
|
4
|
+
import signal
|
|
4
5
|
import tempfile
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from subprocess import CompletedProcess
|
|
7
8
|
from unittest.mock import Mock
|
|
8
9
|
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
9
12
|
from wup.testql_watcher import TestQLWatcher
|
|
10
13
|
from wup.models.config import (
|
|
11
14
|
PlanfileConfig,
|
|
@@ -519,3 +522,38 @@ def test_quick_pass_actions_prefer_config_endpoints_for_visual_diff():
|
|
|
519
522
|
)
|
|
520
523
|
|
|
521
524
|
assert differ.calls == [("backend", ["http://localhost:8101/api/v3/health"])]
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def test_quick_interrupt_does_not_create_failure_track():
|
|
528
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
529
|
+
root = Path(tmpdir)
|
|
530
|
+
scenario_dir = root / "testql-scenarios"
|
|
531
|
+
scenario_dir.mkdir(parents=True, exist_ok=True)
|
|
532
|
+
(scenario_dir / "connect-config-smoke.testql.toon.yaml").write_text("name: smoke\n", encoding="utf-8")
|
|
533
|
+
|
|
534
|
+
cfg = WupConfig(
|
|
535
|
+
project=ProjectConfig(name="demo"),
|
|
536
|
+
services=[ServiceConfig(name="connect-config", paths=["connect-config/**"])],
|
|
537
|
+
watch=WatchConfig(),
|
|
538
|
+
testql=TestQLConfig(scenario_dir="testql-scenarios"),
|
|
539
|
+
)
|
|
540
|
+
watcher = TestQLWatcher(
|
|
541
|
+
project_root=str(root),
|
|
542
|
+
deps_file=str(root / "deps.json"),
|
|
543
|
+
scenarios_dir="testql-scenarios",
|
|
544
|
+
track_dir=".wup/tracks",
|
|
545
|
+
config=cfg,
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
watcher._run_testql = lambda args, timeout: CompletedProcess( # type: ignore[method-assign]
|
|
549
|
+
args=args,
|
|
550
|
+
returncode=-signal.SIGINT,
|
|
551
|
+
stdout="",
|
|
552
|
+
stderr="",
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
with pytest.raises(KeyboardInterrupt):
|
|
556
|
+
asyncio.run(watcher.run_quick_test("connect-config", []))
|
|
557
|
+
|
|
558
|
+
tracks = list((root / ".wup" / "tracks").glob("*.json"))
|
|
559
|
+
assert tracks == []
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Regression: visual_diff is skipped during periodic probe cycles by default."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from unittest.mock import MagicMock
|
|
6
|
+
|
|
7
|
+
from wup.models.config import VisualDiffConfig
|
|
8
|
+
from wup.testql_watcher import TestQLWatcher
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _make_watcher(tmp_path, *, run_on_periodic_probe: bool) -> TestQLWatcher:
|
|
12
|
+
watcher = TestQLWatcher.__new__(TestQLWatcher)
|
|
13
|
+
differ = MagicMock()
|
|
14
|
+
differ.cfg = VisualDiffConfig(enabled=True, run_on_periodic_probe=run_on_periodic_probe)
|
|
15
|
+
watcher.visual_differ = differ
|
|
16
|
+
watcher._periodic_probe_in_progress = False
|
|
17
|
+
return watcher
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_visual_diff_runs_on_file_change_cycles(tmp_path) -> None:
|
|
21
|
+
watcher = _make_watcher(tmp_path, run_on_periodic_probe=False)
|
|
22
|
+
assert watcher._should_run_visual_diff() is True
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_visual_diff_skipped_on_periodic_probe_by_default(tmp_path) -> None:
|
|
26
|
+
watcher = _make_watcher(tmp_path, run_on_periodic_probe=False)
|
|
27
|
+
watcher._periodic_probe_in_progress = True
|
|
28
|
+
assert watcher._should_run_visual_diff() is False
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_visual_diff_runs_on_periodic_probe_when_opted_in(tmp_path) -> None:
|
|
32
|
+
watcher = _make_watcher(tmp_path, run_on_periodic_probe=True)
|
|
33
|
+
watcher._periodic_probe_in_progress = True
|
|
34
|
+
assert watcher._should_run_visual_diff() is True
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_visual_diff_skipped_when_disabled(tmp_path) -> None:
|
|
38
|
+
watcher = _make_watcher(tmp_path, run_on_periodic_probe=True)
|
|
39
|
+
watcher.visual_differ.cfg.enabled = False
|
|
40
|
+
assert watcher._should_run_visual_diff() is False
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Regression: visual_diff shows a progress bar for large scans."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
from wup.models.config import VisualDiffConfig
|
|
8
|
+
from wup.visual_diff import VisualDiffer
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _make_differ(tmp_path) -> VisualDiffer:
|
|
12
|
+
cfg = VisualDiffConfig(
|
|
13
|
+
enabled=True,
|
|
14
|
+
base_url="http://localhost:8100",
|
|
15
|
+
snapshot_dir=str(tmp_path / "snap"),
|
|
16
|
+
diff_dir=str(tmp_path / "diff"),
|
|
17
|
+
pages_from_endpoints=True,
|
|
18
|
+
max_pages=200,
|
|
19
|
+
)
|
|
20
|
+
return VisualDiffer(str(tmp_path), cfg)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_progress_returned_for_big_scans(tmp_path, monkeypatch) -> None:
|
|
24
|
+
monkeypatch.delenv("WUP_VISUAL_DIFF_PROGRESS", raising=False)
|
|
25
|
+
differ = _make_differ(tmp_path)
|
|
26
|
+
progress = differ._build_progress("frontend", total=20)
|
|
27
|
+
assert progress is not None
|
|
28
|
+
assert progress.live.transient is True
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_progress_skipped_for_small_scans(tmp_path) -> None:
|
|
32
|
+
differ = _make_differ(tmp_path)
|
|
33
|
+
assert differ._build_progress("frontend", total=2) is None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_progress_can_be_disabled_via_env(tmp_path, monkeypatch) -> None:
|
|
37
|
+
monkeypatch.setenv("WUP_VISUAL_DIFF_PROGRESS", "0")
|
|
38
|
+
differ = _make_differ(tmp_path)
|
|
39
|
+
assert differ._build_progress("frontend", total=50) is None
|
|
@@ -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.48"
|
|
11
11
|
__author__ = "Tom Sapletta"
|
|
12
12
|
|
|
13
13
|
from .config import load_config, save_config, get_default_config
|
|
@@ -100,6 +100,9 @@ class VisualDiffConfig:
|
|
|
100
100
|
"[class*='error'][class*='container']",
|
|
101
101
|
])
|
|
102
102
|
headless: bool = True
|
|
103
|
+
# Run visual_diff during periodic probe cycles too. Default false: visual_diff
|
|
104
|
+
# only runs when something on disk actually changed (or on first cycle).
|
|
105
|
+
run_on_periodic_probe: bool = False
|
|
103
106
|
|
|
104
107
|
|
|
105
108
|
@dataclass
|
|
@@ -35,7 +35,13 @@ class TestResultEventHandler:
|
|
|
35
35
|
)
|
|
36
36
|
)
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
reason = (event.reason or "TestQL failed").strip().splitlines()[-1]
|
|
39
|
+
if len(reason) > 160:
|
|
40
|
+
reason = reason[:157] + "..."
|
|
41
|
+
self.console.print(
|
|
42
|
+
f"[red]✗ {event.stage.capitalize()} failed: {event.scenario.name} — {reason}[/red]\n"
|
|
43
|
+
f"[dim] track: {event.track_file}[/dim]"
|
|
44
|
+
)
|
|
39
45
|
|
|
40
46
|
def handle_test_passed(self, event: ScenarioPassed) -> None:
|
|
41
47
|
"""Handle scenario pass."""
|
|
@@ -6,6 +6,7 @@ import asyncio
|
|
|
6
6
|
import json
|
|
7
7
|
import os
|
|
8
8
|
import re
|
|
9
|
+
import signal
|
|
9
10
|
import subprocess
|
|
10
11
|
import time
|
|
11
12
|
from pathlib import Path
|
|
@@ -116,6 +117,7 @@ class TestQLWatcher(WupWatcher):
|
|
|
116
117
|
)
|
|
117
118
|
|
|
118
119
|
self._probe_thread = None
|
|
120
|
+
self._periodic_probe_in_progress = False
|
|
119
121
|
self._normalize_fleet_health_entry()
|
|
120
122
|
|
|
121
123
|
def _normalize_fleet_health_entry(self) -> None:
|
|
@@ -405,12 +407,23 @@ class TestQLWatcher(WupWatcher):
|
|
|
405
407
|
stderr=f"Error: TestQL command timed out after {timeout} seconds",
|
|
406
408
|
)
|
|
407
409
|
|
|
410
|
+
@staticmethod
|
|
411
|
+
def _is_interrupted_result(result: subprocess.CompletedProcess) -> bool:
|
|
412
|
+
rc = int(result.returncode)
|
|
413
|
+
if rc in {130, 143}:
|
|
414
|
+
return True
|
|
415
|
+
if rc < 0 and (-rc) in {signal.SIGINT, signal.SIGTERM}:
|
|
416
|
+
return True
|
|
417
|
+
return False
|
|
418
|
+
|
|
408
419
|
def _write_track(self, *, service: str, stage: str, scenario: Optional[Path], result: subprocess.CompletedProcess) -> Path:
|
|
409
420
|
ts = int(time.time())
|
|
410
421
|
safe_service = service.replace("/", "_").replace("\\", "_")
|
|
411
422
|
scenario_name = scenario.name if scenario else "unknown"
|
|
412
|
-
|
|
413
|
-
|
|
423
|
+
stderr_lines = (result.stderr or "").strip().splitlines()
|
|
424
|
+
stdout_lines = (result.stdout or "").strip().splitlines()
|
|
425
|
+
stderr_tail = stderr_lines[-3:] if stderr_lines else []
|
|
426
|
+
stdout_tail = stdout_lines[-5:] if stdout_lines else []
|
|
414
427
|
|
|
415
428
|
payload = {
|
|
416
429
|
"service": service,
|
|
@@ -418,8 +431,11 @@ class TestQLWatcher(WupWatcher):
|
|
|
418
431
|
"scenario": str(scenario) if scenario else None,
|
|
419
432
|
"command": result.args,
|
|
420
433
|
"returncode": result.returncode,
|
|
421
|
-
"stderr_head":
|
|
422
|
-
"stdout_head":
|
|
434
|
+
"stderr_head": stderr_lines[0] if stderr_lines else "",
|
|
435
|
+
"stdout_head": stdout_lines[0] if stdout_lines else "",
|
|
436
|
+
"stderr_tail": stderr_tail,
|
|
437
|
+
"stdout_tail": stdout_tail,
|
|
438
|
+
"failure_summary": self._summarize_testql_failure(result),
|
|
423
439
|
"track": {
|
|
424
440
|
"file": str(scenario) if scenario else "",
|
|
425
441
|
"line": 1,
|
|
@@ -465,8 +481,10 @@ class TestQLWatcher(WupWatcher):
|
|
|
465
481
|
result = self._run_testql(args, timeout=self._quick_timeout())
|
|
466
482
|
if result.returncode == 0:
|
|
467
483
|
return True
|
|
484
|
+
if self._is_interrupted_result(result):
|
|
485
|
+
raise KeyboardInterrupt
|
|
468
486
|
|
|
469
|
-
reason =
|
|
487
|
+
reason = self._summarize_testql_failure(result)
|
|
470
488
|
track_path = self._write_track(service=service, stage="quick",
|
|
471
489
|
scenario=scenario, result=result)
|
|
472
490
|
|
|
@@ -485,6 +503,13 @@ class TestQLWatcher(WupWatcher):
|
|
|
485
503
|
|
|
486
504
|
return False
|
|
487
505
|
|
|
506
|
+
def _should_run_visual_diff(self) -> bool:
|
|
507
|
+
if not (self.visual_differ and self.visual_differ.cfg.enabled):
|
|
508
|
+
return False
|
|
509
|
+
if not getattr(self, "_periodic_probe_in_progress", False):
|
|
510
|
+
return True
|
|
511
|
+
return bool(getattr(self.visual_differ.cfg, "run_on_periodic_probe", False))
|
|
512
|
+
|
|
488
513
|
async def _quick_pass_actions(self, service: str, merged_endpoints: List[str]) -> None:
|
|
489
514
|
"""Actions to perform after all quick scenarios pass."""
|
|
490
515
|
self._record_health_transition(service=service, status="up", stage="quick",
|
|
@@ -492,7 +517,7 @@ class TestQLWatcher(WupWatcher):
|
|
|
492
517
|
if self.web_client.is_active:
|
|
493
518
|
await self.web_client.send_pass(service=service, stage="quick")
|
|
494
519
|
self.console.print(f"[green]✓ Quick TestQL passed for {service}[/green]")
|
|
495
|
-
if self.
|
|
520
|
+
if self._should_run_visual_diff():
|
|
496
521
|
visual_endpoints = self._get_config_endpoints_for_service(service) or merged_endpoints
|
|
497
522
|
visual_results = await self.visual_differ.run_for_service(service, visual_endpoints)
|
|
498
523
|
visual_issues = [
|
|
@@ -593,6 +618,16 @@ class TestQLWatcher(WupWatcher):
|
|
|
593
618
|
return stripped
|
|
594
619
|
return None
|
|
595
620
|
|
|
621
|
+
@staticmethod
|
|
622
|
+
def _summarize_testql_failure(result: subprocess.CompletedProcess) -> str:
|
|
623
|
+
"""Short failure line for tracks and console (last lines of testql output)."""
|
|
624
|
+
if int(result.returncode) == 124:
|
|
625
|
+
return "TestQL subprocess timed out"
|
|
626
|
+
summary = TestQLWatcher._summarize_health_scenario_failure(result)
|
|
627
|
+
if summary != "health_scenario failed":
|
|
628
|
+
return summary
|
|
629
|
+
return "TestQL command failed"
|
|
630
|
+
|
|
596
631
|
@staticmethod
|
|
597
632
|
def _summarize_health_scenario_failure(result: subprocess.CompletedProcess) -> str:
|
|
598
633
|
"""Extract a short human summary from TestQL --output json (avoid trailing '}')."""
|
|
@@ -834,15 +869,19 @@ class TestQLWatcher(WupWatcher):
|
|
|
834
869
|
if not self.config.services:
|
|
835
870
|
return
|
|
836
871
|
self.console.print("[cyan]⟳ Periodic live probe cycle[/cyan]")
|
|
872
|
+
self._periodic_probe_in_progress = True
|
|
837
873
|
try:
|
|
838
|
-
asyncio.run(self._run_fleet_health_scenario())
|
|
839
|
-
except Exception as exc: # noqa: BLE001
|
|
840
|
-
self.console.print(f"[red]Fleet health scenario error: {exc}[/red]")
|
|
841
|
-
for svc in self.config.services:
|
|
842
874
|
try:
|
|
843
|
-
asyncio.run(self.
|
|
875
|
+
asyncio.run(self._run_fleet_health_scenario())
|
|
844
876
|
except Exception as exc: # noqa: BLE001
|
|
845
|
-
self.console.print(f"[red]
|
|
877
|
+
self.console.print(f"[red]Fleet health scenario error: {exc}[/red]")
|
|
878
|
+
for svc in self.config.services:
|
|
879
|
+
try:
|
|
880
|
+
asyncio.run(self.run_quick_test(svc.name, []))
|
|
881
|
+
except Exception as exc: # noqa: BLE001
|
|
882
|
+
self.console.print(f"[red]Probe error for {svc.name}: {exc}[/red]")
|
|
883
|
+
finally:
|
|
884
|
+
self._periodic_probe_in_progress = False
|
|
846
885
|
|
|
847
886
|
def _start_periodic_probe_thread(self) -> None:
|
|
848
887
|
import threading
|
|
@@ -24,6 +24,14 @@ from typing import Any, Dict, List, Optional, Tuple
|
|
|
24
24
|
from urllib.parse import urlparse
|
|
25
25
|
|
|
26
26
|
from rich.console import Console
|
|
27
|
+
from rich.progress import (
|
|
28
|
+
BarColumn,
|
|
29
|
+
MofNCompleteColumn,
|
|
30
|
+
Progress,
|
|
31
|
+
TextColumn,
|
|
32
|
+
TimeElapsedColumn,
|
|
33
|
+
TimeRemainingColumn,
|
|
34
|
+
)
|
|
27
35
|
|
|
28
36
|
from .models.config import VisualDiffConfig
|
|
29
37
|
|
|
@@ -442,14 +450,44 @@ class VisualDiffer:
|
|
|
442
450
|
new_urls: List[str] = []
|
|
443
451
|
error_results: List[Tuple[str, str]] = []
|
|
444
452
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
453
|
+
progress = self._build_progress(service, len(pages))
|
|
454
|
+
if progress is None:
|
|
455
|
+
for url in pages:
|
|
456
|
+
result = await self._check_page(service, url)
|
|
457
|
+
results.append(result)
|
|
458
|
+
self._categorize_page_result(service, url, result, ok_urls, new_urls, error_results)
|
|
459
|
+
else:
|
|
460
|
+
with progress:
|
|
461
|
+
task_id = progress.add_task(
|
|
462
|
+
f"[cyan]🔍 Visual diff {service}", total=len(pages), url=""
|
|
463
|
+
)
|
|
464
|
+
for url in pages:
|
|
465
|
+
progress.update(task_id, url=_short_url(url))
|
|
466
|
+
result = await self._check_page(service, url)
|
|
467
|
+
results.append(result)
|
|
468
|
+
self._categorize_page_result(service, url, result, ok_urls, new_urls, error_results)
|
|
469
|
+
progress.advance(task_id)
|
|
449
470
|
|
|
450
471
|
self._print_scan_summary(service, ok_urls, new_urls, error_results)
|
|
451
472
|
return results
|
|
452
473
|
|
|
474
|
+
def _build_progress(self, service: str, total: int) -> Optional[Progress]:
|
|
475
|
+
"""Return a rich Progress for big scans; None for tiny ones (avoid noise)."""
|
|
476
|
+
if total < 5 or os.environ.get("WUP_VISUAL_DIFF_PROGRESS", "1") == "0":
|
|
477
|
+
return None
|
|
478
|
+
return Progress(
|
|
479
|
+
TextColumn("[bold blue]{task.description}"),
|
|
480
|
+
BarColumn(bar_width=None),
|
|
481
|
+
MofNCompleteColumn(),
|
|
482
|
+
TextColumn("[dim]{task.fields[url]}"),
|
|
483
|
+
TimeElapsedColumn(),
|
|
484
|
+
TextColumn("eta"),
|
|
485
|
+
TimeRemainingColumn(),
|
|
486
|
+
console=console,
|
|
487
|
+
transient=True,
|
|
488
|
+
refresh_per_second=4,
|
|
489
|
+
)
|
|
490
|
+
|
|
453
491
|
async def _check_page(self, service: str, url: str) -> Dict[str, Any]:
|
|
454
492
|
snap_path = _snapshot_path(self.snapshot_dir, service, url)
|
|
455
493
|
old_snapshot = _load_snapshot(snap_path)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wup
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.48
|
|
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
|
|
@@ -31,17 +31,17 @@ Dynamic: license-file
|
|
|
31
31
|
|
|
32
32
|
## AI Cost Tracking
|
|
33
33
|
|
|
34
|
-
    
|
|
35
|
+
  
|
|
36
36
|
|
|
37
|
-
- 🤖 **LLM usage:** $3.
|
|
38
|
-
- 👤 **Human dev:** ~$
|
|
37
|
+
- 🤖 **LLM usage:** $3.3508 (60 commits)
|
|
38
|
+
- 👤 **Human dev:** ~$2573 (25.7h @ $100/h, 30min dedup)
|
|
39
39
|
|
|
40
|
-
Generated on 2026-05-
|
|
40
|
+
Generated on 2026-05-24 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
41
41
|
|
|
42
42
|
---
|
|
43
43
|
|
|
44
|
-
    
|
|
45
45
|
|
|
46
46
|
**WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
|
|
47
47
|
|
|
@@ -8,6 +8,8 @@ tests/test_monitoring_manifest.py
|
|
|
8
8
|
tests/test_service_inference.py
|
|
9
9
|
tests/test_testql_monitor.py
|
|
10
10
|
tests/test_testql_watcher.py
|
|
11
|
+
tests/test_visual_diff_periodic_skip.py
|
|
12
|
+
tests/test_visual_diff_progress.py
|
|
11
13
|
tests/test_web_client.py
|
|
12
14
|
tests/test_wup.py
|
|
13
15
|
wup/__init__.py
|
|
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
|