wup 0.2.24__tar.gz → 0.2.26__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.24/wup.egg-info → wup-0.2.26}/PKG-INFO +7 -7
- {wup-0.2.24 → wup-0.2.26}/README.md +6 -6
- {wup-0.2.24 → wup-0.2.26}/pyproject.toml +1 -1
- wup-0.2.26/tests/test_monitoring_manifest.py +72 -0
- wup-0.2.26/tests/test_testql_monitor.py +121 -0
- {wup-0.2.24 → wup-0.2.26}/wup/__init__.py +1 -1
- {wup-0.2.24 → wup-0.2.26}/wup/cli.py +133 -2
- {wup-0.2.24 → wup-0.2.26}/wup/config.py +6 -0
- {wup-0.2.24 → wup-0.2.26}/wup/core.py +61 -32
- {wup-0.2.24 → wup-0.2.26}/wup/models/config.py +4 -1
- wup-0.2.26/wup/monitoring_manifest.py +306 -0
- wup-0.2.26/wup/testql_monitor.py +329 -0
- {wup-0.2.24 → wup-0.2.26}/wup/testql_watcher.py +129 -0
- {wup-0.2.24 → wup-0.2.26/wup.egg-info}/PKG-INFO +7 -7
- {wup-0.2.24 → wup-0.2.26}/wup.egg-info/SOURCES.txt +4 -0
- {wup-0.2.24 → wup-0.2.26}/LICENSE +0 -0
- {wup-0.2.24 → wup-0.2.26}/setup.cfg +0 -0
- {wup-0.2.24 → wup-0.2.26}/tests/test_e2e.py +0 -0
- {wup-0.2.24 → wup-0.2.26}/tests/test_testql_watcher.py +0 -0
- {wup-0.2.24 → wup-0.2.26}/tests/test_web_client.py +0 -0
- {wup-0.2.24 → wup-0.2.26}/tests/test_wup.py +0 -0
- {wup-0.2.24 → wup-0.2.26}/wup/_ast_detector.py +0 -0
- {wup-0.2.24 → wup-0.2.26}/wup/_hash_detector.py +0 -0
- {wup-0.2.24 → wup-0.2.26}/wup/_yaml_detector.py +0 -0
- {wup-0.2.24 → wup-0.2.26}/wup/anomaly_detector.py +0 -0
- {wup-0.2.24 → wup-0.2.26}/wup/anomaly_models.py +0 -0
- {wup-0.2.24 → wup-0.2.26}/wup/assistant.py +0 -0
- {wup-0.2.24 → wup-0.2.26}/wup/dependency_mapper.py +0 -0
- {wup-0.2.24 → wup-0.2.26}/wup/models/__init__.py +0 -0
- {wup-0.2.24 → wup-0.2.26}/wup/testql_discovery.py +0 -0
- {wup-0.2.24 → wup-0.2.26}/wup/visual_diff.py +0 -0
- {wup-0.2.24 → wup-0.2.26}/wup/web_client.py +0 -0
- {wup-0.2.24 → wup-0.2.26}/wup.egg-info/dependency_links.txt +0 -0
- {wup-0.2.24 → wup-0.2.26}/wup.egg-info/entry_points.txt +0 -0
- {wup-0.2.24 → wup-0.2.26}/wup.egg-info/requires.txt +0 -0
- {wup-0.2.24 → wup-0.2.26}/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.26
|
|
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
|
|
@@ -29,17 +29,17 @@ Dynamic: license-file
|
|
|
29
29
|
|
|
30
30
|
## AI Cost Tracking
|
|
31
31
|
|
|
32
|
-
    
|
|
33
|
+
  
|
|
34
34
|
|
|
35
|
-
- 🤖 **LLM usage:** $1.
|
|
36
|
-
- 👤 **Human dev:** ~$
|
|
35
|
+
- 🤖 **LLM usage:** $1.9540 (36 commits)
|
|
36
|
+
- 👤 **Human dev:** ~$1532 (15.3h @ $100/h, 30min dedup)
|
|
37
37
|
|
|
38
|
-
Generated on 2026-05-
|
|
38
|
+
Generated on 2026-05-16 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
42
|
-
    
|
|
43
43
|
|
|
44
44
|
**WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
|
|
45
45
|
|
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
|
|
4
4
|
## AI Cost Tracking
|
|
5
5
|
|
|
6
|
-
    
|
|
7
|
+
  
