wup 0.2.25__tar.gz → 0.2.27__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.25/wup.egg-info → wup-0.2.27}/PKG-INFO +14 -13
- {wup-0.2.25 → wup-0.2.27}/README.md +13 -12
- {wup-0.2.25 → wup-0.2.27}/pyproject.toml +1 -1
- wup-0.2.27/tests/test_monitoring_manifest.py +72 -0
- wup-0.2.27/tests/test_testql_monitor.py +161 -0
- {wup-0.2.25 → wup-0.2.27}/tests/test_testql_watcher.py +3 -1
- {wup-0.2.25 → wup-0.2.27}/wup/__init__.py +1 -1
- {wup-0.2.25 → wup-0.2.27}/wup/cli.py +153 -11
- {wup-0.2.25 → wup-0.2.27}/wup/config.py +16 -2
- {wup-0.2.25 → wup-0.2.27}/wup/models/config.py +10 -2
- wup-0.2.27/wup/monitoring_manifest.py +306 -0
- wup-0.2.27/wup/testql_monitor.py +392 -0
- {wup-0.2.25 → wup-0.2.27}/wup/testql_watcher.py +194 -9
- {wup-0.2.25 → wup-0.2.27}/wup/visual_diff.py +26 -3
- {wup-0.2.25 → wup-0.2.27}/wup/web_client.py +8 -1
- {wup-0.2.25 → wup-0.2.27/wup.egg-info}/PKG-INFO +14 -13
- {wup-0.2.25 → wup-0.2.27}/wup.egg-info/SOURCES.txt +4 -0
- {wup-0.2.25 → wup-0.2.27}/LICENSE +0 -0
- {wup-0.2.25 → wup-0.2.27}/setup.cfg +0 -0
- {wup-0.2.25 → wup-0.2.27}/tests/test_e2e.py +0 -0
- {wup-0.2.25 → wup-0.2.27}/tests/test_web_client.py +0 -0
- {wup-0.2.25 → wup-0.2.27}/tests/test_wup.py +0 -0
- {wup-0.2.25 → wup-0.2.27}/wup/_ast_detector.py +0 -0
- {wup-0.2.25 → wup-0.2.27}/wup/_hash_detector.py +0 -0
- {wup-0.2.25 → wup-0.2.27}/wup/_yaml_detector.py +0 -0
- {wup-0.2.25 → wup-0.2.27}/wup/anomaly_detector.py +0 -0
- {wup-0.2.25 → wup-0.2.27}/wup/anomaly_models.py +0 -0
- {wup-0.2.25 → wup-0.2.27}/wup/assistant.py +0 -0
- {wup-0.2.25 → wup-0.2.27}/wup/core.py +0 -0
- {wup-0.2.25 → wup-0.2.27}/wup/dependency_mapper.py +0 -0
- {wup-0.2.25 → wup-0.2.27}/wup/models/__init__.py +0 -0
- {wup-0.2.25 → wup-0.2.27}/wup/testql_discovery.py +0 -0
- {wup-0.2.25 → wup-0.2.27}/wup.egg-info/dependency_links.txt +0 -0
- {wup-0.2.25 → wup-0.2.27}/wup.egg-info/entry_points.txt +0 -0
- {wup-0.2.25 → wup-0.2.27}/wup.egg-info/requires.txt +0 -0
- {wup-0.2.25 → wup-0.2.27}/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.27
|
|
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:** $
|
|
36
|
-
- 👤 **Human dev:** ~$
|
|
35
|
+
- 🤖 **LLM usage:** $2.1268 (37 commits)
|
|
36
|
+
- 👤 **Human dev:** ~$1632 (16.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
|
|
|
@@ -83,7 +83,7 @@ wup assistant --quick --template fastapi
|
|
|
83
83
|
# 3. Build dependency map (one-time setup)
|
|
84
84
|
wup map-deps ./my-project
|
|
85
85
|
|
|
86
|
-
# 4. Start watching
|
|
86
|
+
# 4. Start watching (TestQL + live probes every 60s by default)
|
|
87
87
|
wup watch ./my-project
|
|
88
88
|
|
|
89
89
|
# 5. Start with live dashboard
|
|
@@ -110,14 +110,18 @@ wup map-deps ./my-project --output my-deps.json
|
|
|
110
110
|
### Watch Project
|
|
111
111
|
|
|
112
112
|
```bash
|
|
113
|
-
# Basic watching (uses wup.yaml if present)
|
|
113
|
+
# Basic watching: TestQL mode + live HTTP probes every 60s (uses wup.yaml if present)
|
|
114
114
|
wup watch ./my-project
|
|
115
115
|
|
|
116
|
+
# Legacy HTTP-only watcher (no TestQL, no periodic probes unless configured)
|
|
117
|
+
wup watch ./my-project --mode default --probe-interval 0
|
|
118
|
+
|
|
116
119
|
# With custom settings
|
|
117
120
|
wup watch ./my-project \
|
|
118
121
|
--cpu-throttle 0.5 \
|
|
119
122
|
--debounce 3 \
|
|
120
|
-
--cooldown 600
|
|
123
|
+
--cooldown 600 \
|
|
124
|
+
--probe-interval 120
|
|
121
125
|
|
|
122
126
|
# With live dashboard
|
|
123
127
|
wup watch ./my-project --dashboard
|
|
@@ -125,9 +129,6 @@ wup watch ./my-project --dashboard
|
|
|
125
129
|
# Use specific config file
|
|
126
130
|
wup watch ./my-project --config custom-config.yaml
|
|
127
131
|
|
|
128
|
-
# TestQL mode
|
|
129
|
-
wup watch ./my-project --mode testql
|
|
130
|
-
|
|
131
132
|
# Discover endpoints from TestQL scenarios
|
|
132
133
|
wup testql-endpoints /path/to/scenarios --output testql-deps.json
|
|
133
134
|
```
|
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
|
|
4
4
|
## AI Cost Tracking
|
|
5
5
|
|
|
6
|
-
    
|
|
7
|
+
  
|
|
8
8
|
|
|
9
|
-
- 🤖 **LLM usage:** $
|
|
10
|
-
- 👤 **Human dev:** ~$
|
|
9
|
+
- 🤖 **LLM usage:** $2.1268 (37 commits)
|
|
10
|
+
- 👤 **Human dev:** ~$1632 (16.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
|
|
|
@@ -57,7 +57,7 @@ wup assistant --quick --template fastapi
|
|
|
57
57
|
# 3. Build dependency map (one-time setup)
|
|
58
58
|
wup map-deps ./my-project
|
|
59
59
|
|
|
60
|
-
# 4. Start watching
|
|
60
|
+
# 4. Start watching (TestQL + live probes every 60s by default)
|
|
61
61
|
wup watch ./my-project
|
|
62
62
|
|
|
63
63
|
# 5. Start with live dashboard
|
|
@@ -84,14 +84,18 @@ wup map-deps ./my-project --output my-deps.json
|
|
|
84
84
|
### Watch Project
|
|
85
85
|
|
|
86
86
|
```bash
|
|
87
|
-
# Basic watching (uses wup.yaml if present)
|
|
87
|
+
# Basic watching: TestQL mode + live HTTP probes every 60s (uses wup.yaml if present)
|
|
88
88
|
wup watch ./my-project
|
|
89
89
|
|
|
90
|
+
# Legacy HTTP-only watcher (no TestQL, no periodic probes unless configured)
|
|
91
|
+
wup watch ./my-project --mode default --probe-interval 0
|
|
92
|
+
|
|
90
93
|
# With custom settings
|
|
91
94
|
wup watch ./my-project \
|
|
92
95
|
--cpu-throttle 0.5 \
|
|
93
96
|
--debounce 3 \
|
|
94
|
-
--cooldown 600
|
|
97
|
+
--cooldown 600 \
|
|
98
|
+
--probe-interval 120
|
|
95
99
|
|
|
96
100
|
# With live dashboard
|
|
97
101
|
wup watch ./my-project --dashboard
|
|
@@ -99,9 +103,6 @@ wup watch ./my-project --dashboard
|
|
|
99
103
|
# Use specific config file
|
|
100
104
|
wup watch ./my-project --config custom-config.yaml
|
|
101
105
|
|
|
102
|
-
# TestQL mode
|
|
103
|
-
wup watch ./my-project --mode testql
|
|
104
|
-
|
|
105
106
|
# Discover endpoints from TestQL scenarios
|
|
106
107
|
wup testql-endpoints /path/to/scenarios --output testql-deps.json
|
|
107
108
|
```
|
|
@@ -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,161 @@
|
|
|
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_connect_api_paths_on_8100_are_not_monitoring_probes():
|
|
38
|
+
probe = ProbeTarget(url="http://localhost:8100/api/id/health")
|
|
39
|
+
assert not is_monitoring_probe(probe)
|
|
40
|
+
assert assign_probe_to_service(
|
|
41
|
+
probe,
|
|
42
|
+
[ServiceConfig(name="backend", paths=["backend/**", "api/**"])],
|
|
43
|
+
) != "backend"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_connect_health_on_8103_not_assigned_to_backend():
|
|
47
|
+
services = [
|
|
48
|
+
ServiceConfig(name="frontend", paths=["frontend/**"]),
|
|
49
|
+
ServiceConfig(name="backend", paths=["backend/**"]),
|
|
50
|
+
]
|
|
51
|
+
probe = ProbeTarget(url="http://localhost:8103/api/id/health")
|
|
52
|
+
assert assign_probe_to_service(probe, services) is None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_assign_firmware_service():
|
|
56
|
+
services = [
|
|
57
|
+
ServiceConfig(name="frontend", paths=["frontend/**"]),
|
|
58
|
+
ServiceConfig(name="firmware", paths=["backend/firmware/**"]),
|
|
59
|
+
]
|
|
60
|
+
probe = ProbeTarget(url="http://localhost:8100/firmware/api/v1/health")
|
|
61
|
+
assert assign_probe_to_service(probe, services) == "firmware"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_monitor_merges_config_and_service_map():
|
|
65
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
66
|
+
root = Path(tmpdir)
|
|
67
|
+
scenario_dir = root / "testql-scenarios"
|
|
68
|
+
scenario_dir.mkdir()
|
|
69
|
+
scenario = scenario_dir / "fleet-health.testql.toon.yaml"
|
|
70
|
+
scenario.write_text(
|
|
71
|
+
"API[1]{method, endpoint, expected_status}:\n"
|
|
72
|
+
" GET, http://localhost:8100/firmware/api/v1/execution/status, 200\n",
|
|
73
|
+
encoding="utf-8",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
service_map = root / "service-map.yaml"
|
|
77
|
+
service_map.write_text(
|
|
78
|
+
"service:\n base_url: http://localhost:8100\n"
|
|
79
|
+
"endpoints:\n"
|
|
80
|
+
" - { method: GET, path: /firmware/api/v1/health }\n",
|
|
81
|
+
encoding="utf-8",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
cfg = WupConfig(
|
|
85
|
+
project=ProjectConfig(name="demo"),
|
|
86
|
+
services=[
|
|
87
|
+
ServiceConfig(name="firmware", paths=["backend/firmware/**"]),
|
|
88
|
+
],
|
|
89
|
+
watch=WatchConfig(),
|
|
90
|
+
testql=TestQLConfig(
|
|
91
|
+
scenario_dir="testql-scenarios",
|
|
92
|
+
base_url="http://localhost:8100",
|
|
93
|
+
endpoint_discovery=True,
|
|
94
|
+
service_map_globs=["service-map.yaml"],
|
|
95
|
+
endpoints_by_service={
|
|
96
|
+
"firmware": ["/firmware/api/v1/execution/logs"],
|
|
97
|
+
},
|
|
98
|
+
),
|
|
99
|
+
)
|
|
100
|
+
monitor = TestQLMonitor(root, cfg)
|
|
101
|
+
probes = monitor.probes_for_service("firmware")
|
|
102
|
+
urls = {p.url for p in probes}
|
|
103
|
+
assert "http://localhost:8100/firmware/api/v1/health" in urls
|
|
104
|
+
assert "http://localhost:8100/firmware/api/v1/execution/status" in urls
|
|
105
|
+
assert "http://localhost:8100/firmware/api/v1/execution/logs" in urls
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_probes_for_service_ignores_non_health_extra_paths():
|
|
109
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
110
|
+
root = Path(tmpdir)
|
|
111
|
+
cfg = WupConfig(
|
|
112
|
+
project=ProjectConfig(name="demo"),
|
|
113
|
+
services=[ServiceConfig(name="backend", paths=["backend/**"])],
|
|
114
|
+
watch=WatchConfig(),
|
|
115
|
+
testql=TestQLConfig(
|
|
116
|
+
base_url="http://localhost:8100",
|
|
117
|
+
api_base_url="http://localhost:8101",
|
|
118
|
+
endpoints_by_service={"backend": ["http://localhost:8101/api/v3/health"]},
|
|
119
|
+
),
|
|
120
|
+
)
|
|
121
|
+
monitor = TestQLMonitor(root, cfg)
|
|
122
|
+
probes = monitor.probes_for_service(
|
|
123
|
+
"backend",
|
|
124
|
+
["/connect-config", "http://localhost:8101/connect-config"],
|
|
125
|
+
)
|
|
126
|
+
urls = {p.url for p in probes}
|
|
127
|
+
assert urls == {"http://localhost:8101/api/v3/health"}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_live_probe_failure_updates_health():
|
|
131
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
132
|
+
root = Path(tmpdir)
|
|
133
|
+
cfg = WupConfig(
|
|
134
|
+
project=ProjectConfig(name="demo"),
|
|
135
|
+
services=[ServiceConfig(name="firmware", paths=["backend/firmware/**"])],
|
|
136
|
+
watch=WatchConfig(),
|
|
137
|
+
testql=TestQLConfig(
|
|
138
|
+
scenario_dir="testql-scenarios",
|
|
139
|
+
base_url="http://localhost:8100",
|
|
140
|
+
endpoints_by_service={"firmware": ["/firmware/api/v1/health"]},
|
|
141
|
+
),
|
|
142
|
+
)
|
|
143
|
+
watcher = TestQLWatcher(
|
|
144
|
+
project_root=str(root),
|
|
145
|
+
deps_file=str(root / "deps.json"),
|
|
146
|
+
scenarios_dir="testql-scenarios",
|
|
147
|
+
config=cfg,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
failing = ProbeTarget(url="http://localhost:8100/firmware/api/v1/health")
|
|
151
|
+
|
|
152
|
+
def fake_probe(self, timeout_s=10.0):
|
|
153
|
+
return False, "HTTP 500 (expected 200)"
|
|
154
|
+
|
|
155
|
+
with patch.object(ProbeTarget, "probe", fake_probe):
|
|
156
|
+
ok = asyncio.run(watcher._run_live_http_probes("firmware", []))
|
|
157
|
+
|
|
158
|
+
assert ok is False
|
|
159
|
+
state = json.loads((root / ".wup" / "service-health.json").read_text(encoding="utf-8"))
|
|
160
|
+
assert state["firmware"]["status"] == "down"
|
|
161
|
+
assert state["firmware"]["stage"] == "probe"
|
|
@@ -6,7 +6,7 @@ from pathlib import Path
|
|
|
6
6
|
from subprocess import CompletedProcess
|
|
7
7
|
|
|
8
8
|
from wup.testql_watcher import TestQLWatcher
|
|
9
|
-
from wup.models.config import WupConfig, ProjectConfig, TestQLConfig, VisualDiffConfig
|
|
9
|
+
from wup.models.config import WupConfig, ProjectConfig, ServiceConfig, TestQLConfig, VisualDiffConfig
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def test_process_changed_file_creates_track_on_failure():
|
|
@@ -97,6 +97,7 @@ def test_config_endpoints_use_base_url_from_yaml_config():
|
|
|
97
97
|
root = Path(tmpdir)
|
|
98
98
|
cfg = WupConfig(
|
|
99
99
|
project=ProjectConfig(name="demo"),
|
|
100
|
+
services=[ServiceConfig(name="connect-config", paths=["connect-config/**"])],
|
|
100
101
|
testql=TestQLConfig(
|
|
101
102
|
base_url="http://localhost:8100",
|
|
102
103
|
explicit_endpoints=["/connect-config"],
|
|
@@ -122,6 +123,7 @@ def test_config_endpoints_use_base_url_from_env_when_yaml_missing():
|
|
|
122
123
|
root = Path(tmpdir)
|
|
123
124
|
cfg = WupConfig(
|
|
124
125
|
project=ProjectConfig(name="demo"),
|
|
126
|
+
services=[ServiceConfig(name="connect-data", paths=["connect-data/**"])],
|
|
125
127
|
testql=TestQLConfig(
|
|
126
128
|
base_url="",
|
|
127
129
|
base_url_env="WUP_BASE_URL",
|
|
@@ -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.27"
|
|
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
|
|
@@ -31,21 +31,33 @@ def watch(
|
|
|
31
31
|
debounce: int = typer.Option(2, "--debounce", "-b", help="Debounce time in seconds"),
|
|
32
32
|
cooldown: int = typer.Option(300, "--cooldown", "-t", help="Test cooldown in seconds"),
|
|
33
33
|
dashboard: bool = typer.Option(False, "--dashboard", help="Enable live dashboard"),
|
|
34
|
-
mode: str = typer.Option(
|
|
35
|
-
|
|
34
|
+
mode: str = typer.Option(
|
|
35
|
+
"testql",
|
|
36
|
+
"--mode",
|
|
37
|
+
help="Watcher mode: testql (default) or default (HTTP-only, no TestQL)",
|
|
38
|
+
),
|
|
39
|
+
scenarios_dir: Optional[str] = typer.Option(
|
|
40
|
+
None,
|
|
41
|
+
"--scenarios-dir",
|
|
42
|
+
help="TestQL scenarios directory (default: testql.scenario_dir from wup.yaml)",
|
|
43
|
+
),
|
|
36
44
|
testql_bin: str = typer.Option("testql", "--testql-bin", help="TestQL executable name/path"),
|
|
37
45
|
browser_service_url: Optional[str] = typer.Option(None, "--browser-service-url", help="HTTP endpoint for browser notifications"),
|
|
38
46
|
track_dir: str = typer.Option(".wup/tracks", "--track-dir", help="Directory where error track JSON files are written"),
|
|
39
47
|
quick_limit: int = typer.Option(3, "--quick-limit", help="Maximum TestQL scenarios used in quick pass"),
|
|
48
|
+
probe_interval: Optional[int] = typer.Option(
|
|
49
|
+
None,
|
|
50
|
+
"--probe-interval",
|
|
51
|
+
help="Periodic live HTTP probes in seconds (default: 60 in testql mode, or testql.probe_interval_s from wup.yaml; use 0 to disable)",
|
|
52
|
+
),
|
|
40
53
|
config: Optional[str] = typer.Option(None, "--config", "-C", help="Path to wup.yaml config file"),
|
|
41
54
|
):
|
|
42
55
|
"""
|
|
43
|
-
Watch project for file changes and run
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
3. Detail: Full tests with blame reports (only on failure)
|
|
56
|
+
Watch project for file changes and run regression tests.
|
|
57
|
+
|
|
58
|
+
Defaults (no extra flags): ``--mode testql`` and live probes every **60s**
|
|
59
|
+
(unless ``testql.probe_interval_s`` is set in wup.yaml). Use
|
|
60
|
+
``--mode default`` for the legacy HTTP-only watcher without TestQL.
|
|
49
61
|
"""
|
|
50
62
|
project_path = Path(project).resolve()
|
|
51
63
|
|
|
@@ -56,16 +68,34 @@ def watch(
|
|
|
56
68
|
# Load configuration
|
|
57
69
|
config_path = Path(config) if config else None
|
|
58
70
|
wup_config = load_config(project_path, config_path)
|
|
59
|
-
|
|
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
|
+
|
|
76
|
+
effective_scenarios_dir = scenarios_dir or wup_config.testql.scenario_dir
|
|
77
|
+
|
|
60
78
|
console.print(f"[bold cyan]🚀 WUP Watcher[/bold cyan]")
|
|
61
79
|
console.print(f"[dim]Project: {wup_config.project.name}[/dim]")
|
|
62
80
|
console.print(f"[dim]Description: {wup_config.project.description}[/dim]")
|
|
63
81
|
console.print(f"[dim]CPU Throttle: {cpu_throttle * 100}%[/dim]")
|
|
64
82
|
console.print(f"[dim]Debounce: {debounce}s[/dim]")
|
|
65
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]")
|
|
66
86
|
console.print(f"[dim]Config: {config_path or 'auto-detected'}[/dim]")
|
|
67
87
|
console.print()
|
|
68
88
|
|
|
89
|
+
cfg_path = config_path if config_path and config_path.exists() else find_config_file(project_path)
|
|
90
|
+
if cfg_path:
|
|
91
|
+
from .monitoring_manifest import build_monitoring_manifest, patch_wup_yaml_monitoring
|
|
92
|
+
try:
|
|
93
|
+
manifest = build_monitoring_manifest(project_path, wup_config)
|
|
94
|
+
patch_wup_yaml_monitoring(cfg_path, manifest)
|
|
95
|
+
console.print("[dim]Refreshed monitoring manifest in wup.yaml[/dim]")
|
|
96
|
+
except OSError as exc:
|
|
97
|
+
console.print(f"[yellow]Could not refresh monitoring manifest: {exc}[/yellow]")
|
|
98
|
+
|
|
69
99
|
if mode.lower() == "testql":
|
|
70
100
|
watcher = TestQLWatcher(
|
|
71
101
|
project_root=str(project_path),
|
|
@@ -73,7 +103,7 @@ def watch(
|
|
|
73
103
|
cpu_throttle=cpu_throttle,
|
|
74
104
|
debounce_seconds=debounce,
|
|
75
105
|
test_cooldown_seconds=cooldown,
|
|
76
|
-
scenarios_dir=
|
|
106
|
+
scenarios_dir=effective_scenarios_dir,
|
|
77
107
|
testql_bin=testql_bin,
|
|
78
108
|
browser_service_url=browser_service_url,
|
|
79
109
|
track_dir=track_dir,
|
|
@@ -274,6 +304,33 @@ def status(
|
|
|
274
304
|
if track_file:
|
|
275
305
|
lines.append(Text.from_markup(f" [dim]track: {track_file}[/dim]"))
|
|
276
306
|
|
|
307
|
+
# --- monitoring manifest (from wup.yaml) ---
|
|
308
|
+
manifest_path = config_path if config_path and config_path.exists() else find_config_file(project_path)
|
|
309
|
+
if manifest_path:
|
|
310
|
+
from .monitoring_manifest import load_monitoring_manifest_from_yaml
|
|
311
|
+
|
|
312
|
+
manifest = load_monitoring_manifest_from_yaml(manifest_path)
|
|
313
|
+
if manifest:
|
|
314
|
+
lines.append(Text(""))
|
|
315
|
+
lines.append(Text.from_markup("[bold]Configured monitoring (wup.yaml):[/bold]"))
|
|
316
|
+
lines.append(Text.from_markup(
|
|
317
|
+
f" [dim]manifest {manifest.get('generated_at', '?')} · "
|
|
318
|
+
f"probe {manifest.get('probe_interval_s', 0)}s[/dim]"
|
|
319
|
+
))
|
|
320
|
+
for svc, info in sorted((manifest.get("wup_services") or {}).items()):
|
|
321
|
+
probes = info.get("live_probes") or []
|
|
322
|
+
dockers = info.get("docker") or []
|
|
323
|
+
lines.append(Text.from_markup(
|
|
324
|
+
f" [cyan]{svc}[/cyan]: {len(probes)} probe(s), docker: "
|
|
325
|
+
+ ", ".join(
|
|
326
|
+
d.get("compose_service", "?") for d in dockers[:4]
|
|
327
|
+
)
|
|
328
|
+
+ ("…" if len(dockers) > 4 else "")
|
|
329
|
+
))
|
|
330
|
+
lines.append(Text.from_markup(
|
|
331
|
+
" [dim]Pełna lista: sekcja monitoring: w wup.yaml (BEGIN WUP MONITORING MANIFEST)[/dim]"
|
|
332
|
+
))
|
|
333
|
+
|
|
277
334
|
# --- visual diff section ---
|
|
278
335
|
if wup_config.visual_diff and wup_config.visual_diff.enabled:
|
|
279
336
|
from .visual_diff import VisualDiffer
|
|
@@ -439,6 +496,91 @@ def map_deps(
|
|
|
439
496
|
console.print(f"[dim]Files: {len(deps.get('files', {}))}[/dim]")
|
|
440
497
|
|
|
441
498
|
|
|
499
|
+
@app.command("sync-testql")
|
|
500
|
+
def sync_testql(
|
|
501
|
+
project: str = typer.Argument(".", help="Path to the project root directory"),
|
|
502
|
+
write: bool = typer.Option(False, "--write", "-w", help="Write monitoring manifest block into wup.yaml"),
|
|
503
|
+
merge_endpoints: bool = typer.Option(
|
|
504
|
+
False,
|
|
505
|
+
"--merge-endpoints",
|
|
506
|
+
help="Also merge discovered paths into testql.endpoints_by_service (rewrites YAML body)",
|
|
507
|
+
),
|
|
508
|
+
config: Optional[str] = typer.Option(None, "--config", "-C", help="Path to wup.yaml config file"),
|
|
509
|
+
):
|
|
510
|
+
"""
|
|
511
|
+
Discover monitoring targets and document them in wup.yaml.
|
|
512
|
+
|
|
513
|
+
With ``--write``, appends/updates the auto-generated ``monitoring:`` block
|
|
514
|
+
(Docker Compose services, live HTTP probes, sources). Use this to verify
|
|
515
|
+
whether a failure is a WUP config gap vs a down container.
|
|
516
|
+
|
|
517
|
+
Use ``--merge-endpoints`` cautiously — it re-serializes wup.yaml (may drop comments).
|
|
518
|
+
"""
|
|
519
|
+
import json
|
|
520
|
+
|
|
521
|
+
from .config import find_config_file, load_config
|
|
522
|
+
from .monitoring_manifest import (
|
|
523
|
+
MANIFEST_BEGIN,
|
|
524
|
+
build_monitoring_manifest,
|
|
525
|
+
format_manifest_summary,
|
|
526
|
+
patch_wup_yaml_monitoring,
|
|
527
|
+
)
|
|
528
|
+
from .testql_monitor import TestQLMonitor
|
|
529
|
+
|
|
530
|
+
project_path = Path(project).resolve()
|
|
531
|
+
if not project_path.exists():
|
|
532
|
+
console.print(f"[red]Error: Project path '{project}' does not exist[/red]")
|
|
533
|
+
raise typer.Exit(1)
|
|
534
|
+
|
|
535
|
+
config_path = Path(config) if config else find_config_file(project_path)
|
|
536
|
+
wup_config = load_config(project_path, config_path)
|
|
537
|
+
monitor = TestQLMonitor(project_path, wup_config)
|
|
538
|
+
suggested = monitor.suggested_endpoints_by_service()
|
|
539
|
+
manifest = build_monitoring_manifest(project_path, wup_config)
|
|
540
|
+
|
|
541
|
+
console.print("[bold]Monitoring manifest (preview):[/bold]")
|
|
542
|
+
console.print(format_manifest_summary(manifest))
|
|
543
|
+
|
|
544
|
+
if suggested:
|
|
545
|
+
console.print()
|
|
546
|
+
console.print("[bold]Suggested testql.endpoints_by_service additions:[/bold]")
|
|
547
|
+
console.print(json.dumps(suggested, indent=2))
|
|
548
|
+
|
|
549
|
+
if not write:
|
|
550
|
+
console.print()
|
|
551
|
+
console.print("[dim]Run: wup sync-testql . --write → dokumentacja w wup.yaml[/dim]")
|
|
552
|
+
return
|
|
553
|
+
|
|
554
|
+
if config_path is None:
|
|
555
|
+
console.print("[red]No wup.yaml found — run `wup init` first[/red]")
|
|
556
|
+
raise typer.Exit(1)
|
|
557
|
+
|
|
558
|
+
if merge_endpoints and suggested:
|
|
559
|
+
import yaml as pyyaml
|
|
560
|
+
|
|
561
|
+
merged = dict(wup_config.testql.endpoints_by_service or {})
|
|
562
|
+
for service, paths in suggested.items():
|
|
563
|
+
existing = set(merged.get(service, []))
|
|
564
|
+
existing.update(paths)
|
|
565
|
+
merged[service] = sorted(existing)
|
|
566
|
+
raw = pyyaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
|
|
567
|
+
raw.setdefault("testql", {})["endpoints_by_service"] = merged
|
|
568
|
+
wup_config.testql.endpoints_by_service = merged
|
|
569
|
+
manifest = build_monitoring_manifest(project_path, wup_config)
|
|
570
|
+
body = pyyaml.safe_dump(
|
|
571
|
+
{k: v for k, v in raw.items() if k != "monitoring"},
|
|
572
|
+
sort_keys=False,
|
|
573
|
+
allow_unicode=True,
|
|
574
|
+
default_flow_style=False,
|
|
575
|
+
)
|
|
576
|
+
config_path.write_text(body.rstrip() + "\n\n", encoding="utf-8")
|
|
577
|
+
console.print("[yellow]Merged endpoints_by_service (review git diff for comment loss)[/yellow]")
|
|
578
|
+
|
|
579
|
+
patch_wup_yaml_monitoring(config_path, manifest)
|
|
580
|
+
console.print(f"[green]✓ monitoring manifest written to {config_path}[/green]")
|
|
581
|
+
console.print(f"[dim]Szukaj w pliku: {MANIFEST_BEGIN}[/dim]")
|
|
582
|
+
|
|
583
|
+
|
|
442
584
|
@app.command()
|
|
443
585
|
def assistant(
|
|
444
586
|
quick: bool = typer.Option(False, "--quick", "-q", help="Non-interactive mode with auto-detected values"),
|