wup 0.2.76__tar.gz → 0.2.78__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.76/wup.egg-info → wup-0.2.78}/PKG-INFO +12 -7
- {wup-0.2.76 → wup-0.2.78}/README.md +6 -6
- {wup-0.2.76 → wup-0.2.78}/pyproject.toml +18 -2
- {wup-0.2.76 → wup-0.2.78}/tests/test_assistant.py +0 -1
- {wup-0.2.76 → wup-0.2.78}/tests/test_auto_detection.py +1 -1
- wup-0.2.78/tests/test_bootstrap.py +31 -0
- {wup-0.2.76 → wup-0.2.78}/tests/test_cli_filtering.py +5 -5
- {wup-0.2.76 → wup-0.2.78}/tests/test_e2e.py +1 -3
- {wup-0.2.76 → wup-0.2.78}/tests/test_monitoring_manifest.py +0 -1
- {wup-0.2.76 → wup-0.2.78}/tests/test_multi_project.py +38 -1
- {wup-0.2.76 → wup-0.2.78}/tests/test_service_inference.py +0 -2
- {wup-0.2.76 → wup-0.2.78}/tests/test_testql_monitor.py +0 -3
- {wup-0.2.76 → wup-0.2.78}/tests/test_visual_diff_progress.py +0 -1
- {wup-0.2.76 → wup-0.2.78}/tests/test_wup.py +38 -0
- {wup-0.2.76 → wup-0.2.78}/wup/__init__.py +1 -1
- {wup-0.2.76 → wup-0.2.78}/wup/aql.py +36 -34
- {wup-0.2.76 → wup-0.2.78}/wup/assistant.py +4 -14
- wup-0.2.78/wup/bootstrap.py +61 -0
- {wup-0.2.76 → wup-0.2.78}/wup/cli.py +9 -9
- wup-0.2.78/wup/cli_bridge.py +194 -0
- {wup-0.2.76 → wup-0.2.78}/wup/cli_config_generator.py +2 -2
- {wup-0.2.76 → wup-0.2.78}/wup/cli_scanner.py +1 -2
- {wup-0.2.76 → wup-0.2.78}/wup/config.py +178 -203
- {wup-0.2.76 → wup-0.2.78}/wup/control.py +28 -7
- {wup-0.2.76 → wup-0.2.78}/wup/core.py +2 -2
- {wup-0.2.76 → wup-0.2.78}/wup/testing/events/test_results.py +0 -1
- {wup-0.2.76 → wup-0.2.78}/wup/testql_cli_generator.py +1 -1
- {wup-0.2.76 → wup-0.2.78}/wup/testql_discovery.py +1 -1
- {wup-0.2.76 → wup-0.2.78}/wup/testql_watcher.py +2 -2
- {wup-0.2.76 → wup-0.2.78/wup.egg-info}/PKG-INFO +12 -7
- {wup-0.2.76 → wup-0.2.78}/wup.egg-info/SOURCES.txt +2 -0
- wup-0.2.78/wup.egg-info/entry_points.txt +2 -0
- {wup-0.2.76 → wup-0.2.78}/wup.egg-info/requires.txt +6 -0
- wup-0.2.76/wup/cli_bridge.py +0 -80
- wup-0.2.76/wup.egg-info/entry_points.txt +0 -2
- {wup-0.2.76 → wup-0.2.78}/LICENSE +0 -0
- {wup-0.2.76 → wup-0.2.78}/setup.cfg +0 -0
- {wup-0.2.76 → wup-0.2.78}/tests/test_aql.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/tests/test_cli_bridge.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/tests/test_control.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/tests/test_discovery_adapters.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/tests/test_endpoints_init_cli.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/tests/test_genericity.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/tests/test_health_summary_passed.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/tests/test_oql.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/tests/test_planfile_reporter_dedupe.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/tests/test_probe_mutex.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/tests/test_status_data.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/tests/test_sync.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/tests/test_testql_watcher.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/tests/test_visual_diff_periodic_skip.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/tests/test_watch_exclude.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/tests/test_watch_no_paths_exit.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/tests/test_web_client.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/tests/test_wup_generate.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/wup/_ast_detector.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/wup/_base_detector.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/wup/_hash_detector.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/wup/_yaml_detector.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/wup/anomaly_detector.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/wup/anomaly_models.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/wup/assistant_discovery.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/wup/assistant_validator.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/wup/bus.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/wup/dependency_mapper.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/wup/discovery.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/wup/endpoints.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/wup/event_store.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/wup/file_watcher/events/file_events.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/wup/generate.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/wup/init_cli.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/wup/models/__init__.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/wup/models/config.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/wup/models/target.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/wup/monitoring_manifest.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/wup/multi.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/wup/oql.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/wup/paths.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/wup/planfile_reporter.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/wup/status_data.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/wup/sync.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/wup/testing/events/health_events.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/wup/testing/handlers/event_handlers.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/wup/testing/handlers/health_handlers.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/wup/testing/queries/health_queries.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/wup/testql_monitor.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/wup/validate.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/wup/visual_diff.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/wup/web_client.py +0 -0
- {wup-0.2.76 → wup-0.2.78}/wup.egg-info/dependency_links.txt +0 -0
- {wup-0.2.76 → wup-0.2.78}/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.78
|
|
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
|
|
@@ -23,6 +23,8 @@ Requires-Dist: typer>=0.9.0
|
|
|
23
23
|
Requires-Dist: pyyaml>=6.0
|
|
24
24
|
Provides-Extra: dev
|
|
25
25
|
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
27
|
+
Requires-Dist: ruff>=0.8; extra == "dev"
|
|
26
28
|
Requires-Dist: goal>=2.1.0; extra == "dev"
|
|
27
29
|
Requires-Dist: costs>=0.1.20; extra == "dev"
|
|
28
30
|
Requires-Dist: pfix>=0.1.60; extra == "dev"
|
|
@@ -35,6 +37,9 @@ Requires-Dist: rest2wup; extra == "dev"
|
|
|
35
37
|
Requires-Dist: httpx>=0.27; extra == "dev"
|
|
36
38
|
Provides-Extra: visual
|
|
37
39
|
Requires-Dist: playwright<2,>=1.40; extra == "visual"
|
|
40
|
+
Provides-Extra: control
|
|
41
|
+
Requires-Dist: dsl2wup>=0.1.0; extra == "control"
|
|
42
|
+
Requires-Dist: uri2wup>=0.1.0; extra == "control"
|
|
38
43
|
Dynamic: license-file
|
|
39
44
|
|
|
40
45
|
# WUP (What's Up)
|
|
@@ -42,17 +47,17 @@ Dynamic: license-file
|
|
|
42
47
|
|
|
43
48
|
## AI Cost Tracking
|
|
44
49
|
|
|
45
|
-
    