|
|
8
8
|
|
|
9
|
-
- 🤖 **LLM usage:** $1.
|
|
10
|
-
- 👤 **Human dev:** ~$
|
|
9
|
+
- 🤖 **LLM usage:** $1.9540 (36 commits)
|
|
10
|
+
- 👤 **Human dev:** ~$1532 (15.3h @ $100/h, 30min dedup)
|
|
11
11
|
|
|
12
|
-
Generated on 2026-05-
|
|
12
|
+
Generated on 2026-05-16 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
|
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import tempfile
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import yaml
|
|
5
|
+
|
|
6
|
+
from wup.models.config import ProjectConfig, ServiceConfig, TestQLConfig, WupConfig, WatchConfig
|
|
7
|
+
from wup.monitoring_manifest import (
|
|
8
|
+
MANIFEST_BEGIN,
|
|
9
|
+
MANIFEST_END,
|
|
10
|
+
build_monitoring_manifest,
|
|
11
|
+
discover_docker_compose_services,
|
|
12
|
+
load_monitoring_manifest_from_yaml,
|
|
13
|
+
patch_wup_yaml_monitoring,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_discover_docker_compose():
|
|
18
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
19
|
+
root = Path(tmpdir)
|
|
20
|
+
(root / "docker-compose.yml").write_text(
|
|
21
|
+
"services:\n"
|
|
22
|
+
" firmware:\n"
|
|
23
|
+
" container_name: test-simulator-firmware\n"
|
|
24
|
+
" ports:\n"
|
|
25
|
+
" - '8202:8202'\n"
|
|
26
|
+
" healthcheck:\n"
|
|
27
|
+
" test: ['CMD', 'curl', 'http://127.0.0.1:8202/health']\n",
|
|
28
|
+
encoding="utf-8",
|
|
29
|
+
)
|
|
30
|
+
found = discover_docker_compose_services(root)
|
|
31
|
+
assert len(found) == 1
|
|
32
|
+
assert found[0].compose_service == "firmware"
|
|
33
|
+
assert found[0].container_name == "test-simulator-firmware"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_patch_and_load_monitoring_block():
|
|
37
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
38
|
+
root = Path(tmpdir)
|
|
39
|
+
(root / "docker-compose.yml").write_text(
|
|
40
|
+
"services:\n firmware:\n container_name: test-simulator-firmware\n ports:\n - '8202:8202'\n",
|
|
41
|
+
encoding="utf-8",
|
|
42
|
+
)
|
|
43
|
+
cfg_path = root / "wup.yaml"
|
|
44
|
+
cfg_path.write_text(
|
|
45
|
+
"project:\n name: demo\n"
|
|
46
|
+
"services:\n - name: firmware\n paths: ['backend/firmware/**']\n"
|
|
47
|
+
"testql:\n base_url: http://localhost:8100\n"
|
|
48
|
+
" endpoints_by_service:\n firmware:\n - /firmware/api/v1/health\n",
|
|
49
|
+
encoding="utf-8",
|
|
50
|
+
)
|
|
51
|
+
wup_config = WupConfig(
|
|
52
|
+
project=ProjectConfig(name="demo"),
|
|
53
|
+
services=[ServiceConfig(name="firmware", paths=["backend/firmware/**"])],
|
|
54
|
+
watch=WatchConfig(),
|
|
55
|
+
testql=TestQLConfig(
|
|
56
|
+
base_url="http://localhost:8100",
|
|
57
|
+
endpoints_by_service={"firmware": ["/firmware/api/v1/health"]},
|
|
58
|
+
probe_interval_s=60,
|
|
59
|
+
),
|
|
60
|
+
)
|
|
61
|
+
manifest = build_monitoring_manifest(root, wup_config)
|
|
62
|
+
patch_wup_yaml_monitoring(cfg_path, manifest)
|
|
63
|
+
|
|
64
|
+
text = cfg_path.read_text(encoding="utf-8")
|
|
65
|
+
assert MANIFEST_BEGIN in text
|
|
66
|
+
assert MANIFEST_END in text
|
|
67
|
+
|
|
68
|
+
loaded = load_monitoring_manifest_from_yaml(cfg_path)
|
|
69
|
+
assert loaded is not None
|
|
70
|
+
assert loaded["wup_services"]["firmware"]["live_probes"]
|
|
71
|
+
assert loaded["wup_services"]["firmware"]["live_probes"][0]["url"].endswith("/firmware/api/v1/health")
|
|
72
|
+
assert loaded["wup_services"]["firmware"]["docker"][0]["compose_service"] == "firmware"
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import tempfile
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from unittest.mock import patch
|
|
6
|
+
|
|
7
|
+
from wup.models.config import ProjectConfig, ServiceConfig, TestQLConfig, WupConfig, WatchConfig
|
|
8
|
+
from wup.testql_monitor import (
|
|
9
|
+
ProbeTarget,
|
|
10
|
+
TestQLMonitor,
|
|
11
|
+
assign_probe_to_service,
|
|
12
|
+
is_monitoring_probe,
|
|
13
|
+
parse_scenario_probes,
|
|
14
|
+
parse_service_map_probes,
|
|
15
|
+
)
|
|
16
|
+
from wup.testql_watcher import TestQLWatcher
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_parse_scenario_probes_full_url():
|
|
20
|
+
content = """
|
|
21
|
+
API[1]{method, endpoint, expected_status}:
|
|
22
|
+
GET, http://localhost:8100/firmware/api/v1/health, 200
|
|
23
|
+
"""
|
|
24
|
+
with tempfile.NamedTemporaryFile("w", suffix=".testql.toon.yaml", delete=False) as handle:
|
|
25
|
+
handle.write(content)
|
|
26
|
+
handle.flush()
|
|
27
|
+
path = Path(handle.name)
|
|
28
|
+
|
|
29
|
+
probes = parse_scenario_probes(path)
|
|
30
|
+
path.unlink(missing_ok=True)
|
|
31
|
+
assert len(probes) == 1
|
|
32
|
+
assert probes[0].url.endswith("/firmware/api/v1/health")
|
|
33
|
+
assert probes[0].expected_status == 200
|
|
34
|
+
assert is_monitoring_probe(probes[0])
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_assign_firmware_service():
|
|
38
|
+
services = [
|
|
39
|
+
ServiceConfig(name="frontend", paths=["frontend/**"]),
|
|
40
|
+
ServiceConfig(name="firmware", paths=["backend/firmware/**"]),
|
|
41
|
+
]
|
|
42
|
+
probe = ProbeTarget(url="http://localhost:8100/firmware/api/v1/health")
|
|
43
|
+
assert assign_probe_to_service(probe, services) == "firmware"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_monitor_merges_config_and_service_map():
|
|
47
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
48
|
+
root = Path(tmpdir)
|
|
49
|
+
scenario_dir = root / "testql-scenarios"
|
|
50
|
+
scenario_dir.mkdir()
|
|
51
|
+
scenario = scenario_dir / "fleet-health.testql.toon.yaml"
|
|
52
|
+
scenario.write_text(
|
|
53
|
+
"API[1]{method, endpoint, expected_status}:\n"
|
|
54
|
+
" GET, http://localhost:8100/firmware/api/v1/execution/status, 200\n",
|
|
55
|
+
encoding="utf-8",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
service_map = root / "service-map.yaml"
|
|
59
|
+
service_map.write_text(
|
|
60
|
+
"service:\n base_url: http://localhost:8100\n"
|
|
61
|
+
"endpoints:\n"
|
|
62
|
+
" - { method: GET, path: /firmware/api/v1/health }\n",
|
|
63
|
+
encoding="utf-8",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
cfg = WupConfig(
|
|
67
|
+
project=ProjectConfig(name="demo"),
|
|
68
|
+
services=[
|
|
69
|
+
ServiceConfig(name="firmware", paths=["backend/firmware/**"]),
|
|
70
|
+
],
|
|
71
|
+
watch=WatchConfig(),
|
|
72
|
+
testql=TestQLConfig(
|
|
73
|
+
scenario_dir="testql-scenarios",
|
|
74
|
+
base_url="http://localhost:8100",
|
|
75
|
+
endpoint_discovery=True,
|
|
76
|
+
service_map_globs=["service-map.yaml"],
|
|
77
|
+
endpoints_by_service={
|
|
78
|
+
"firmware": ["/firmware/api/v1/execution/logs"],
|
|
79
|
+
},
|
|
80
|
+
),
|
|
81
|
+
)
|
|
82
|
+
monitor = TestQLMonitor(root, cfg)
|
|
83
|
+
probes = monitor.probes_for_service("firmware")
|
|
84
|
+
urls = {p.url for p in probes}
|
|
85
|
+
assert "http://localhost:8100/firmware/api/v1/health" in urls
|
|
86
|
+
assert "http://localhost:8100/firmware/api/v1/execution/status" in urls
|
|
87
|
+
assert "http://localhost:8100/firmware/api/v1/execution/logs" in urls
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_live_probe_failure_updates_health():
|
|
91
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
92
|
+
root = Path(tmpdir)
|
|
93
|
+
cfg = WupConfig(
|
|
94
|
+
project=ProjectConfig(name="demo"),
|
|
95
|
+
services=[ServiceConfig(name="firmware", paths=["backend/firmware/**"])],
|
|
96
|
+
watch=WatchConfig(),
|
|
97
|
+
testql=TestQLConfig(
|
|
98
|
+
scenario_dir="testql-scenarios",
|
|
99
|
+
base_url="http://localhost:8100",
|
|
100
|
+
endpoints_by_service={"firmware": ["/firmware/api/v1/health"]},
|
|
101
|
+
),
|
|
102
|
+
)
|
|
103
|
+
watcher = TestQLWatcher(
|
|
104
|
+
project_root=str(root),
|
|
105
|
+
deps_file=str(root / "deps.json"),
|
|
106
|
+
scenarios_dir="testql-scenarios",
|
|
107
|
+
config=cfg,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
failing = ProbeTarget(url="http://localhost:8100/firmware/api/v1/health")
|
|
111
|
+
|
|
112
|
+
def fake_probe(self, timeout_s=10.0):
|
|
113
|
+
return False, "HTTP 500 (expected 200)"
|
|
114
|
+
|
|
115
|
+
with patch.object(ProbeTarget, "probe", fake_probe):
|
|
116
|
+
ok = asyncio.run(watcher._run_live_http_probes("firmware", []))
|
|
117
|
+
|
|
118
|
+
assert ok is False
|
|
119
|
+
state = json.loads((root / ".wup" / "service-health.json").read_text(encoding="utf-8"))
|
|
120
|
+
assert state["firmware"]["status"] == "down"
|
|
121
|
+
assert state["firmware"]["stage"] == "probe"
|
|
@@ -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.26"
|
|
11
11
|
__author__ = "Tom Sapletta"
|
|
12
12
|
|
|
13
13
|
from .config import load_config, save_config, get_default_config
|
|
@@ -9,7 +9,7 @@ from typing import Optional
|
|
|
9
9
|
import typer
|
|
10
10
|
from rich.console import Console
|
|
11
11
|
|
|
12
|
-
from .config import load_config
|
|
12
|
+
from .config import find_config_file, load_config
|
|
13
13
|
from .core import WupWatcher
|
|
14
14
|
from .dependency_mapper import DependencyMapper
|
|
15
15
|
from .models.config import WupConfig
|
|
@@ -37,6 +37,11 @@ def watch(
|
|
|
37
37
|
browser_service_url: Optional[str] = typer.Option(None, "--browser-service-url", help="HTTP endpoint for browser notifications"),
|
|
38
38
|
track_dir: str = typer.Option(".wup/tracks", "--track-dir", help="Directory where error track JSON files are written"),
|
|
39
39
|
quick_limit: int = typer.Option(3, "--quick-limit", help="Maximum TestQL scenarios used in quick pass"),
|
|
40
|
+
probe_interval: Optional[int] = typer.Option(
|
|
41
|
+
None,
|
|
42
|
+
"--probe-interval",
|
|
43
|
+
help="Periodic live HTTP/TestQL probes in seconds (overrides testql.probe_interval_s)",
|
|
44
|
+
),
|
|
40
45
|
config: Optional[str] = typer.Option(None, "--config", "-C", help="Path to wup.yaml config file"),
|
|
41
46
|
):
|
|
42
47
|
"""
|
|
@@ -56,16 +61,30 @@ def watch(
|
|
|
56
61
|
# Load configuration
|
|
57
62
|
config_path = Path(config) if config else None
|
|
58
63
|
wup_config = load_config(project_path, config_path)
|
|
59
|
-
|
|
64
|
+
if probe_interval is not None:
|
|
65
|
+
wup_config.testql.probe_interval_s = int(probe_interval)
|
|
66
|
+
|
|
60
67
|
console.print(f"[bold cyan]🚀 WUP Watcher[/bold cyan]")
|
|
61
68
|
console.print(f"[dim]Project: {wup_config.project.name}[/dim]")
|
|
62
69
|
console.print(f"[dim]Description: {wup_config.project.description}[/dim]")
|
|
63
70
|
console.print(f"[dim]CPU Throttle: {cpu_throttle * 100}%[/dim]")
|
|
64
71
|
console.print(f"[dim]Debounce: {debounce}s[/dim]")
|
|
65
72
|
console.print(f"[dim]Cooldown: {cooldown}s[/dim]")
|
|
73
|
+
if wup_config.testql.probe_interval_s:
|
|
74
|
+
console.print(f"[dim]Live probes: every {wup_config.testql.probe_interval_s}s[/dim]")
|
|
66
75
|
console.print(f"[dim]Config: {config_path or 'auto-detected'}[/dim]")
|
|
67
76
|
console.print()
|
|
68
77
|
|
|
78
|
+
cfg_path = config_path if config_path and config_path.exists() else find_config_file(project_path)
|
|
79
|
+
if cfg_path:
|
|
80
|
+
from .monitoring_manifest import build_monitoring_manifest, patch_wup_yaml_monitoring
|
|
81
|
+
try:
|
|
82
|
+
manifest = build_monitoring_manifest(project_path, wup_config)
|
|
83
|
+
patch_wup_yaml_monitoring(cfg_path, manifest)
|
|
84
|
+
console.print("[dim]Refreshed monitoring manifest in wup.yaml[/dim]")
|
|
85
|
+
except OSError as exc:
|
|
86
|
+
console.print(f"[yellow]Could not refresh monitoring manifest: {exc}[/yellow]")
|
|
87
|
+
|
|
69
88
|
if mode.lower() == "testql":
|
|
70
89
|
watcher = TestQLWatcher(
|
|
71
90
|
project_root=str(project_path),
|
|
@@ -274,6 +293,33 @@ def status(
|
|
|
274
293
|
if track_file:
|
|
275
294
|
lines.append(Text.from_markup(f" [dim]track: {track_file}[/dim]"))
|
|
276
295
|
|
|
296
|
+
# --- monitoring manifest (from wup.yaml) ---
|
|
297
|
+
manifest_path = config_path if config_path and config_path.exists() else find_config_file(project_path)
|
|
298
|
+
if manifest_path:
|
|
299
|
+
from .monitoring_manifest import load_monitoring_manifest_from_yaml
|
|
300
|
+
|
|
301
|
+
manifest = load_monitoring_manifest_from_yaml(manifest_path)
|
|
302
|
+
if manifest:
|
|
303
|
+
lines.append(Text(""))
|
|
304
|
+
lines.append(Text.from_markup("[bold]Configured monitoring (wup.yaml):[/bold]"))
|
|
305
|
+
lines.append(Text.from_markup(
|
|
306
|
+
f" [dim]manifest {manifest.get('generated_at', '?')} · "
|
|
307
|
+
f"probe {manifest.get('probe_interval_s', 0)}s[/dim]"
|
|
308
|
+
))
|
|
309
|
+
for svc, info in sorted((manifest.get("wup_services") or {}).items()):
|
|
310
|
+
probes = info.get("live_probes") or []
|
|
311
|
+
dockers = info.get("docker") or []
|
|
312
|
+
lines.append(Text.from_markup(
|
|
313
|
+
f" [cyan]{svc}[/cyan]: {len(probes)} probe(s), docker: "
|
|
314
|
+
+ ", ".join(
|
|
315
|
+
d.get("compose_service", "?") for d in dockers[:4]
|
|
316
|
+
)
|
|
317
|
+
+ ("…" if len(dockers) > 4 else "")
|
|
318
|
+
))
|
|
319
|
+
lines.append(Text.from_markup(
|
|
320
|
+
" [dim]Pełna lista: sekcja monitoring: w wup.yaml (BEGIN WUP MONITORING MANIFEST)[/dim]"
|
|
321
|
+
))
|
|
322
|
+
|
|
277
323
|
# --- visual diff section ---
|
|
278
324
|
if wup_config.visual_diff and wup_config.visual_diff.enabled:
|
|
279
325
|
from .visual_diff import VisualDiffer
|
|
@@ -439,6 +485,91 @@ def map_deps(
|
|
|
439
485
|
console.print(f"[dim]Files: {len(deps.get('files', {}))}[/dim]")
|
|
440
486
|
|
|
441
487
|
|
|
488
|
+
@app.command("sync-testql")
|
|
489
|
+
def sync_testql(
|
|
490
|
+
project: str = typer.Argument(".", help="Path to the project root directory"),
|
|
491
|
+
write: bool = typer.Option(False, "--write", "-w", help="Write monitoring manifest block into wup.yaml"),
|
|
492
|
+
merge_endpoints: bool = typer.Option(
|
|
493
|
+
False,
|
|
494
|
+
"--merge-endpoints",
|
|
495
|
+
help="Also merge discovered paths into testql.endpoints_by_service (rewrites YAML body)",
|
|
496
|
+
),
|
|
497
|
+
config: Optional[str] = typer.Option(None, "--config", "-C", help="Path to wup.yaml config file"),
|
|
498
|
+
):
|
|
499
|
+
"""
|
|
500
|
+
Discover monitoring targets and document them in wup.yaml.
|
|
501
|
+
|
|
502
|
+
With ``--write``, appends/updates the auto-generated ``monitoring:`` block
|
|
503
|
+
(Docker Compose services, live HTTP probes, sources). Use this to verify
|
|
504
|
+
whether a failure is a WUP config gap vs a down container.
|
|
505
|
+
|
|
506
|
+
Use ``--merge-endpoints`` cautiously — it re-serializes wup.yaml (may drop comments).
|
|
507
|
+
"""
|
|
508
|
+
import json
|
|
509
|
+
|
|
510
|
+
from .config import find_config_file, load_config
|
|
511
|
+
from .monitoring_manifest import (
|
|
512
|
+
MANIFEST_BEGIN,
|
|
513
|
+
build_monitoring_manifest,
|
|
514
|
+
format_manifest_summary,
|
|
515
|
+
patch_wup_yaml_monitoring,
|
|
516
|
+
)
|
|
517
|
+
from .testql_monitor import TestQLMonitor
|
|
518
|
+
|
|
519
|
+
project_path = Path(project).resolve()
|
|
520
|
+
if not project_path.exists():
|
|
521
|
+
console.print(f"[red]Error: Project path '{project}' does not exist[/red]")
|
|
522
|
+
raise typer.Exit(1)
|
|
523
|
+
|
|
524
|
+
config_path = Path(config) if config else find_config_file(project_path)
|
|
525
|
+
wup_config = load_config(project_path, config_path)
|
|
526
|
+
monitor = TestQLMonitor(project_path, wup_config)
|
|
527
|
+
suggested = monitor.suggested_endpoints_by_service()
|
|
528
|
+
manifest = build_monitoring_manifest(project_path, wup_config)
|
|
529
|
+
|
|
530
|
+
console.print("[bold]Monitoring manifest (preview):[/bold]")
|
|
531
|
+
console.print(format_manifest_summary(manifest))
|
|
532
|
+
|
|
533
|
+
if suggested:
|
|
534
|
+
console.print()
|
|
535
|
+
console.print("[bold]Suggested testql.endpoints_by_service additions:[/bold]")
|
|
536
|
+
console.print(json.dumps(suggested, indent=2))
|
|
537
|
+
|
|
538
|
+
if not write:
|
|
539
|
+
console.print()
|
|
540
|
+
console.print("[dim]Run: wup sync-testql . --write → dokumentacja w wup.yaml[/dim]")
|
|
541
|
+
return
|
|
542
|
+
|
|
543
|
+
if config_path is None:
|
|
544
|
+
console.print("[red]No wup.yaml found — run `wup init` first[/red]")
|
|
545
|
+
raise typer.Exit(1)
|
|
546
|
+
|
|
547
|
+
if merge_endpoints and suggested:
|
|
548
|
+
import yaml as pyyaml
|
|
549
|
+
|
|
550
|
+
merged = dict(wup_config.testql.endpoints_by_service or {})
|
|
551
|
+
for service, paths in suggested.items():
|
|
552
|
+
existing = set(merged.get(service, []))
|
|
553
|
+
existing.update(paths)
|
|
554
|
+
merged[service] = sorted(existing)
|
|
555
|
+
raw = pyyaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
|
|
556
|
+
raw.setdefault("testql", {})["endpoints_by_service"] = merged
|
|
557
|
+
wup_config.testql.endpoints_by_service = merged
|
|
558
|
+
manifest = build_monitoring_manifest(project_path, wup_config)
|
|
559
|
+
body = pyyaml.safe_dump(
|
|
560
|
+
{k: v for k, v in raw.items() if k != "monitoring"},
|
|
561
|
+
sort_keys=False,
|
|
562
|
+
allow_unicode=True,
|
|
563
|
+
default_flow_style=False,
|
|
564
|
+
)
|
|
565
|
+
config_path.write_text(body.rstrip() + "\n\n", encoding="utf-8")
|
|
566
|
+
console.print("[yellow]Merged endpoints_by_service (review git diff for comment loss)[/yellow]")
|
|
567
|
+
|
|
568
|
+
patch_wup_yaml_monitoring(config_path, manifest)
|
|
569
|
+
console.print(f"[green]✓ monitoring manifest written to {config_path}[/green]")
|
|
570
|
+
console.print(f"[dim]Szukaj w pliku: {MANIFEST_BEGIN}[/dim]")
|
|
571
|
+
|
|
572
|
+
|
|
442
573
|
@app.command()
|
|
443
574
|
def assistant(
|
|
444
575
|
quick: bool = typer.Option(False, "--quick", "-q", help="Non-interactive mode with auto-detected values"),
|
|
@@ -176,6 +176,9 @@ def validate_config(raw: dict) -> WupConfig:
|
|
|
176
176
|
output_format=testql_raw.get("output_format", "json"),
|
|
177
177
|
extra_args=testql_raw.get("extra_args", ["--timeout 10s"]),
|
|
178
178
|
endpoint_discovery=testql_raw.get("endpoint_discovery", True),
|
|
179
|
+
probe_interval_s=int(testql_raw.get("probe_interval_s", 0) or 0),
|
|
180
|
+
health_scenario=testql_raw.get("health_scenario", ""),
|
|
181
|
+
service_map_globs=testql_raw.get("service_map_globs", []),
|
|
179
182
|
base_url=testql_raw.get("base_url", ""),
|
|
180
183
|
base_url_env=testql_raw.get("base_url_env", "WUP_BASE_URL"),
|
|
181
184
|
explicit_endpoints=testql_raw.get("explicit_endpoints", []),
|
|
@@ -328,6 +331,9 @@ def save_config(config: WupConfig, output_path: Path):
|
|
|
328
331
|
"output_format": config.testql.output_format,
|
|
329
332
|
"extra_args": config.testql.extra_args,
|
|
330
333
|
"endpoint_discovery": config.testql.endpoint_discovery,
|
|
334
|
+
"probe_interval_s": config.testql.probe_interval_s,
|
|
335
|
+
"health_scenario": config.testql.health_scenario,
|
|
336
|
+
"service_map_globs": config.testql.service_map_globs,
|
|
331
337
|
"base_url": config.testql.base_url,
|
|
332
338
|
"base_url_env": config.testql.base_url_env,
|
|
333
339
|
"explicit_endpoints": config.testql.explicit_endpoints,
|
|
@@ -6,6 +6,8 @@ import asyncio
|
|
|
6
6
|
import json
|
|
7
7
|
import subprocess
|
|
8
8
|
import time
|
|
9
|
+
import urllib.error
|
|
10
|
+
import urllib.request
|
|
9
11
|
from collections import defaultdict, deque
|
|
10
12
|
from pathlib import Path
|
|
11
13
|
from typing import Dict, List, Optional, Set, Tuple
|
|
@@ -298,69 +300,96 @@ class WupWatcher:
|
|
|
298
300
|
async def run_quick_test(self, service: str, endpoints: List[str]) -> bool:
|
|
299
301
|
"""
|
|
300
302
|
Run a quick test for a service (smoke test).
|
|
301
|
-
|
|
303
|
+
|
|
302
304
|
Args:
|
|
303
305
|
service: Service name
|
|
304
306
|
endpoints: List of endpoints to test
|
|
305
|
-
|
|
307
|
+
|
|
306
308
|
Returns:
|
|
307
309
|
True if all tests passed, False otherwise
|
|
308
310
|
"""
|
|
309
311
|
self.console.print(f"[cyan]🧪 Quick testing {service} ({len(endpoints)} endpoints)[/cyan]")
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
312
|
+
|
|
313
|
+
if not endpoints:
|
|
314
|
+
self.console.print(f"[yellow]⚠ No endpoints configured for {service}, skipping quick test[/yellow]")
|
|
315
|
+
return True
|
|
316
|
+
|
|
317
|
+
passed = True
|
|
318
|
+
for endpoint in endpoints:
|
|
319
|
+
try:
|
|
320
|
+
req = urllib.request.Request(endpoint, method="HEAD")
|
|
321
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
322
|
+
if resp.status >= 400:
|
|
323
|
+
self.console.print(f"[red]✗ {endpoint} → HTTP {resp.status}[/red]")
|
|
324
|
+
passed = False
|
|
325
|
+
else:
|
|
326
|
+
self.console.print(f"[green]✓ {endpoint} → HTTP {resp.status}[/green]")
|
|
327
|
+
except Exception as e:
|
|
328
|
+
self.console.print(f"[red]✗ {endpoint} → {e}[/red]")
|
|
329
|
+
passed = False
|
|
330
|
+
|
|
319
331
|
if passed:
|
|
320
332
|
self.console.print(f"[green]✓ Quick test passed for {service}[/green]")
|
|
321
333
|
else:
|
|
322
334
|
self.console.print(f"[red]✗ Quick test failed for {service}[/red]")
|
|
323
|
-
|
|
335
|
+
|
|
324
336
|
return passed
|
|
325
337
|
|
|
326
338
|
async def run_detail_test(self, service: str, endpoints: List[str]) -> Dict:
|
|
327
339
|
"""
|
|
328
340
|
Run a detailed test for a service with blame report.
|
|
329
|
-
|
|
341
|
+
|
|
330
342
|
Args:
|
|
331
343
|
service: Service name
|
|
332
344
|
endpoints: List of endpoints to test
|
|
333
|
-
|
|
345
|
+
|
|
334
346
|
Returns:
|
|
335
347
|
Dictionary with test results and blame information
|
|
336
348
|
"""
|
|
337
349
|
self.console.print(f"[cyan]🔍 Detail testing {service} ({len(endpoints)} endpoints)[/cyan]")
|
|
338
|
-
|
|
339
|
-
# This is a placeholder - integrate with TestQL or your test framework
|
|
340
|
-
# For now, simulate a test
|
|
341
|
-
await asyncio.sleep(3)
|
|
342
|
-
|
|
343
|
-
# Simulate results
|
|
350
|
+
|
|
344
351
|
results = {
|
|
345
352
|
"service": service,
|
|
346
353
|
"total_endpoints": len(endpoints),
|
|
347
|
-
"passed":
|
|
348
|
-
"failed":
|
|
349
|
-
"failed_endpoint":
|
|
350
|
-
"blame": {
|
|
351
|
-
"file": f"app/{service}/routes.py",
|
|
352
|
-
"line": 42,
|
|
353
|
-
"commit": "abc123",
|
|
354
|
-
"author": "developer"
|
|
355
|
-
}
|
|
354
|
+
"passed": 0,
|
|
355
|
+
"failed": 0,
|
|
356
|
+
"failed_endpoint": None,
|
|
357
|
+
"blame": {},
|
|
356
358
|
}
|
|
357
|
-
|
|
359
|
+
|
|
360
|
+
for endpoint in endpoints:
|
|
361
|
+
try:
|
|
362
|
+
req = urllib.request.Request(endpoint, method="GET")
|
|
363
|
+
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
364
|
+
if resp.status >= 400:
|
|
365
|
+
results["failed"] += 1
|
|
366
|
+
self.console.print(f"[red]✗ {endpoint} → HTTP {resp.status}[/red]")
|
|
367
|
+
else:
|
|
368
|
+
results["passed"] += 1
|
|
369
|
+
self.console.print(f"[green]✓ {endpoint} → HTTP {resp.status}[/green]")
|
|
370
|
+
except Exception as e:
|
|
371
|
+
results["failed"] += 1
|
|
372
|
+
self.console.print(f"[red]✗ {endpoint} → {e}[/red]")
|
|
373
|
+
|
|
358
374
|
if results["failed"] > 0:
|
|
375
|
+
results["failed_endpoint"] = endpoints[0] if endpoints else None
|
|
376
|
+
try:
|
|
377
|
+
blame_result = subprocess.run(
|
|
378
|
+
["git", "log", "--oneline", "-5", "--", f"*/{service}/*"],
|
|
379
|
+
cwd=str(self.project_root),
|
|
380
|
+
capture_output=True,
|
|
381
|
+
text=True,
|
|
382
|
+
)
|
|
383
|
+
if blame_result.returncode == 0:
|
|
384
|
+
lines = blame_result.stdout.strip().split("\n")
|
|
385
|
+
if lines and lines[0]:
|
|
386
|
+
results["blame"] = {"recent_commits": lines}
|
|
387
|
+
except Exception:
|
|
388
|
+
pass
|
|
359
389
|
self.console.print(f"[red]✗ Detail test found {results['failed']} regression(s)[/red]")
|
|
360
|
-
self.console.print(f"[red] Blame: {results['blame']['file']}:{results['blame']['line']}[/red]")
|
|
361
390
|
else:
|
|
362
391
|
self.console.print(f"[green]✓ Detail test passed for {service}[/green]")
|
|
363
|
-
|
|
392
|
+
|
|
364
393
|
return results
|
|
365
394
|
|
|
366
395
|
async def test_loop(self):
|
|
@@ -60,7 +60,10 @@ class TestQLConfig:
|
|
|
60
60
|
smoke_scenario: str = "smoke.testql.toon.yaml"
|
|
61
61
|
output_format: str = "json"
|
|
62
62
|
extra_args: List[str] = field(default_factory=lambda: ["--timeout 10s"])
|
|
63
|
-
endpoint_discovery: bool = True #
|
|
63
|
+
endpoint_discovery: bool = True # Merge health probes from scenarios + service maps
|
|
64
|
+
probe_interval_s: int = 0 # Periodic live probes for all services (0 = file-change only)
|
|
65
|
+
health_scenario: str = "" # Optional TestQL scenario run live (not --dry-run) on each quick pass
|
|
66
|
+
service_map_globs: List[str] = field(default_factory=list) # e.g. testql-testing/service-map/*.yaml
|
|
64
67
|
base_url: str = ""
|
|
65
68
|
base_url_env: str = "WUP_BASE_URL"
|
|
66
69
|
explicit_endpoints: List[str] = field(default_factory=list)
|