wup 0.2.70__tar.gz → 0.2.71__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.70/wup.egg-info → wup-0.2.71}/PKG-INFO +6 -6
- {wup-0.2.70 → wup-0.2.71}/README.md +5 -5
- {wup-0.2.70 → wup-0.2.71}/pyproject.toml +1 -1
- wup-0.2.71/tests/test_genericity.py +84 -0
- {wup-0.2.70 → wup-0.2.71}/wup/__init__.py +1 -1
- {wup-0.2.70 → wup-0.2.71}/wup/config.py +4 -0
- {wup-0.2.70 → wup-0.2.71}/wup/dependency_mapper.py +56 -39
- {wup-0.2.70 → wup-0.2.71}/wup/models/config.py +6 -0
- {wup-0.2.70 → wup-0.2.71}/wup/monitoring_manifest.py +57 -15
- {wup-0.2.70 → wup-0.2.71/wup.egg-info}/PKG-INFO +6 -6
- {wup-0.2.70 → wup-0.2.71}/wup.egg-info/SOURCES.txt +1 -0
- {wup-0.2.70 → wup-0.2.71}/LICENSE +0 -0
- {wup-0.2.70 → wup-0.2.71}/setup.cfg +0 -0
- {wup-0.2.70 → wup-0.2.71}/tests/test_assistant.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/tests/test_auto_detection.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/tests/test_cli_bridge.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/tests/test_cli_filtering.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/tests/test_control.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/tests/test_e2e.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/tests/test_endpoints_init_cli.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/tests/test_health_summary_passed.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/tests/test_monitoring_manifest.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/tests/test_multi_project.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/tests/test_planfile_reporter_dedupe.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/tests/test_probe_mutex.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/tests/test_service_inference.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/tests/test_status_data.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/tests/test_sync.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/tests/test_testql_monitor.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/tests/test_testql_watcher.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/tests/test_visual_diff_periodic_skip.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/tests/test_visual_diff_progress.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/tests/test_watch_exclude.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/tests/test_watch_no_paths_exit.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/tests/test_web_client.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/tests/test_wup.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/tests/test_wup_generate.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/wup/_ast_detector.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/wup/_base_detector.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/wup/_hash_detector.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/wup/_yaml_detector.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/wup/anomaly_detector.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/wup/anomaly_models.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/wup/assistant.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/wup/assistant_discovery.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/wup/assistant_validator.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/wup/bus.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/wup/cli.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/wup/cli_bridge.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/wup/cli_config_generator.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/wup/cli_scanner.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/wup/control.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/wup/core.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/wup/endpoints.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/wup/event_store.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/wup/file_watcher/events/file_events.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/wup/generate.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/wup/init_cli.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/wup/models/__init__.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/wup/models/target.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/wup/multi.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/wup/paths.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/wup/planfile_reporter.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/wup/status_data.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/wup/sync.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/wup/testing/events/health_events.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/wup/testing/events/test_results.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/wup/testing/handlers/event_handlers.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/wup/testing/handlers/health_handlers.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/wup/testing/queries/health_queries.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/wup/testql_cli_generator.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/wup/testql_discovery.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/wup/testql_monitor.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/wup/testql_watcher.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/wup/validate.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/wup/visual_diff.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/wup/web_client.py +0 -0
- {wup-0.2.70 → wup-0.2.71}/wup.egg-info/dependency_links.txt +0 -0
- {wup-0.2.70 → wup-0.2.71}/wup.egg-info/entry_points.txt +0 -0
- {wup-0.2.70 → wup-0.2.71}/wup.egg-info/requires.txt +0 -0
- {wup-0.2.70 → wup-0.2.71}/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.71
|
|
4
4
|
Summary: WUP (What's Up) - Intelligent file watcher for regression testing in large projects
|
|
5
5
|
Author-email: Tom Sapletta <tom@sapletta.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -42,17 +42,17 @@ Dynamic: license-file
|
|
|
42
42
|
|
|
43
43
|
## AI Cost Tracking
|
|
44
44
|
|
|
45
|
-
    
|
|
46
|
+
  
|
|
47
47
|
|
|
48
|
-
- 🤖 **LLM usage:** $
|
|
49
|
-
- 👤 **Human dev:** ~$
|
|
48
|
+
- 🤖 **LLM usage:** $7.2227 (99 commits)
|
|
49
|
+
- 👤 **Human dev:** ~$5215 (52.1h @ $100/h, 30min dedup)
|
|
50
50
|
|
|
51
51
|
Generated on 2026-07-17 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
52
52
|
|
|
53
53
|
---
|
|
54
54
|
|
|
55
|
-
    
|
|
56
56
|
|
|
57
57
|
**WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
|
|
58
58
|
|
|
@@ -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:** $7.2227 (99 commits)
|
|
10
|
+
- 👤 **Human dev:** ~$5215 (52.1h @ $100/h, 30min dedup)
|
|
11
11
|
|
|
12
12
|
Generated on 2026-07-17 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,84 @@
|
|
|
1
|
+
"""Tests for language-agnostic deps.json discovery and config-driven docker mapping."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from wup.config import validate_config
|
|
8
|
+
from wup.dependency_mapper import DependencyMapper
|
|
9
|
+
from wup.monitoring_manifest import DockerComposeService, _map_docker_to_wup_service
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _dc(compose: str, container: str = "") -> DockerComposeService:
|
|
13
|
+
return DockerComposeService(compose_service=compose, container_name=container)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# --- deps.json genericity -------------------------------------------------
|
|
17
|
+
|
|
18
|
+
def test_express_ts_endpoints_discovered(tmp_path: Path) -> None:
|
|
19
|
+
"""A TypeScript Express project yields a non-empty deps map (regression: rglob brace bug)."""
|
|
20
|
+
svc = tmp_path / "services" / "users"
|
|
21
|
+
svc.mkdir(parents=True)
|
|
22
|
+
(svc / "routes.ts").write_text('router.get("/api/users", h)\nrouter.post("/api/users", h)\n')
|
|
23
|
+
|
|
24
|
+
mapper = DependencyMapper(str(tmp_path))
|
|
25
|
+
result = mapper.build_from_codebase()
|
|
26
|
+
|
|
27
|
+
assert mapper._detect_framework() == "express"
|
|
28
|
+
assert "services/users" in result["services"]
|
|
29
|
+
assert "/api/users" in result["services"]["services/users"]["endpoints"]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_infer_service_uses_services_dir(tmp_path: Path) -> None:
|
|
33
|
+
mapper = DependencyMapper(str(tmp_path))
|
|
34
|
+
assert mapper._infer_service("services/payments/handler.ts") == "services/payments"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_to_dict_handles_service_without_endpoints(tmp_path: Path) -> None:
|
|
38
|
+
"""Services with files but no endpoints must not corrupt the map (regression: fragile zip)."""
|
|
39
|
+
mapper = DependencyMapper(str(tmp_path))
|
|
40
|
+
mapper.service_to_files["orphan"].add("a.py")
|
|
41
|
+
mapper.service_to_endpoints["web"].append("/x")
|
|
42
|
+
|
|
43
|
+
d = mapper.to_dict()
|
|
44
|
+
assert d["services"]["orphan"]["files"] == ["a.py"]
|
|
45
|
+
assert d["services"]["orphan"]["endpoints"] == []
|
|
46
|
+
assert d["services"]["web"]["endpoints"] == ["/x"]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# --- config-driven docker mapping ----------------------------------------
|
|
50
|
+
|
|
51
|
+
def test_docker_map_default_is_generic() -> None:
|
|
52
|
+
"""Without a profile, project-specific fleet literals do not auto-map."""
|
|
53
|
+
services = ["backend", "frontend", "payments"]
|
|
54
|
+
# 'connect-scenario' carries no generic token for these services -> unmapped.
|
|
55
|
+
assert _map_docker_to_wup_service(_dc("connect-scenario"), services) is None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_docker_map_connect_profile_opt_in() -> None:
|
|
59
|
+
services = ["backend", "frontend"]
|
|
60
|
+
assert _map_docker_to_wup_service(_dc("connect-scenario"), services, profile="connect") == "backend"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_docker_map_user_rules() -> None:
|
|
64
|
+
services = ["payments", "web"]
|
|
65
|
+
got = _map_docker_to_wup_service(_dc("stripe-worker"), services, user_map={"stripe": "payments"})
|
|
66
|
+
assert got == "payments"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_docker_map_generic_token_match() -> None:
|
|
70
|
+
services = ["payments", "web"]
|
|
71
|
+
assert _map_docker_to_wup_service(_dc("payments-api"), services) == "payments"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_config_roundtrips_docker_service_map() -> None:
|
|
75
|
+
raw = {
|
|
76
|
+
"project": {"name": "p"},
|
|
77
|
+
"testql": {
|
|
78
|
+
"docker_service_map": {"stripe": "payments"},
|
|
79
|
+
"service_map_profile": "connect",
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
cfg = validate_config(raw)
|
|
83
|
+
assert cfg.testql.docker_service_map == {"stripe": "payments"}
|
|
84
|
+
assert cfg.testql.service_map_profile == "connect"
|
|
@@ -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.71"
|
|
11
11
|
__author__ = "Tom Sapletta"
|
|
12
12
|
|
|
13
13
|
from .config import load_config, save_config, get_default_config
|
|
@@ -230,6 +230,8 @@ def _parse_testql_config(raw: dict) -> TestQLConfig:
|
|
|
230
230
|
health_scenario_strict=bool(testql_raw.get("health_scenario_strict", False)),
|
|
231
231
|
quick_smoke_only=bool(testql_raw.get("quick_smoke_only", False)),
|
|
232
232
|
service_map_globs=testql_raw.get("service_map_globs", []),
|
|
233
|
+
docker_service_map=testql_raw.get("docker_service_map", {}),
|
|
234
|
+
service_map_profile=testql_raw.get("service_map_profile", ""),
|
|
233
235
|
base_url=testql_raw.get("base_url", ""),
|
|
234
236
|
api_base_url=testql_raw.get("api_base_url", ""),
|
|
235
237
|
base_url_env=testql_raw.get("base_url_env", "WUP_BASE_URL"),
|
|
@@ -493,6 +495,8 @@ def save_config(config: WupConfig, output_path: Path):
|
|
|
493
495
|
"health_scenario": config.testql.health_scenario,
|
|
494
496
|
"health_scenario_strict": config.testql.health_scenario_strict,
|
|
495
497
|
"service_map_globs": config.testql.service_map_globs,
|
|
498
|
+
"docker_service_map": config.testql.docker_service_map,
|
|
499
|
+
"service_map_profile": config.testql.service_map_profile,
|
|
496
500
|
"base_url": config.testql.base_url,
|
|
497
501
|
"api_base_url": config.testql.api_base_url,
|
|
498
502
|
"base_url_env": config.testql.base_url_env,
|
|
@@ -11,11 +11,24 @@ This enables intelligent testing by only testing related services when files cha
|
|
|
11
11
|
|
|
12
12
|
import json
|
|
13
13
|
from pathlib import Path
|
|
14
|
-
from typing import Dict, List, Set, Optional
|
|
14
|
+
from typing import Dict, Iterable, Iterator, List, Set, Optional
|
|
15
15
|
from collections import defaultdict
|
|
16
16
|
import re
|
|
17
17
|
from .testql_discovery import TestQLEndpointDiscovery
|
|
18
18
|
|
|
19
|
+
# Source file extensions scanned when discovering endpoints / detecting the
|
|
20
|
+
# framework. Kept broad so JS/TS projects are covered, not just Python.
|
|
21
|
+
PY_EXTENSIONS = (".py",)
|
|
22
|
+
JS_EXTENSIONS = (".js", ".ts", ".jsx", ".tsx", ".mjs", ".cjs")
|
|
23
|
+
SOURCE_EXTENSIONS = PY_EXTENSIONS + JS_EXTENSIONS
|
|
24
|
+
|
|
25
|
+
# Directory names that commonly sit directly above a service/module. Used by
|
|
26
|
+
# _infer_service to derive a service name from a file path in a language-agnostic
|
|
27
|
+
# way (Python `app/`, Node `services/`, monorepo `packages/`, …).
|
|
28
|
+
SERVICE_ROOT_DIRS = (
|
|
29
|
+
"app", "src", "services", "packages", "lib", "modules", "apps", "cmd", "internal", "pkg",
|
|
30
|
+
)
|
|
31
|
+
|
|
19
32
|
|
|
20
33
|
class DependencyMapper:
|
|
21
34
|
"""Maps project dependencies for intelligent testing."""
|
|
@@ -66,27 +79,35 @@ class DependencyMapper:
|
|
|
66
79
|
|
|
67
80
|
def _detect_framework(self) -> str:
|
|
68
81
|
"""Detect the web framework used in the project."""
|
|
69
|
-
#
|
|
82
|
+
# Indicators are language-scoped so Python patterns are only matched in
|
|
83
|
+
# Python files and JS patterns only in JS/TS files — otherwise a frontend
|
|
84
|
+
# file that merely mentions e.g. "Blueprint" would false-positive as Flask.
|
|
70
85
|
indicators = {
|
|
71
|
-
"fastapi": ["FastAPI", "APIRouter", "from fastapi"],
|
|
72
|
-
"flask": ["Flask", "Blueprint", "from flask"],
|
|
73
|
-
"django": ["urlpatterns", "
|
|
74
|
-
"express": ["express()", "app.get(", "router."],
|
|
86
|
+
"fastapi": (["FastAPI", "APIRouter", "from fastapi"], PY_EXTENSIONS),
|
|
87
|
+
"flask": (["Flask", "Blueprint", "from flask"], PY_EXTENSIONS),
|
|
88
|
+
"django": (["urlpatterns", "from django.urls"], PY_EXTENSIONS),
|
|
89
|
+
"express": (["express()", "app.get(", "router.get(", "router.post("], JS_EXTENSIONS),
|
|
75
90
|
}
|
|
76
|
-
|
|
77
|
-
for framework, patterns in indicators.items():
|
|
91
|
+
|
|
92
|
+
for framework, (patterns, extensions) in indicators.items():
|
|
78
93
|
for pattern in patterns:
|
|
79
|
-
if self._search_codebase(pattern):
|
|
94
|
+
if self._search_codebase(pattern, extensions):
|
|
80
95
|
return framework
|
|
81
|
-
|
|
96
|
+
|
|
82
97
|
return "generic"
|
|
83
98
|
|
|
84
|
-
def
|
|
85
|
-
"""
|
|
86
|
-
|
|
99
|
+
def _iter_source_files(self, extensions: Iterable[str]) -> Iterator[Path]:
|
|
100
|
+
"""Yield source files with any of the given extensions (recursively)."""
|
|
101
|
+
wanted = tuple(extensions)
|
|
102
|
+
for path in self.project_root.rglob("*"):
|
|
103
|
+
if path.is_file() and path.suffix in wanted:
|
|
104
|
+
yield path
|
|
105
|
+
|
|
106
|
+
def _search_codebase(self, pattern: str, extensions: Iterable[str] = SOURCE_EXTENSIONS) -> bool:
|
|
107
|
+
"""Search for a pattern across source files of the given extensions."""
|
|
108
|
+
for source_file in self._iter_source_files(extensions):
|
|
87
109
|
try:
|
|
88
|
-
|
|
89
|
-
if pattern in content:
|
|
110
|
+
if pattern in source_file.read_text():
|
|
90
111
|
return True
|
|
91
112
|
except (UnicodeDecodeError, PermissionError):
|
|
92
113
|
continue
|
|
@@ -106,8 +127,8 @@ class DependencyMapper:
|
|
|
106
127
|
def _scan_python_endpoints(self, framework: str) -> List[Dict]:
|
|
107
128
|
"""Scan Python files for endpoint definitions."""
|
|
108
129
|
endpoints = []
|
|
109
|
-
|
|
110
|
-
for py_file in self.
|
|
130
|
+
|
|
131
|
+
for py_file in self._iter_source_files(PY_EXTENSIONS):
|
|
111
132
|
try:
|
|
112
133
|
content = py_file.read_text()
|
|
113
134
|
rel_path = str(py_file.relative_to(self.project_root))
|
|
@@ -154,8 +175,9 @@ class DependencyMapper:
|
|
|
154
175
|
def _scan_js_endpoints(self) -> List[Dict]:
|
|
155
176
|
"""Scan JavaScript/TypeScript files for endpoint definitions."""
|
|
156
177
|
endpoints = []
|
|
157
|
-
|
|
158
|
-
|
|
178
|
+
|
|
179
|
+
# NOTE: pathlib.rglob does not brace-expand, so we iterate real extensions.
|
|
180
|
+
for js_file in self._iter_source_files(JS_EXTENSIONS):
|
|
159
181
|
try:
|
|
160
182
|
content = js_file.read_text()
|
|
161
183
|
rel_path = str(js_file.relative_to(self.project_root))
|
|
@@ -185,22 +207,19 @@ class DependencyMapper:
|
|
|
185
207
|
src/components/auth.ts → "components/auth"
|
|
186
208
|
"""
|
|
187
209
|
parts = Path(file_path).parts
|
|
188
|
-
|
|
189
|
-
#
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
if idx + 1 < len(parts):
|
|
198
|
-
return "/".join(parts[idx:idx+2])
|
|
199
|
-
|
|
210
|
+
|
|
211
|
+
# Derive the service from the segment following a known service-root dir
|
|
212
|
+
# (app/, src/, services/, packages/, …) — language-agnostic.
|
|
213
|
+
for root in SERVICE_ROOT_DIRS:
|
|
214
|
+
if root in parts:
|
|
215
|
+
idx = parts.index(root)
|
|
216
|
+
if idx + 1 < len(parts):
|
|
217
|
+
return "/".join(parts[idx:idx + 2])
|
|
218
|
+
|
|
200
219
|
# Fallback: use first two meaningful parts
|
|
201
220
|
if len(parts) >= 2:
|
|
202
221
|
return "/".join(parts[:2])
|
|
203
|
-
|
|
222
|
+
|
|
204
223
|
return None
|
|
205
224
|
|
|
206
225
|
def get_endpoints_for_file(self, file_path: str) -> List[str]:
|
|
@@ -228,18 +247,16 @@ class DependencyMapper:
|
|
|
228
247
|
|
|
229
248
|
def to_dict(self) -> Dict:
|
|
230
249
|
"""Convert the dependency map to a dictionary."""
|
|
250
|
+
services = sorted(set(self.service_to_endpoints) | set(self.service_to_files))
|
|
231
251
|
return {
|
|
232
252
|
"services": {
|
|
233
253
|
service: {
|
|
234
|
-
"endpoints":
|
|
235
|
-
"files":
|
|
254
|
+
"endpoints": self.service_to_endpoints.get(service, []),
|
|
255
|
+
"files": sorted(self.service_to_files.get(service, set())),
|
|
236
256
|
}
|
|
237
|
-
for service
|
|
238
|
-
self.service_to_endpoints.keys(),
|
|
239
|
-
zip(self.service_to_endpoints.values(), self.service_to_files.values())
|
|
240
|
-
)
|
|
257
|
+
for service in services
|
|
241
258
|
},
|
|
242
|
-
"files": dict(self.file_to_endpoints)
|
|
259
|
+
"files": dict(self.file_to_endpoints),
|
|
243
260
|
}
|
|
244
261
|
|
|
245
262
|
def save(self, output_path: str = "deps.json"):
|
|
@@ -69,6 +69,12 @@ class TestQLConfig:
|
|
|
69
69
|
health_scenario_strict: bool = False # If false, fleet scenario failure is logged but does not block per-service probes
|
|
70
70
|
quick_smoke_only: bool = False # Quick TestQL uses smoke_scenario only (skip auto-api scoring)
|
|
71
71
|
service_map_globs: List[str] = field(default_factory=list) # e.g. testql-testing/service-map/*.yaml
|
|
72
|
+
# docker-compose service/container substring → WUP service name. Lets a project
|
|
73
|
+
# map its containers to services without hardcoding names in WUP itself.
|
|
74
|
+
docker_service_map: Dict[str, str] = field(default_factory=dict)
|
|
75
|
+
# Optional built-in mapping profile (e.g. "connect") applied after
|
|
76
|
+
# docker_service_map and before generic token matching. Empty = generic only.
|
|
77
|
+
service_map_profile: str = ""
|
|
72
78
|
base_url: str = ""
|
|
73
79
|
api_base_url: str = "" # Core API (c2004: http://localhost:8101) — used for backend probes
|
|
74
80
|
base_url_env: str = "WUP_BASE_URL"
|
|
@@ -124,13 +124,13 @@ def _host_port_from_mapping(mapping: str) -> Optional[int]:
|
|
|
124
124
|
return None
|
|
125
125
|
|
|
126
126
|
|
|
127
|
-
def
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
) -> Optional[str]:
|
|
131
|
-
name = docker.compose_service.lower()
|
|
132
|
-
container = docker.container_name.lower()
|
|
127
|
+
def _connect_profile_rules(name: str, container: str, wup_services: Sequence[str]) -> List[Any]:
|
|
128
|
+
"""
|
|
129
|
+
Built-in "connect" mapping profile (maskservice / c2004 fleet).
|
|
133
130
|
|
|
131
|
+
Opt-in via ``testql.service_map_profile: connect``. Kept out of the default
|
|
132
|
+
path so WUP carries no project-specific service names unless asked.
|
|
133
|
+
"""
|
|
134
134
|
def _connect_backend_target() -> Optional[str]:
|
|
135
135
|
if not (name.endswith("-backend") and "connect" in name):
|
|
136
136
|
return None
|
|
@@ -141,20 +141,53 @@ def _map_docker_to_wup_service(
|
|
|
141
141
|
return "backend"
|
|
142
142
|
return None
|
|
143
143
|
|
|
144
|
-
|
|
145
|
-
# candidate that names a configured service wins.
|
|
146
|
-
rules = [
|
|
144
|
+
return [
|
|
147
145
|
lambda: "firmware" if ("firmware" in name or "firmware" in container) else None,
|
|
148
146
|
lambda: "frontend" if (name == "frontend" or "frontend" in container) else None,
|
|
149
147
|
lambda: "backend" if (name == "backend" or container == "identification-backend") else None,
|
|
150
148
|
lambda: "backend" if ("connect-scenario" in name and "backend" not in name) else None,
|
|
151
149
|
_connect_backend_target,
|
|
152
150
|
]
|
|
153
|
-
for rule in rules:
|
|
154
|
-
candidate = rule()
|
|
155
|
-
if candidate and candidate in wup_services:
|
|
156
|
-
return candidate
|
|
157
151
|
|
|
152
|
+
|
|
153
|
+
_BUILTIN_SERVICE_MAP_PROFILES = {
|
|
154
|
+
"connect": _connect_profile_rules,
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _map_docker_to_wup_service(
|
|
159
|
+
docker: DockerComposeService,
|
|
160
|
+
wup_services: Sequence[str],
|
|
161
|
+
*,
|
|
162
|
+
user_map: Optional[Dict[str, str]] = None,
|
|
163
|
+
profile: str = "",
|
|
164
|
+
) -> Optional[str]:
|
|
165
|
+
"""
|
|
166
|
+
Map a docker-compose service to a configured WUP service.
|
|
167
|
+
|
|
168
|
+
Resolution order (first configured match wins):
|
|
169
|
+
1. user rules from ``testql.docker_service_map`` (substring → service),
|
|
170
|
+
2. an opt-in built-in ``profile`` (e.g. "connect"),
|
|
171
|
+
3. generic token matching (service name appears in compose/container name).
|
|
172
|
+
"""
|
|
173
|
+
name = docker.compose_service.lower()
|
|
174
|
+
container = docker.container_name.lower()
|
|
175
|
+
|
|
176
|
+
# 1. User-defined substring rules (fully project-agnostic).
|
|
177
|
+
for needle, target in (user_map or {}).items():
|
|
178
|
+
if needle.lower() in name or needle.lower() in container:
|
|
179
|
+
if target in wup_services:
|
|
180
|
+
return target
|
|
181
|
+
|
|
182
|
+
# 2. Opt-in built-in profile rules.
|
|
183
|
+
profile_factory = _BUILTIN_SERVICE_MAP_PROFILES.get(profile.lower()) if profile else None
|
|
184
|
+
if profile_factory:
|
|
185
|
+
for rule in profile_factory(name, container, wup_services):
|
|
186
|
+
candidate = rule()
|
|
187
|
+
if candidate and candidate in wup_services:
|
|
188
|
+
return candidate
|
|
189
|
+
|
|
190
|
+
# 3. Generic token matching.
|
|
158
191
|
for svc in wup_services:
|
|
159
192
|
token = svc.lower().replace("_", "-")
|
|
160
193
|
if token in name or token in container:
|
|
@@ -188,6 +221,9 @@ def _build_docker_rows(
|
|
|
188
221
|
docker_all: List[DockerComposeService],
|
|
189
222
|
wup_names: List[str],
|
|
190
223
|
by_wup: Dict[str, Dict[str, Any]],
|
|
224
|
+
*,
|
|
225
|
+
user_map: Optional[Dict[str, str]] = None,
|
|
226
|
+
profile: str = "",
|
|
191
227
|
) -> List[Dict[str, Any]]:
|
|
192
228
|
"""Group docker-compose rows under WUP services; return unmapped leftovers."""
|
|
193
229
|
unmapped: List[Dict[str, Any]] = []
|
|
@@ -202,7 +238,7 @@ def _build_docker_rows(
|
|
|
202
238
|
if d.healthcheck_test:
|
|
203
239
|
row["compose_healthcheck"] = d.healthcheck_test
|
|
204
240
|
|
|
205
|
-
mapped = _map_docker_to_wup_service(d, wup_names)
|
|
241
|
+
mapped = _map_docker_to_wup_service(d, wup_names, user_map=user_map, profile=profile)
|
|
206
242
|
if mapped:
|
|
207
243
|
by_wup[mapped]["docker"].append(row)
|
|
208
244
|
else:
|
|
@@ -288,7 +324,13 @@ def build_monitoring_manifest(project_root: Path, config: WupConfig) -> Dict[str
|
|
|
288
324
|
docker_all = discover_docker_compose_services(project_root)
|
|
289
325
|
|
|
290
326
|
by_wup = _build_wup_service_dicts(config)
|
|
291
|
-
unmapped_docker = _build_docker_rows(
|
|
327
|
+
unmapped_docker = _build_docker_rows(
|
|
328
|
+
docker_all,
|
|
329
|
+
wup_names,
|
|
330
|
+
by_wup,
|
|
331
|
+
user_map=config.testql.docker_service_map,
|
|
332
|
+
profile=config.testql.service_map_profile,
|
|
333
|
+
)
|
|
292
334
|
|
|
293
335
|
# Live HTTP probes (exactly what WUP will call)
|
|
294
336
|
for svc_name in wup_names:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wup
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.71
|
|
4
4
|
Summary: WUP (What's Up) - Intelligent file watcher for regression testing in large projects
|
|
5
5
|
Author-email: Tom Sapletta <tom@sapletta.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -42,17 +42,17 @@ Dynamic: license-file
|
|
|
42
42
|
|
|
43
43
|
## AI Cost Tracking
|
|
44
44
|
|
|
45
|
-
    
|
|
46
|
+
  
|
|
47
47
|
|
|
48
|
-
- 🤖 **LLM usage:** $
|
|
49
|
-
- 👤 **Human dev:** ~$
|
|
48
|
+
- 🤖 **LLM usage:** $7.2227 (99 commits)
|
|
49
|
+
- 👤 **Human dev:** ~$5215 (52.1h @ $100/h, 30min dedup)
|
|
50
50
|
|
|
51
51
|
Generated on 2026-07-17 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
52
52
|
|
|
53
53
|
---
|
|
54
54
|
|
|
55
|
-
    
|
|
56
56
|
|
|
57
57
|
**WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
|
|
58
58
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|