|
|
51
|
+
  
|
|
47
52
|
|
|
48
|
-
- 🤖 **LLM usage:** $
|
|
49
|
-
- 👤 **Human dev:** ~$
|
|
53
|
+
- 🤖 **LLM usage:** $7.1247 (108 commits)
|
|
54
|
+
- 👤 **Human dev:** ~$5451 (54.5h @ $100/h, 30min dedup)
|
|
50
55
|
|
|
51
|
-
Generated on 2026-07-
|
|
56
|
+
Generated on 2026-07-29 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
52
57
|
|
|
53
58
|
---
|
|
54
59
|
|
|
55
|
-
    
|
|
56
61
|
|
|
57
62
|
**WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
|
|
58
63
|
|
|
@@ -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.1247 (108 commits)
|
|
10
|
+
- 👤 **Human dev:** ~$5451 (54.5h @ $100/h, 30min dedup)
|
|
11
11
|
|
|
12
|
-
Generated on 2026-07-
|
|
12
|
+
Generated on 2026-07-29 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
16
|
-
    
|
|
17
17
|
|
|
18
18
|
**WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
|
|
19
19
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "wup"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.78"
|
|
8
8
|
description = "WUP (What's Up) - Intelligent file watcher for regression testing in large projects"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -32,6 +32,8 @@ license = "Apache-2.0"
|
|
|
32
32
|
[project.optional-dependencies]
|
|
33
33
|
dev = [
|
|
34
34
|
"pytest>=7.0.0",
|
|
35
|
+
"pytest-cov>=5.0",
|
|
36
|
+
"ruff>=0.8",
|
|
35
37
|
"goal>=2.1.0",
|
|
36
38
|
"costs>=0.1.20",
|
|
37
39
|
"pfix>=0.1.60",
|
|
@@ -46,10 +48,16 @@ dev = [
|
|
|
46
48
|
visual = [
|
|
47
49
|
"playwright>=1.40,<2",
|
|
48
50
|
]
|
|
51
|
+
control = [
|
|
52
|
+
"dsl2wup>=0.1.0",
|
|
53
|
+
"uri2wup>=0.1.0",
|
|
54
|
+
]
|
|
49
55
|
|
|
50
56
|
[dependency-groups]
|
|
51
57
|
dev = [
|
|
52
58
|
"pytest>=7.0.0",
|
|
59
|
+
"pytest-cov>=5.0",
|
|
60
|
+
"ruff>=0.8",
|
|
53
61
|
"goal>=2.1.0",
|
|
54
62
|
"costs>=0.1.20",
|
|
55
63
|
"pfix>=0.1.60",
|
|
@@ -87,11 +95,19 @@ testpaths = ["tests", "packages"]
|
|
|
87
95
|
python_files = ["test_*.py"]
|
|
88
96
|
addopts = "-ra --tb=short"
|
|
89
97
|
|
|
98
|
+
[tool.ruff.lint]
|
|
99
|
+
# Start with Ruff's stable Pyflakes/pycodestyle correctness baseline. Broader
|
|
100
|
+
# modernization and complexity families can be enabled incrementally.
|
|
101
|
+
select = ["E4", "E7", "E9", "F"]
|
|
102
|
+
|
|
103
|
+
[tool.ruff.lint.per-file-ignores]
|
|
104
|
+
"packages/dsl2wup/src/dsl2wup/v1/*_pb2.py" = ["E402", "F401"]
|
|
105
|
+
|
|
90
106
|
[tool.setuptools.packages.find]
|
|
91
107
|
include = ["wup*"]
|
|
92
108
|
|
|
93
109
|
[project.scripts]
|
|
94
|
-
wup = "wup.
|
|
110
|
+
wup = "wup.bootstrap:main"
|
|
95
111
|
|
|
96
112
|
[project.urls]
|
|
97
113
|
Homepage = "https://github.com/semcod/wup"
|
|
@@ -4,7 +4,7 @@ from pathlib import Path
|
|
|
4
4
|
|
|
5
5
|
from wup.cli_scanner import CLIScanner
|
|
6
6
|
from wup.cli_config_generator import CLIConfigGenerator
|
|
7
|
-
from wup.config import
|
|
7
|
+
from wup.config import load_config
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
def test_cli_scanner_detects_from_pyproject_toml():
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Tests for the crash-safe console bootstrap."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from unittest.mock import Mock
|
|
6
|
+
|
|
7
|
+
from wup import bootstrap
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_watchdog_preflight_reports_signal(monkeypatch) -> None:
|
|
11
|
+
monkeypatch.setattr(
|
|
12
|
+
bootstrap.subprocess,
|
|
13
|
+
"run",
|
|
14
|
+
Mock(return_value=Mock(returncode=-11, stderr="")),
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
ok, detail = bootstrap._watchdog_preflight()
|
|
18
|
+
|
|
19
|
+
assert ok is False
|
|
20
|
+
assert "signal 11" in detail
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_main_stops_before_importing_cli_on_failed_watch_preflight(
|
|
24
|
+
monkeypatch, capsys
|
|
25
|
+
) -> None:
|
|
26
|
+
monkeypatch.setattr(bootstrap, "_watchdog_preflight", lambda: (False, "signal 11"))
|
|
27
|
+
|
|
28
|
+
code = bootstrap.main(["watch", "."])
|
|
29
|
+
|
|
30
|
+
assert code == 70
|
|
31
|
+
assert "ABI preflight" in capsys.readouterr().err
|
|
@@ -258,8 +258,8 @@ def test_scenario_matches_type():
|
|
|
258
258
|
)
|
|
259
259
|
|
|
260
260
|
# Test type matching
|
|
261
|
-
assert watcher._scenario_matches_type(cli_scenario, "shell")
|
|
262
|
-
assert watcher._scenario_matches_type(cli_scenario, "web")
|
|
263
|
-
assert watcher._scenario_matches_type(web_scenario, "shell")
|
|
264
|
-
assert watcher._scenario_matches_type(web_scenario, "web")
|
|
265
|
-
assert watcher._scenario_matches_type(cli_scenario, "auto")
|
|
261
|
+
assert watcher._scenario_matches_type(cli_scenario, "shell") is True
|
|
262
|
+
assert watcher._scenario_matches_type(cli_scenario, "web") is False
|
|
263
|
+
assert watcher._scenario_matches_type(web_scenario, "shell") is False
|
|
264
|
+
assert watcher._scenario_matches_type(web_scenario, "web") is True
|
|
265
|
+
assert watcher._scenario_matches_type(cli_scenario, "auto") is True
|
|
@@ -7,9 +7,7 @@ import sys
|
|
|
7
7
|
import tempfile
|
|
8
8
|
import time
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from typing import List
|
|
11
10
|
|
|
12
|
-
import pytest
|
|
13
11
|
|
|
14
12
|
|
|
15
13
|
def run_wup_command(args, cwd=None, timeout=30, capture_output=True, text=True):
|
|
@@ -115,7 +113,7 @@ def get_users():
|
|
|
115
113
|
"files": {"app/users/routes.py": ["/users"]}
|
|
116
114
|
}))
|
|
117
115
|
|
|
118
|
-
|
|
116
|
+
run_wup_command(
|
|
119
117
|
[sys.executable, "-m", "wup.cli", "status", "--deps", str(deps_file)],
|
|
120
118
|
cwd=tmpdir,
|
|
121
119
|
capture_output=True,
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import os
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
|
|
7
8
|
from wup.cli import _discover_projects, _resolve_project_paths
|
|
8
|
-
from wup.config import detect_watch_paths, get_default_config
|
|
9
|
+
from wup.config import detect_watch_paths, get_default_config, load_config
|
|
9
10
|
from wup.core import WupWatcher
|
|
10
11
|
from wup.models.config import ProjectConfig, WatchConfig, WupConfig
|
|
11
12
|
from wup.multi import MultiProjectWatcher
|
|
@@ -48,12 +49,48 @@ def test_detect_watch_paths_falls_back_when_nothing_matches(tmp_path: Path) -> N
|
|
|
48
49
|
assert detect_watch_paths(tmp_path) == ["app/**", "src/**", "routes/**"]
|
|
49
50
|
|
|
50
51
|
|
|
52
|
+
def test_detect_watch_paths_backend_frontend(tmp_path: Path) -> None:
|
|
53
|
+
# c2004-style module layout (backend/ + frontend/, no app/src).
|
|
54
|
+
(tmp_path / "backend").mkdir()
|
|
55
|
+
(tmp_path / "frontend").mkdir()
|
|
56
|
+
paths = detect_watch_paths(tmp_path)
|
|
57
|
+
assert "backend/**" in paths and "frontend/**" in paths
|
|
58
|
+
assert "app/**" not in paths
|
|
59
|
+
|
|
60
|
+
|
|
51
61
|
def test_default_config_watches_only_real_dirs(tmp_path: Path) -> None:
|
|
52
62
|
(tmp_path / "services").mkdir()
|
|
53
63
|
cfg = get_default_config(tmp_path)
|
|
54
64
|
assert cfg.watch.paths == ["services/**"]
|
|
55
65
|
|
|
56
66
|
|
|
67
|
+
def test_project_dotenv_is_resolved_without_cross_project_leakage(
|
|
68
|
+
tmp_path: Path, monkeypatch
|
|
69
|
+
) -> None:
|
|
70
|
+
monkeypatch.delenv("WUP_BASE_URL", raising=False)
|
|
71
|
+
monkeypatch.delenv("WUPBRO_ENDPOINT", raising=False)
|
|
72
|
+
alpha = _make_project(tmp_path, "alpha")
|
|
73
|
+
beta = _make_project(tmp_path, "beta")
|
|
74
|
+
(alpha / ".wup.env").write_text(
|
|
75
|
+
"WUP_BASE_URL=http://alpha.invalid\nWUPBRO_ENDPOINT=http://alpha-web.invalid\n",
|
|
76
|
+
encoding="utf-8",
|
|
77
|
+
)
|
|
78
|
+
(beta / ".wup.env").write_text(
|
|
79
|
+
"WUP_BASE_URL=http://beta.invalid\nWUPBRO_ENDPOINT=http://beta-web.invalid\n",
|
|
80
|
+
encoding="utf-8",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
alpha_cfg = load_config(alpha)
|
|
84
|
+
beta_cfg = load_config(beta)
|
|
85
|
+
|
|
86
|
+
assert alpha_cfg.testql.base_url == "http://alpha.invalid"
|
|
87
|
+
assert beta_cfg.testql.base_url == "http://beta.invalid"
|
|
88
|
+
assert alpha_cfg.web.endpoint == "http://alpha-web.invalid"
|
|
89
|
+
assert beta_cfg.web.endpoint == "http://beta-web.invalid"
|
|
90
|
+
assert "WUP_BASE_URL" not in os.environ
|
|
91
|
+
assert "WUPBRO_ENDPOINT" not in os.environ
|
|
92
|
+
|
|
93
|
+
|
|
57
94
|
def test_discover_finds_subprojects(tmp_path: Path) -> None:
|
|
58
95
|
_make_project(tmp_path, "alpha")
|
|
59
96
|
_make_project(tmp_path, "beta")
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""Unit tests for service inference logic."""
|
|
2
2
|
import tempfile
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from unittest.mock import Mock
|
|
5
4
|
|
|
6
5
|
from wup.core import WupWatcher
|
|
7
6
|
from wup.models.config import (
|
|
@@ -201,7 +200,6 @@ def test_file_change_uses_configured_services_when_inference_fails():
|
|
|
201
200
|
|
|
202
201
|
# Mock schedule_quick_test to track which services are tested
|
|
203
202
|
tested_services = []
|
|
204
|
-
original_schedule = watcher.schedule_quick_test
|
|
205
203
|
watcher.schedule_quick_test = lambda s: tested_services.append(s)
|
|
206
204
|
|
|
207
205
|
# Trigger file change
|
|
@@ -11,7 +11,6 @@ from wup.testql_monitor import (
|
|
|
11
11
|
assign_probe_to_service,
|
|
12
12
|
is_monitoring_probe,
|
|
13
13
|
parse_scenario_probes,
|
|
14
|
-
parse_service_map_probes,
|
|
15
14
|
)
|
|
16
15
|
from wup.testql_watcher import TestQLWatcher
|
|
17
16
|
|
|
@@ -206,8 +205,6 @@ def test_live_probe_failure_updates_health():
|
|
|
206
205
|
config=cfg,
|
|
207
206
|
)
|
|
208
207
|
|
|
209
|
-
failing = ProbeTarget(url="http://localhost:8100/firmware/api/v1/health")
|
|
210
|
-
|
|
211
208
|
def fake_probe(self, timeout_s=10.0):
|
|
212
209
|
return False, "HTTP 500 (expected 200)"
|
|
213
210
|
|
|
@@ -23,6 +23,7 @@ from wup.models.config import (
|
|
|
23
23
|
ProjectConfig,
|
|
24
24
|
PlanfileConfig,
|
|
25
25
|
VisualDiffConfig,
|
|
26
|
+
AnomalyDetectionConfig,
|
|
26
27
|
)
|
|
27
28
|
from wup.testql_watcher import TestQLWatcher
|
|
28
29
|
from wup.visual_diff import (
|
|
@@ -1313,6 +1314,43 @@ class TestConfigLoader:
|
|
|
1313
1314
|
assert loaded_config.project.name == "test-project"
|
|
1314
1315
|
assert len(loaded_config.services) == 1
|
|
1315
1316
|
assert loaded_config.services[0].name == "users"
|
|
1317
|
+
|
|
1318
|
+
def test_save_and_load_preserves_every_non_default_section(self):
|
|
1319
|
+
"""Fields added to the dataclass schema must not disappear on save."""
|
|
1320
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1321
|
+
config = WupConfig(
|
|
1322
|
+
project=ProjectConfig(name="roundtrip", description="all fields"),
|
|
1323
|
+
watch=WatchConfig(
|
|
1324
|
+
paths=["src/**"],
|
|
1325
|
+
exclude_patterns=["dist/**"],
|
|
1326
|
+
file_types=[".py", ".ts"],
|
|
1327
|
+
),
|
|
1328
|
+
services=[ServiceConfig(
|
|
1329
|
+
name="worker",
|
|
1330
|
+
type="shell",
|
|
1331
|
+
cpu_throttle=0.25,
|
|
1332
|
+
notify=NotifyConfig(type="http", url="http://notify.invalid"),
|
|
1333
|
+
)],
|
|
1334
|
+
testql=TestQLConfig(quick_smoke_only=True),
|
|
1335
|
+
visual_diff=VisualDiffConfig(run_on_periodic_probe=True),
|
|
1336
|
+
anomaly_detection=AnomalyDetectionConfig(
|
|
1337
|
+
enabled=False,
|
|
1338
|
+
methods=["ast"],
|
|
1339
|
+
strict_mode=True,
|
|
1340
|
+
watch_paths=["schemas/**"],
|
|
1341
|
+
severity_threshold="high",
|
|
1342
|
+
),
|
|
1343
|
+
)
|
|
1344
|
+
config_path = Path(tmpdir) / "wup.yaml"
|
|
1345
|
+
|
|
1346
|
+
save_config(config, config_path)
|
|
1347
|
+
loaded = load_config(Path(tmpdir), config_path)
|
|
1348
|
+
|
|
1349
|
+
assert loaded.watch.file_types == [".py", ".ts"]
|
|
1350
|
+
assert loaded.services[0].type == "shell"
|
|
1351
|
+
assert loaded.testql.quick_smoke_only is True
|
|
1352
|
+
assert loaded.visual_diff.run_on_periodic_probe is True
|
|
1353
|
+
assert loaded.anomaly_detection == config.anomaly_detection
|
|
1316
1354
|
|
|
1317
1355
|
def test_load_config_from_yaml(self):
|
|
1318
1356
|
"""Test loading configuration from YAML file."""
|
|
@@ -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.78"
|
|
11
11
|
__author__ = "Tom Sapletta"
|
|
12
12
|
|
|
13
13
|
from .config import load_config, save_config, get_default_config
|
|
@@ -130,51 +130,53 @@ def _tokenize(rule: str) -> List[str]:
|
|
|
130
130
|
return [placeholder.get(t, t) for t in tokens]
|
|
131
131
|
|
|
132
132
|
|
|
133
|
+
def _rule_selector(tokens: List[str]) -> Tuple[str, str]:
|
|
134
|
+
selector = tokens.pop(0).lower()
|
|
135
|
+
if selector not in _SELECTORS:
|
|
136
|
+
raise AQLError(f"unknown selector {selector!r}; expected one of {', '.join(_SELECTORS)}")
|
|
137
|
+
if selector == "text":
|
|
138
|
+
return selector, ""
|
|
139
|
+
if not tokens:
|
|
140
|
+
raise AQLError(f"selector {selector!r} requires a path")
|
|
141
|
+
return selector, tokens.pop(0)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _predicate_rule(selector: str, path: str, keyword: str, tokens: List[str], severity: str, raw: str) -> AQLRule:
|
|
145
|
+
lower = keyword.lower()
|
|
146
|
+
if lower in ("exists", "missing"):
|
|
147
|
+
return AQLRule(selector, path, lower, severity=severity, raw=raw)
|
|
148
|
+
if lower == "matches":
|
|
149
|
+
if tokens:
|
|
150
|
+
return AQLRule(selector, path, "matches", tokens.pop(0), severity=severity, raw=raw)
|
|
151
|
+
raise AQLError("'matches' requires a regex")
|
|
152
|
+
if lower == "type":
|
|
153
|
+
if tokens and tokens[0].lower() in _TYPE_NAMES:
|
|
154
|
+
return AQLRule(selector, path, "type", tokens.pop(0).lower(), severity=severity, raw=raw)
|
|
155
|
+
raise AQLError(f"'type' requires one of {', '.join(sorted(_TYPE_NAMES))}")
|
|
156
|
+
if lower == "length":
|
|
157
|
+
if len(tokens) >= 2 and tokens[0] in _COMPARATORS and tokens[1].lstrip("-").isdigit():
|
|
158
|
+
return AQLRule(selector, path, "length", tokens[1], length_op=tokens[0], severity=severity, raw=raw)
|
|
159
|
+
raise AQLError("'length' requires an operator and number, e.g. length > 0")
|
|
160
|
+
if keyword in _COMPARATORS and tokens:
|
|
161
|
+
return AQLRule(selector, path, keyword, tokens.pop(0), severity=severity, raw=raw)
|
|
162
|
+
if keyword in _COMPARATORS:
|
|
163
|
+
raise AQLError(f"operator {keyword!r} requires a value")
|
|
164
|
+
raise AQLError(f"unknown predicate {keyword!r}")
|
|
165
|
+
|
|
166
|
+
|
|
133
167
|
def parse_rule(rule: str) -> AQLRule:
|
|
134
168
|
"""Parse a single AQL rule string into an :class:`AQLRule`."""
|
|
135
169
|
tokens, severity = _split_severity(_tokenize(rule))
|
|
136
170
|
if not tokens:
|
|
137
171
|
raise AQLError("empty rule")
|
|
138
172
|
|
|
139
|
-
selector = tokens
|
|
140
|
-
if selector not in _SELECTORS:
|
|
141
|
-
raise AQLError(f"unknown selector {selector!r}; expected one of {', '.join(_SELECTORS)}")
|
|
142
|
-
|
|
143
|
-
path = ""
|
|
144
|
-
if selector != "text":
|
|
145
|
-
if not tokens:
|
|
146
|
-
raise AQLError(f"selector {selector!r} requires a path")
|
|
147
|
-
path = tokens.pop(0)
|
|
173
|
+
selector, path = _rule_selector(tokens)
|
|
148
174
|
|
|
149
175
|
if not tokens:
|
|
150
176
|
raise AQLError("rule requires a predicate (exists, =, length, type, …)")
|
|
151
177
|
|
|
152
178
|
keyword = tokens.pop(0)
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if kw_lower in ("exists", "missing"):
|
|
156
|
-
return AQLRule(selector, path, kw_lower, severity=severity, raw=rule)
|
|
157
|
-
if kw_lower == "matches":
|
|
158
|
-
if not tokens:
|
|
159
|
-
raise AQLError("'matches' requires a regex")
|
|
160
|
-
return AQLRule(selector, path, "matches", tokens.pop(0), severity=severity, raw=rule)
|
|
161
|
-
if kw_lower == "type":
|
|
162
|
-
if not tokens or tokens[0].lower() not in _TYPE_NAMES:
|
|
163
|
-
raise AQLError(f"'type' requires one of {', '.join(sorted(_TYPE_NAMES))}")
|
|
164
|
-
return AQLRule(selector, path, "type", tokens.pop(0).lower(), severity=severity, raw=rule)
|
|
165
|
-
if kw_lower == "length":
|
|
166
|
-
if len(tokens) < 2 or tokens[0] not in _COMPARATORS:
|
|
167
|
-
raise AQLError("'length' requires an operator and number, e.g. length > 0")
|
|
168
|
-
length_op, number = tokens[0], tokens[1]
|
|
169
|
-
if not number.lstrip("-").isdigit():
|
|
170
|
-
raise AQLError(f"'length' expects an integer, got {number!r}")
|
|
171
|
-
return AQLRule(selector, path, "length", number, length_op=length_op, severity=severity, raw=rule)
|
|
172
|
-
if keyword in _COMPARATORS:
|
|
173
|
-
if not tokens:
|
|
174
|
-
raise AQLError(f"operator {keyword!r} requires a value")
|
|
175
|
-
return AQLRule(selector, path, keyword, tokens.pop(0), severity=severity, raw=rule)
|
|
176
|
-
|
|
177
|
-
raise AQLError(f"unknown predicate {keyword!r}")
|
|
179
|
+
return _predicate_rule(selector, path, keyword, tokens, severity, rule)
|
|
178
180
|
|
|
179
181
|
|
|
180
182
|
# --- evaluation -----------------------------------------------------------
|
|
@@ -11,12 +11,9 @@ Usage:
|
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
|
-
import
|
|
15
|
-
import re
|
|
16
|
-
import sys
|
|
17
|
-
from dataclasses import asdict, field
|
|
14
|
+
from dataclasses import asdict
|
|
18
15
|
from pathlib import Path
|
|
19
|
-
from typing import
|
|
16
|
+
from typing import Dict, List, Optional
|
|
20
17
|
|
|
21
18
|
import yaml
|
|
22
19
|
|
|
@@ -24,19 +21,12 @@ from rich.console import Console
|
|
|
24
21
|
from rich.panel import Panel
|
|
25
22
|
from rich.prompt import Confirm, IntPrompt, Prompt
|
|
26
23
|
from rich.table import Table
|
|
27
|
-
from rich.tree import Tree
|
|
28
24
|
|
|
29
25
|
from .models.config import (
|
|
30
26
|
AnomalyDetectionConfig,
|
|
31
|
-
NotifyConfig,
|
|
32
27
|
ProjectConfig,
|
|
33
28
|
ServiceConfig,
|
|
34
29
|
ServiceType,
|
|
35
|
-
TestQLConfig,
|
|
36
|
-
TestStrategyConfig,
|
|
37
|
-
VisualDiffConfig,
|
|
38
|
-
WatchConfig,
|
|
39
|
-
WebConfig,
|
|
40
30
|
WupConfig,
|
|
41
31
|
)
|
|
42
32
|
from .assistant_discovery import (
|
|
@@ -52,7 +42,7 @@ from .assistant_validator import (
|
|
|
52
42
|
|
|
53
43
|
# Import ServiceType for type checking
|
|
54
44
|
if False: # TYPE_CHECKING
|
|
55
|
-
|
|
45
|
+
pass
|
|
56
46
|
|
|
57
47
|
console = Console()
|
|
58
48
|
|
|
@@ -513,7 +503,7 @@ class WupAssistant:
|
|
|
513
503
|
yaml.dump(config_dict, default_flow_style=False, sort_keys=False, allow_unicode=True)
|
|
514
504
|
)
|
|
515
505
|
|
|
516
|
-
console.print(
|
|
506
|
+
console.print("\n[bold green]✓ Configuration saved to wup.yaml[/bold green]")
|
|
517
507
|
|
|
518
508
|
# Clean up draft
|
|
519
509
|
if self.draft_path.exists():
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Minimal console bootstrap with a crash-safe watcher dependency preflight."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
|
|
10
|
+
_WATCH_PREFLIGHT = """
|
|
11
|
+
try:
|
|
12
|
+
import resource
|
|
13
|
+
resource.setrlimit(resource.RLIMIT_CORE, (0, 0))
|
|
14
|
+
except (ImportError, OSError, ValueError):
|
|
15
|
+
pass
|
|
16
|
+
import ctypes
|
|
17
|
+
from watchdog.observers import Observer
|
|
18
|
+
assert Observer
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _watchdog_preflight() -> tuple[bool, str]:
|
|
23
|
+
"""Probe native/stdlib imports in a child so a segfault is reportable."""
|
|
24
|
+
try:
|
|
25
|
+
completed = subprocess.run(
|
|
26
|
+
[sys.executable, "-c", _WATCH_PREFLIGHT],
|
|
27
|
+
stdin=subprocess.DEVNULL,
|
|
28
|
+
stdout=subprocess.DEVNULL,
|
|
29
|
+
stderr=subprocess.PIPE,
|
|
30
|
+
text=True,
|
|
31
|
+
timeout=15,
|
|
32
|
+
check=False,
|
|
33
|
+
)
|
|
34
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
35
|
+
return False, str(exc)
|
|
36
|
+
if completed.returncode == 0:
|
|
37
|
+
return True, ""
|
|
38
|
+
if completed.returncode < 0:
|
|
39
|
+
detail = f"child terminated by signal {-completed.returncode}"
|
|
40
|
+
else:
|
|
41
|
+
detail = completed.stderr.strip() or f"child exited with {completed.returncode}"
|
|
42
|
+
return False, detail
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def main(argv: Sequence[str] | None = None) -> int | None:
|
|
46
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
47
|
+
if args and args[0] == "watch" and not os.environ.get("WUP_SKIP_RUNTIME_PREFLIGHT"):
|
|
48
|
+
ok, detail = _watchdog_preflight()
|
|
49
|
+
if not ok:
|
|
50
|
+
print(
|
|
51
|
+
"WUP cannot start the file watcher because this Python environment "
|
|
52
|
+
"failed its ctypes/watchdog ABI preflight. Recreate the virtualenv "
|
|
53
|
+
f"with one interpreter ({sys.executable}). Details: {detail}",
|
|
54
|
+
file=sys.stderr,
|
|
55
|
+
)
|
|
56
|
+
return 70
|
|
57
|
+
|
|
58
|
+
from .cli import app
|
|
59
|
+
|
|
60
|
+
app(args=args, prog_name="wup")
|
|
61
|
+
return None
|