wup 0.2.49__tar.gz → 0.2.62__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.49/wup.egg-info → wup-0.2.62}/PKG-INFO +7 -7
- {wup-0.2.49 → wup-0.2.62}/README.md +6 -6
- {wup-0.2.49 → wup-0.2.62}/pyproject.toml +1 -1
- wup-0.2.62/tests/test_assistant.py +180 -0
- wup-0.2.62/tests/test_health_summary_passed.py +53 -0
- wup-0.2.62/tests/test_probe_mutex.py +38 -0
- {wup-0.2.49 → wup-0.2.62}/tests/test_visual_diff_progress.py +17 -0
- wup-0.2.62/tests/test_watch_exclude.py +34 -0
- {wup-0.2.49 → wup-0.2.62}/wup/__init__.py +1 -1
- {wup-0.2.49 → wup-0.2.62}/wup/assistant.py +16 -116
- wup-0.2.62/wup/assistant_discovery.py +99 -0
- wup-0.2.62/wup/assistant_validator.py +57 -0
- {wup-0.2.49 → wup-0.2.62}/wup/cli.py +249 -169
- {wup-0.2.49 → wup-0.2.62}/wup/config.py +82 -30
- {wup-0.2.49 → wup-0.2.62}/wup/core.py +16 -4
- {wup-0.2.49 → wup-0.2.62}/wup/models/config.py +19 -0
- {wup-0.2.49 → wup-0.2.62}/wup/monitoring_manifest.py +81 -18
- {wup-0.2.49 → wup-0.2.62}/wup/testql_watcher.py +63 -14
- {wup-0.2.49 → wup-0.2.62}/wup/visual_diff.py +50 -25
- {wup-0.2.49 → wup-0.2.62/wup.egg-info}/PKG-INFO +7 -7
- {wup-0.2.49 → wup-0.2.62}/wup.egg-info/SOURCES.txt +6 -0
- {wup-0.2.49 → wup-0.2.62}/LICENSE +0 -0
- {wup-0.2.49 → wup-0.2.62}/setup.cfg +0 -0
- {wup-0.2.49 → wup-0.2.62}/tests/test_auto_detection.py +0 -0
- {wup-0.2.49 → wup-0.2.62}/tests/test_cli_filtering.py +0 -0
- {wup-0.2.49 → wup-0.2.62}/tests/test_e2e.py +0 -0
- {wup-0.2.49 → wup-0.2.62}/tests/test_monitoring_manifest.py +0 -0
- {wup-0.2.49 → wup-0.2.62}/tests/test_service_inference.py +0 -0
- {wup-0.2.49 → wup-0.2.62}/tests/test_testql_monitor.py +0 -0
- {wup-0.2.49 → wup-0.2.62}/tests/test_testql_watcher.py +0 -0
- {wup-0.2.49 → wup-0.2.62}/tests/test_visual_diff_periodic_skip.py +0 -0
- {wup-0.2.49 → wup-0.2.62}/tests/test_web_client.py +0 -0
- {wup-0.2.49 → wup-0.2.62}/tests/test_wup.py +0 -0
- {wup-0.2.49 → wup-0.2.62}/wup/_ast_detector.py +0 -0
- {wup-0.2.49 → wup-0.2.62}/wup/_base_detector.py +0 -0
- {wup-0.2.49 → wup-0.2.62}/wup/_hash_detector.py +0 -0
- {wup-0.2.49 → wup-0.2.62}/wup/_yaml_detector.py +0 -0
- {wup-0.2.49 → wup-0.2.62}/wup/anomaly_detector.py +0 -0
- {wup-0.2.49 → wup-0.2.62}/wup/anomaly_models.py +0 -0
- {wup-0.2.49 → wup-0.2.62}/wup/bus.py +0 -0
- {wup-0.2.49 → wup-0.2.62}/wup/cli_config_generator.py +0 -0
- {wup-0.2.49 → wup-0.2.62}/wup/cli_scanner.py +0 -0
- {wup-0.2.49 → wup-0.2.62}/wup/dependency_mapper.py +0 -0
- {wup-0.2.49 → wup-0.2.62}/wup/event_store.py +0 -0
- {wup-0.2.49 → wup-0.2.62}/wup/file_watcher/events/file_events.py +0 -0
- {wup-0.2.49 → wup-0.2.62}/wup/models/__init__.py +0 -0
- {wup-0.2.49 → wup-0.2.62}/wup/planfile_reporter.py +0 -0
- {wup-0.2.49 → wup-0.2.62}/wup/testing/events/health_events.py +0 -0
- {wup-0.2.49 → wup-0.2.62}/wup/testing/events/test_results.py +0 -0
- {wup-0.2.49 → wup-0.2.62}/wup/testing/handlers/event_handlers.py +0 -0
- {wup-0.2.49 → wup-0.2.62}/wup/testing/handlers/health_handlers.py +0 -0
- {wup-0.2.49 → wup-0.2.62}/wup/testing/queries/health_queries.py +0 -0
- {wup-0.2.49 → wup-0.2.62}/wup/testql_cli_generator.py +0 -0
- {wup-0.2.49 → wup-0.2.62}/wup/testql_discovery.py +0 -0
- {wup-0.2.49 → wup-0.2.62}/wup/testql_monitor.py +0 -0
- {wup-0.2.49 → wup-0.2.62}/wup/web_client.py +0 -0
- {wup-0.2.49 → wup-0.2.62}/wup.egg-info/dependency_links.txt +0 -0
- {wup-0.2.49 → wup-0.2.62}/wup.egg-info/entry_points.txt +0 -0
- {wup-0.2.49 → wup-0.2.62}/wup.egg-info/requires.txt +0 -0
- {wup-0.2.49 → wup-0.2.62}/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.62
|
|
4
4
|
Summary: WUP (What's Up) - Intelligent file watcher for regression testing in large projects
|
|
5
5
|
Author-email: Tom Sapletta <tom@sapletta.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -31,17 +31,17 @@ Dynamic: license-file
|
|
|
31
31
|
|
|
32
32
|
## AI Cost Tracking
|
|
33
33
|
|
|
34
|
-
    
|
|
35
|
+
  
|
|
36
36
|
|
|
37
|
-
- 🤖 **LLM usage:** $3.
|
|
38
|
-
- 👤 **Human dev:** ~$
|
|
37
|
+
- 🤖 **LLM usage:** $3.4952 (75 commits)
|
|
38
|
+
- 👤 **Human dev:** ~$3168 (31.7h @ $100/h, 30min dedup)
|
|
39
39
|
|
|
40
|
-
Generated on 2026-05-
|
|
40
|
+
Generated on 2026-05-27 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
41
41
|
|
|
42
42
|
---
|
|
43
43
|
|
|
44
|
-
    
|
|
45
45
|
|
|
46
46
|
**WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
|
|
47
47
|
|
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
|
|
4
4
|
## AI Cost Tracking
|
|
5
5
|
|
|
6
|
-
    
|
|
7
|
+
  
|
|
8
8
|
|
|
9
|
-
- 🤖 **LLM usage:** $3.
|
|
10
|
-
- 👤 **Human dev:** ~$
|
|
9
|
+
- 🤖 **LLM usage:** $3.4952 (75 commits)
|
|
10
|
+
- 👤 **Human dev:** ~$3168 (31.7h @ $100/h, 30min dedup)
|
|
11
11
|
|
|
12
|
-
Generated on 2026-05-
|
|
12
|
+
Generated on 2026-05-27 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,180 @@
|
|
|
1
|
+
"""Unit tests for WupAssistant."""
|
|
2
|
+
import tempfile
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from wup.assistant import WupAssistant
|
|
7
|
+
from wup.models.config import WupConfig, ProjectConfig, ServiceConfig
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_framework_detection_fastapi():
|
|
11
|
+
"""Test auto-detecting FastAPI project framework."""
|
|
12
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
13
|
+
root = Path(tmpdir)
|
|
14
|
+
|
|
15
|
+
# Create FastAPI characteristic files and content
|
|
16
|
+
main_py = root / "main.py"
|
|
17
|
+
main_py.write_text("from fastapi import FastAPI\napp = FastAPI()", encoding="utf-8")
|
|
18
|
+
|
|
19
|
+
assistant = WupAssistant(str(root))
|
|
20
|
+
detected = assistant._detect_framework()
|
|
21
|
+
assert detected == "fastapi"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_framework_detection_flask():
|
|
25
|
+
"""Test auto-detecting Flask project framework."""
|
|
26
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
27
|
+
root = Path(tmpdir)
|
|
28
|
+
|
|
29
|
+
# Create Flask characteristic files and content
|
|
30
|
+
app_py = root / "app.py"
|
|
31
|
+
app_py.write_text("from flask import Flask\napp = Flask(__name__)", encoding="utf-8")
|
|
32
|
+
|
|
33
|
+
assistant = WupAssistant(str(root))
|
|
34
|
+
detected = assistant._detect_framework()
|
|
35
|
+
assert detected == "flask"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_framework_detection_none():
|
|
39
|
+
"""Test framework detection returns None when no markers match."""
|
|
40
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
41
|
+
root = Path(tmpdir)
|
|
42
|
+
assistant = WupAssistant(str(root))
|
|
43
|
+
detected = assistant._detect_framework()
|
|
44
|
+
assert detected is None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_auto_detect_services_fastapi():
|
|
48
|
+
"""Test auto-detecting services for a FastAPI project."""
|
|
49
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
50
|
+
root = Path(tmpdir)
|
|
51
|
+
|
|
52
|
+
# Create a router folder under app
|
|
53
|
+
router_dir = root / "app" / "routers"
|
|
54
|
+
router_dir.mkdir(parents=True)
|
|
55
|
+
(router_dir / "users.py").write_text("# user routes", encoding="utf-8")
|
|
56
|
+
|
|
57
|
+
assistant = WupAssistant(str(root))
|
|
58
|
+
services = assistant._auto_detect_services("fastapi")
|
|
59
|
+
|
|
60
|
+
assert len(services) == 1
|
|
61
|
+
assert services[0].name == "users"
|
|
62
|
+
assert services[0].type == "auto"
|
|
63
|
+
assert services[0].paths[0].endswith("app/routers/users.py")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_detect_service_type():
|
|
67
|
+
"""Test service type detection based on name and path."""
|
|
68
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
69
|
+
root = Path(tmpdir)
|
|
70
|
+
assistant = WupAssistant(str(root))
|
|
71
|
+
|
|
72
|
+
# Web service detection
|
|
73
|
+
assert assistant._detect_service_type("my-api", root) == "web"
|
|
74
|
+
assert assistant._detect_service_type("http-server", root) == "web"
|
|
75
|
+
|
|
76
|
+
# Shell service detection
|
|
77
|
+
assert assistant._detect_service_type("cli-tool", root) == "shell"
|
|
78
|
+
assert assistant._detect_service_type("run-command", root) == "shell"
|
|
79
|
+
|
|
80
|
+
# Default fallback
|
|
81
|
+
assert assistant._detect_service_type("unknown", root) == "auto"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_validate_config_success():
|
|
85
|
+
"""Test config validation passes on a valid config."""
|
|
86
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
87
|
+
root = Path(tmpdir)
|
|
88
|
+
|
|
89
|
+
# Create a watch path to satisfy validator
|
|
90
|
+
src_dir = root / "src"
|
|
91
|
+
src_dir.mkdir()
|
|
92
|
+
|
|
93
|
+
# Create scenario dir to satisfy validator
|
|
94
|
+
scenario_dir = root / "scenarios"
|
|
95
|
+
scenario_dir.mkdir()
|
|
96
|
+
|
|
97
|
+
assistant = WupAssistant(str(root))
|
|
98
|
+
assistant.config = WupConfig(
|
|
99
|
+
project=ProjectConfig(
|
|
100
|
+
name="MyProj",
|
|
101
|
+
description="Valid project"
|
|
102
|
+
),
|
|
103
|
+
services=[
|
|
104
|
+
ServiceConfig(name="web-service", type="web", paths=["src"])
|
|
105
|
+
]
|
|
106
|
+
)
|
|
107
|
+
assistant.config.watch.paths = ["src/**"]
|
|
108
|
+
assistant.config.testql.scenario_dir = "scenarios"
|
|
109
|
+
|
|
110
|
+
issues = assistant._validate_config()
|
|
111
|
+
assert len(issues) == 0
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_validate_config_issues():
|
|
115
|
+
"""Test config validation detects common errors."""
|
|
116
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
117
|
+
root = Path(tmpdir)
|
|
118
|
+
assistant = WupAssistant(str(root))
|
|
119
|
+
|
|
120
|
+
# 1. Empty/Missing project name
|
|
121
|
+
assistant.config.project.name = ""
|
|
122
|
+
# 2. No services configured
|
|
123
|
+
assistant.config.services = []
|
|
124
|
+
# 3. Non-existent watch path
|
|
125
|
+
assistant.config.watch.paths = ["non_existent_dir/**"]
|
|
126
|
+
# 4. Non-existent scenario directory
|
|
127
|
+
assistant.config.testql.scenario_dir = "missing_scenarios"
|
|
128
|
+
|
|
129
|
+
issues = assistant._validate_config()
|
|
130
|
+
assert len(issues) == 4
|
|
131
|
+
assert "Project name is required" in issues
|
|
132
|
+
assert "No services configured" in issues
|
|
133
|
+
assert "Watch path does not exist" in issues[2]
|
|
134
|
+
assert "TestQL scenario directory not found" in issues[3]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_generate_suggestions():
|
|
138
|
+
"""Test assistant suggestions generation."""
|
|
139
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
140
|
+
root = Path(tmpdir)
|
|
141
|
+
assistant = WupAssistant(str(root))
|
|
142
|
+
|
|
143
|
+
# Empty watch file types, single service, web dashboard disabled
|
|
144
|
+
assistant.config.services = [ServiceConfig(name="single", type="web")]
|
|
145
|
+
assistant.config.watch.file_types = []
|
|
146
|
+
assistant.config.web.enabled = False
|
|
147
|
+
assistant.config.testql.scenario_dir = "scenarios"
|
|
148
|
+
assistant.config.testql.smoke_scenario = ""
|
|
149
|
+
|
|
150
|
+
suggestions = assistant._generate_suggestions()
|
|
151
|
+
assert len(suggestions) == 4
|
|
152
|
+
assert "Consider splitting into multiple services" in suggestions[0]
|
|
153
|
+
assert "Specify file types to avoid watching" in suggestions[1]
|
|
154
|
+
assert "Enable web dashboard for real-time monitoring" in suggestions[2]
|
|
155
|
+
assert "Set a smoke test scenario for quick health checks" in suggestions[3]
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_quick_setup():
|
|
159
|
+
"""Test quick non-interactive setup."""
|
|
160
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
161
|
+
root = Path(tmpdir)
|
|
162
|
+
|
|
163
|
+
# Create FastAPI marker to let quick setup detect it
|
|
164
|
+
main_py = root / "main.py"
|
|
165
|
+
main_py.write_text("from fastapi import FastAPI", encoding="utf-8")
|
|
166
|
+
|
|
167
|
+
assistant = WupAssistant(str(root))
|
|
168
|
+
assistant.run(quick=True)
|
|
169
|
+
|
|
170
|
+
# Should save wup.yaml
|
|
171
|
+
config_path = root / "wup.yaml"
|
|
172
|
+
assert config_path.exists()
|
|
173
|
+
|
|
174
|
+
# Verify content of saved config
|
|
175
|
+
import yaml
|
|
176
|
+
saved_data = yaml.safe_load(config_path.read_text(encoding="utf-8"))
|
|
177
|
+
assert saved_data["project"]["name"] == root.name
|
|
178
|
+
assert "fastapi" in saved_data["project"]["description"].lower()
|
|
179
|
+
assert len(saved_data["services"]) > 0
|
|
180
|
+
assert saved_data["web"]["enabled"] is True
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Regression: fleet health treats all-pass summaries as success in non-strict mode."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import tempfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from subprocess import CompletedProcess
|
|
10
|
+
|
|
11
|
+
from wup.models.config import ProjectConfig, TestQLConfig, WatchConfig, WupConfig
|
|
12
|
+
from wup.testql_watcher import TestQLWatcher
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_health_summary_all_passed_parser() -> None:
|
|
16
|
+
assert TestQLWatcher._health_summary_all_passed("17/17 passed, 0 failed")
|
|
17
|
+
assert not TestQLWatcher._health_summary_all_passed("16/17 passed, 1 failed")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_fleet_health_nonzero_exit_all_passed_counts_as_up() -> None:
|
|
21
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
22
|
+
root = Path(tmpdir)
|
|
23
|
+
scenario_dir = root / "testql-scenarios"
|
|
24
|
+
scenario_dir.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
(scenario_dir / "fleet.testql.toon.yaml").write_text("name: fleet\n", encoding="utf-8")
|
|
26
|
+
(root / ".wup").mkdir(parents=True, exist_ok=True)
|
|
27
|
+
|
|
28
|
+
cfg = WupConfig(
|
|
29
|
+
project=ProjectConfig(name="demo"),
|
|
30
|
+
watch=WatchConfig(),
|
|
31
|
+
testql=TestQLConfig(
|
|
32
|
+
scenario_dir="testql-scenarios",
|
|
33
|
+
health_scenario="fleet.testql.toon.yaml",
|
|
34
|
+
health_scenario_strict=False,
|
|
35
|
+
),
|
|
36
|
+
)
|
|
37
|
+
watcher = TestQLWatcher(
|
|
38
|
+
project_root=str(root),
|
|
39
|
+
deps_file=str(root / "deps.json"),
|
|
40
|
+
scenarios_dir="testql-scenarios",
|
|
41
|
+
track_dir=".wup/tracks",
|
|
42
|
+
config=cfg,
|
|
43
|
+
)
|
|
44
|
+
watcher._run_testql = lambda args, timeout: CompletedProcess( # type: ignore[method-assign]
|
|
45
|
+
args=args,
|
|
46
|
+
returncode=1,
|
|
47
|
+
stdout='{"passed": 17, "failed": 0}',
|
|
48
|
+
stderr="",
|
|
49
|
+
)
|
|
50
|
+
assert asyncio.run(watcher._run_fleet_health_scenario()) is True
|
|
51
|
+
state = json.loads((root / ".wup" / "service-health.json").read_text(encoding="utf-8"))
|
|
52
|
+
assert state["demo"]["status"] == "up"
|
|
53
|
+
assert state["demo"]["stage"] == "health_scenario"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Regression: periodic probes do not overlap file-change quick/visual cycles."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import io
|
|
6
|
+
import threading
|
|
7
|
+
from unittest.mock import patch
|
|
8
|
+
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
from wup.models.config import ProjectConfig, ServiceConfig, WupConfig
|
|
12
|
+
from wup.testql_watcher import TestQLWatcher
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _minimal_watcher() -> TestQLWatcher:
|
|
16
|
+
watcher = TestQLWatcher.__new__(TestQLWatcher)
|
|
17
|
+
watcher.console = Console(file=io.StringIO(), width=120)
|
|
18
|
+
watcher.config = WupConfig(
|
|
19
|
+
project=ProjectConfig(name="t"),
|
|
20
|
+
services=[ServiceConfig(name="frontend", paths=["frontend"])],
|
|
21
|
+
)
|
|
22
|
+
watcher._watch_work_lock = threading.Lock()
|
|
23
|
+
watcher._periodic_probe_in_progress = False
|
|
24
|
+
return watcher
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_periodic_probe_skipped_when_watch_lock_held() -> None:
|
|
28
|
+
watcher = _minimal_watcher()
|
|
29
|
+
watcher._watch_work_lock.acquire()
|
|
30
|
+
try:
|
|
31
|
+
with patch("asyncio.run"):
|
|
32
|
+
watcher._run_periodic_probes_once()
|
|
33
|
+
finally:
|
|
34
|
+
watcher._watch_work_lock.release()
|
|
35
|
+
|
|
36
|
+
output = watcher.console.file.getvalue()
|
|
37
|
+
assert "skipped" in output.lower()
|
|
38
|
+
assert watcher._periodic_probe_in_progress is False
|
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import io
|
|
5
6
|
import os
|
|
6
7
|
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
7
10
|
from wup.models.config import VisualDiffConfig
|
|
8
11
|
from wup.visual_diff import VisualDiffer
|
|
9
12
|
|
|
@@ -37,3 +40,17 @@ def test_progress_can_be_disabled_via_env(tmp_path, monkeypatch) -> None:
|
|
|
37
40
|
monkeypatch.setenv("WUP_VISUAL_DIFF_PROGRESS", "0")
|
|
38
41
|
differ = _make_differ(tmp_path)
|
|
39
42
|
assert differ._build_progress("frontend", total=50) is None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_progress_uses_injected_console(tmp_path) -> None:
|
|
46
|
+
custom = Console(file=io.StringIO(), width=80)
|
|
47
|
+
cfg = VisualDiffConfig(
|
|
48
|
+
enabled=True,
|
|
49
|
+
base_url="http://localhost:8100",
|
|
50
|
+
snapshot_dir=str(tmp_path / "snap"),
|
|
51
|
+
diff_dir=str(tmp_path / "diff"),
|
|
52
|
+
)
|
|
53
|
+
differ = VisualDiffer(str(tmp_path), cfg, console=custom)
|
|
54
|
+
progress = differ._build_progress("frontend", total=10)
|
|
55
|
+
assert progress is not None
|
|
56
|
+
assert progress.console is custom
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Regression: nested test directories are ignored by the file watcher."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from wup.core import WupWatcher
|
|
8
|
+
from wup.models.config import ProjectConfig, WatchConfig, WupConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _watcher() -> WupWatcher:
|
|
12
|
+
cfg = WupConfig(
|
|
13
|
+
project=ProjectConfig(name="t"),
|
|
14
|
+
watch=WatchConfig(
|
|
15
|
+
exclude_patterns=["**/tests/**", "*.md"],
|
|
16
|
+
file_types=[".ts", ".py"],
|
|
17
|
+
),
|
|
18
|
+
)
|
|
19
|
+
return WupWatcher(project_root=".", config=cfg)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_nested_tests_directory_ignored() -> None:
|
|
23
|
+
watcher = _watcher()
|
|
24
|
+
assert watcher._is_file_ignored(Path("frontend/src/tests/setup.ts"))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_src_file_not_ignored() -> None:
|
|
28
|
+
watcher = _watcher()
|
|
29
|
+
assert not watcher._is_file_ignored(Path("frontend/src/services/foo.ts"))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_glob_exclude_pattern() -> None:
|
|
33
|
+
watcher = _watcher()
|
|
34
|
+
assert watcher._path_matches_exclude_pattern(Path("docs/readme.md"), "*.md")
|
|
@@ -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.62"
|
|
11
11
|
__author__ = "Tom Sapletta"
|
|
12
12
|
|
|
13
13
|
from .config import load_config, save_config, get_default_config
|
|
@@ -39,6 +39,16 @@ from .models.config import (
|
|
|
39
39
|
WebConfig,
|
|
40
40
|
WupConfig,
|
|
41
41
|
)
|
|
42
|
+
from .assistant_discovery import (
|
|
43
|
+
FRAMEWORK_PATTERNS,
|
|
44
|
+
detect_framework,
|
|
45
|
+
auto_detect_services,
|
|
46
|
+
detect_service_type,
|
|
47
|
+
)
|
|
48
|
+
from .assistant_validator import (
|
|
49
|
+
validate_config,
|
|
50
|
+
generate_suggestions,
|
|
51
|
+
)
|
|
42
52
|
|
|
43
53
|
# Import ServiceType for type checking
|
|
44
54
|
if False: # TYPE_CHECKING
|
|
@@ -50,33 +60,7 @@ console = Console()
|
|
|
50
60
|
class WupAssistant:
|
|
51
61
|
"""Interactive configuration assistant."""
|
|
52
62
|
|
|
53
|
-
|
|
54
|
-
FRAMEWORK_PATTERNS = {
|
|
55
|
-
'fastapi': {
|
|
56
|
-
'files': ['main.py', 'app/main.py'],
|
|
57
|
-
'content': ['FastAPI', 'from fastapi', 'app = FastAPI'],
|
|
58
|
-
'services': ['app/routers/*', 'app/routes/*', 'routes/*'],
|
|
59
|
-
'default_services': ['web', 'api'],
|
|
60
|
-
},
|
|
61
|
-
'flask': {
|
|
62
|
-
'files': ['app.py', 'wsgi.py', 'application.py'],
|
|
63
|
-
'content': ['Flask', 'from flask', 'app = Flask'],
|
|
64
|
-
'services': ['app/*/__init__.py', 'blueprints/*'],
|
|
65
|
-
'default_services': ['web', 'admin'],
|
|
66
|
-
},
|
|
67
|
-
'django': {
|
|
68
|
-
'files': ['manage.py', 'settings.py'],
|
|
69
|
-
'content': ['Django', 'from django', 'INSTALLED_APPS'],
|
|
70
|
-
'services': ['*/apps.py', '*/models.py'],
|
|
71
|
-
'default_services': ['models', 'views', 'tasks'],
|
|
72
|
-
},
|
|
73
|
-
'express': {
|
|
74
|
-
'files': ['server.js', 'app.js'],
|
|
75
|
-
'content': ['express', 'require("express")', "require('express')"],
|
|
76
|
-
'services': ['routes/*', 'controllers/*'],
|
|
77
|
-
'default_services': ['api', 'web'],
|
|
78
|
-
},
|
|
79
|
-
}
|
|
63
|
+
FRAMEWORK_PATTERNS = FRAMEWORK_PATTERNS
|
|
80
64
|
|
|
81
65
|
def __init__(self, project_root: str = "."):
|
|
82
66
|
self.project_root = Path(project_root).resolve()
|
|
@@ -192,59 +176,15 @@ class WupAssistant:
|
|
|
192
176
|
|
|
193
177
|
def _detect_framework(self) -> Optional[str]:
|
|
194
178
|
"""Auto-detect project framework."""
|
|
195
|
-
|
|
196
|
-
# Check for characteristic files
|
|
197
|
-
for file in patterns['files']:
|
|
198
|
-
if (self.project_root / file).exists():
|
|
199
|
-
# Verify content
|
|
200
|
-
content = (self.project_root / file).read_text()
|
|
201
|
-
if any(marker in content for marker in patterns['content']):
|
|
202
|
-
return framework
|
|
203
|
-
return None
|
|
179
|
+
return detect_framework(self.project_root)
|
|
204
180
|
|
|
205
181
|
def _auto_detect_services(self, framework: str) -> List[ServiceConfig]:
|
|
206
182
|
"""Auto-detect services based on framework patterns."""
|
|
207
|
-
|
|
208
|
-
patterns = self.FRAMEWORK_PATTERNS.get(framework, {})
|
|
209
|
-
|
|
210
|
-
for pattern in patterns.get('services', []):
|
|
211
|
-
for path in self.project_root.rglob(pattern):
|
|
212
|
-
if path.is_dir() or path.is_file():
|
|
213
|
-
service_name = path.parent.name if path.name == '__init__.py' else path.stem
|
|
214
|
-
|
|
215
|
-
# Detect service type
|
|
216
|
-
svc_type = self._detect_service_type(service_name, path)
|
|
217
|
-
|
|
218
|
-
services.append(ServiceConfig(
|
|
219
|
-
name=service_name,
|
|
220
|
-
type=svc_type,
|
|
221
|
-
paths=[str(path.parent if path.name == '__init__.py' else path)],
|
|
222
|
-
))
|
|
223
|
-
|
|
224
|
-
return services
|
|
183
|
+
return auto_detect_services(self.project_root, framework)
|
|
225
184
|
|
|
226
185
|
def _detect_service_type(self, name: str, path: Path) -> ServiceType:
|
|
227
186
|
"""Detect service type from name and path."""
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
# Web indicators
|
|
231
|
-
if any(x in name_lower for x in ['web', 'api', 'http', 'rest', 'router', 'route']):
|
|
232
|
-
return 'web'
|
|
233
|
-
|
|
234
|
-
# Shell indicators
|
|
235
|
-
if any(x in name_lower for x in ['shell', 'cli', 'cmd', 'command']):
|
|
236
|
-
return 'shell'
|
|
237
|
-
|
|
238
|
-
# Check directory contents
|
|
239
|
-
if path.is_dir():
|
|
240
|
-
files = list(path.iterdir())
|
|
241
|
-
has_html = any(f.suffix in ['.html', '.htm'] for f in files)
|
|
242
|
-
has_routes = any('route' in f.name.lower() for f in files)
|
|
243
|
-
|
|
244
|
-
if has_html or has_routes:
|
|
245
|
-
return 'web'
|
|
246
|
-
|
|
247
|
-
return 'auto'
|
|
187
|
+
return detect_service_type(name, path)
|
|
248
188
|
|
|
249
189
|
def _configure_services(self):
|
|
250
190
|
"""Interactive service configuration."""
|
|
@@ -549,51 +489,11 @@ class WupAssistant:
|
|
|
549
489
|
|
|
550
490
|
def _validate_config(self) -> List[str]:
|
|
551
491
|
"""Validate current configuration."""
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
# Check project name
|
|
555
|
-
if not self.config.project.name:
|
|
556
|
-
issues.append("Project name is required")
|
|
557
|
-
|
|
558
|
-
# Check services
|
|
559
|
-
if not self.config.services:
|
|
560
|
-
issues.append("No services configured")
|
|
561
|
-
|
|
562
|
-
for svc in self.config.services:
|
|
563
|
-
if not svc.name:
|
|
564
|
-
issues.append("Service with empty name found")
|
|
565
|
-
|
|
566
|
-
# Check watch paths exist
|
|
567
|
-
for path in self.config.watch.paths:
|
|
568
|
-
resolved = self.project_root / path.replace('/**', '').replace('/*', '')
|
|
569
|
-
if not resolved.exists():
|
|
570
|
-
issues.append(f"Watch path does not exist: {path}")
|
|
571
|
-
|
|
572
|
-
# Check TestQL
|
|
573
|
-
if self.config.testql.scenario_dir:
|
|
574
|
-
scenario_path = self.project_root / self.config.testql.scenario_dir
|
|
575
|
-
if not scenario_path.exists():
|
|
576
|
-
issues.append(f"TestQL scenario directory not found: {self.config.testql.scenario_dir}")
|
|
577
|
-
|
|
578
|
-
return issues
|
|
492
|
+
return validate_config(self.config, self.project_root)
|
|
579
493
|
|
|
580
494
|
def _generate_suggestions(self) -> List[str]:
|
|
581
495
|
"""Generate helpful suggestions."""
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
if len(self.config.services) == 1:
|
|
585
|
-
suggestions.append("Consider splitting into multiple services for better granularity")
|
|
586
|
-
|
|
587
|
-
if not self.config.watch.file_types:
|
|
588
|
-
suggestions.append("Specify file types to avoid watching unnecessary files")
|
|
589
|
-
|
|
590
|
-
if not self.config.web.enabled:
|
|
591
|
-
suggestions.append("Enable web dashboard for real-time monitoring and notifications")
|
|
592
|
-
|
|
593
|
-
if self.config.testql.scenario_dir and not self.config.testql.smoke_scenario:
|
|
594
|
-
suggestions.append("Set a smoke test scenario for quick health checks")
|
|
595
|
-
|
|
596
|
-
return suggestions
|
|
496
|
+
return generate_suggestions(self.config)
|
|
597
497
|
|
|
598
498
|
def _save_configuration(self):
|
|
599
499
|
"""Save configuration to wup.yaml."""
|