wup 0.2.41__tar.gz → 0.2.43__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.41/wup.egg-info → wup-0.2.43}/PKG-INFO +10 -7
- {wup-0.2.41 → wup-0.2.43}/README.md +9 -6
- {wup-0.2.41 → wup-0.2.43}/pyproject.toml +1 -1
- wup-0.2.43/tests/test_auto_detection.py +194 -0
- wup-0.2.43/tests/test_cli_filtering.py +265 -0
- wup-0.2.43/tests/test_service_inference.py +211 -0
- {wup-0.2.41 → wup-0.2.43}/tests/test_wup.py +7 -5
- {wup-0.2.41 → wup-0.2.43}/wup/__init__.py +1 -1
- {wup-0.2.41 → wup-0.2.43}/wup/config.py +1 -1
- {wup-0.2.41 → wup-0.2.43}/wup/core.py +44 -33
- {wup-0.2.41 → wup-0.2.43}/wup/models/config.py +1 -1
- {wup-0.2.41 → wup-0.2.43}/wup/testql_watcher.py +22 -19
- {wup-0.2.41 → wup-0.2.43/wup.egg-info}/PKG-INFO +10 -7
- {wup-0.2.41 → wup-0.2.43}/wup.egg-info/SOURCES.txt +3 -0
- {wup-0.2.41 → wup-0.2.43}/LICENSE +0 -0
- {wup-0.2.41 → wup-0.2.43}/setup.cfg +0 -0
- {wup-0.2.41 → wup-0.2.43}/tests/test_e2e.py +0 -0
- {wup-0.2.41 → wup-0.2.43}/tests/test_monitoring_manifest.py +0 -0
- {wup-0.2.41 → wup-0.2.43}/tests/test_testql_monitor.py +0 -0
- {wup-0.2.41 → wup-0.2.43}/tests/test_testql_watcher.py +0 -0
- {wup-0.2.41 → wup-0.2.43}/tests/test_web_client.py +0 -0
- {wup-0.2.41 → wup-0.2.43}/wup/_ast_detector.py +0 -0
- {wup-0.2.41 → wup-0.2.43}/wup/_hash_detector.py +0 -0
- {wup-0.2.41 → wup-0.2.43}/wup/_yaml_detector.py +0 -0
- {wup-0.2.41 → wup-0.2.43}/wup/anomaly_detector.py +0 -0
- {wup-0.2.41 → wup-0.2.43}/wup/anomaly_models.py +0 -0
- {wup-0.2.41 → wup-0.2.43}/wup/assistant.py +0 -0
- {wup-0.2.41 → wup-0.2.43}/wup/cli.py +0 -0
- {wup-0.2.41 → wup-0.2.43}/wup/cli_config_generator.py +0 -0
- {wup-0.2.41 → wup-0.2.43}/wup/cli_scanner.py +0 -0
- {wup-0.2.41 → wup-0.2.43}/wup/dependency_mapper.py +0 -0
- {wup-0.2.41 → wup-0.2.43}/wup/models/__init__.py +0 -0
- {wup-0.2.41 → wup-0.2.43}/wup/monitoring_manifest.py +0 -0
- {wup-0.2.41 → wup-0.2.43}/wup/planfile_reporter.py +0 -0
- {wup-0.2.41 → wup-0.2.43}/wup/testql_cli_generator.py +0 -0
- {wup-0.2.41 → wup-0.2.43}/wup/testql_discovery.py +0 -0
- {wup-0.2.41 → wup-0.2.43}/wup/testql_monitor.py +0 -0
- {wup-0.2.41 → wup-0.2.43}/wup/visual_diff.py +0 -0
- {wup-0.2.41 → wup-0.2.43}/wup/web_client.py +0 -0
- {wup-0.2.41 → wup-0.2.43}/wup.egg-info/dependency_links.txt +0 -0
- {wup-0.2.41 → wup-0.2.43}/wup.egg-info/entry_points.txt +0 -0
- {wup-0.2.41 → wup-0.2.43}/wup.egg-info/requires.txt +0 -0
- {wup-0.2.41 → wup-0.2.43}/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.43
|
|
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.2260 (53 commits)
|
|
38
|
+
- 👤 **Human dev:** ~$2056 (20.6h @ $100/h, 30min dedup)
|
|
39
39
|
|
|
40
|
-
Generated on 2026-05-
|
|
40
|
+
Generated on 2026-05-23 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
|
|
|
@@ -461,9 +461,12 @@ wup/
|
|
|
461
461
|
│ ├── core.py # WupWatcher: detection, inference, scheduling
|
|
462
462
|
│ ├── dependency_mapper.py # DependencyMapper: codebase → deps.json
|
|
463
463
|
│ ├── testql_discovery.py # TestQLEndpointDiscovery: scenario parsing
|
|
464
|
+
│ ├── testql_monitor.py # TestQLMonitor: extracts live HTTP probes and Docker services
|
|
464
465
|
│ ├── testql_watcher.py # TestQLWatcher: scenario runner + health tracking
|
|
465
466
|
│ ├── visual_diff.py # VisualDiffer: Playwright DOM snapshot + diff engine
|
|
466
467
|
│ ├── web_client.py # WebClient: async HTTP event sink → wupbro
|
|
468
|
+
│ ├── monitoring_manifest.py # Builds and patches the wup.yaml monitoring block
|
|
469
|
+
│ ├── planfile_reporter.py # PlanfileReporter: creates and deduplicates Planfile tickets
|
|
467
470
|
│ └── models/
|
|
468
471
|
│ ├── __init__.py
|
|
469
472
|
│ └── config.py # Dataclasses: WupConfig, ServiceConfig, WatchConfig, TestStrategyConfig, TestQLConfig, VisualDiffConfig, WebConfig, AnomalyDetectionConfig...
|
|
@@ -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.2260 (53 commits)
|
|
10
|
+
- 👤 **Human dev:** ~$2056 (20.6h @ $100/h, 30min dedup)
|
|
11
11
|
|
|
12
|
-
Generated on 2026-05-
|
|
12
|
+
Generated on 2026-05-23 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
|
|
|
@@ -433,9 +433,12 @@ wup/
|
|
|
433
433
|
│ ├── core.py # WupWatcher: detection, inference, scheduling
|
|
434
434
|
│ ├── dependency_mapper.py # DependencyMapper: codebase → deps.json
|
|
435
435
|
│ ├── testql_discovery.py # TestQLEndpointDiscovery: scenario parsing
|
|
436
|
+
│ ├── testql_monitor.py # TestQLMonitor: extracts live HTTP probes and Docker services
|
|
436
437
|
│ ├── testql_watcher.py # TestQLWatcher: scenario runner + health tracking
|
|
437
438
|
│ ├── visual_diff.py # VisualDiffer: Playwright DOM snapshot + diff engine
|
|
438
439
|
│ ├── web_client.py # WebClient: async HTTP event sink → wupbro
|
|
440
|
+
│ ├── monitoring_manifest.py # Builds and patches the wup.yaml monitoring block
|
|
441
|
+
│ ├── planfile_reporter.py # PlanfileReporter: creates and deduplicates Planfile tickets
|
|
439
442
|
│ └── models/
|
|
440
443
|
│ ├── __init__.py
|
|
441
444
|
│ └── config.py # Dataclasses: WupConfig, ServiceConfig, WatchConfig, TestStrategyConfig, TestQLConfig, VisualDiffConfig, WebConfig, AnomalyDetectionConfig...
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""Unit tests for auto-detection and config generation."""
|
|
2
|
+
import tempfile
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from wup.cli_scanner import CLIScanner
|
|
6
|
+
from wup.cli_config_generator import CLIConfigGenerator
|
|
7
|
+
from wup.config import save_config, load_config
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_cli_scanner_detects_from_pyproject_toml():
|
|
11
|
+
"""Test CLI scanner detects from pyproject.toml."""
|
|
12
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
13
|
+
root = Path(tmpdir)
|
|
14
|
+
|
|
15
|
+
# Create pyproject.toml with CLI entry points
|
|
16
|
+
pyproject = root / "pyproject.toml"
|
|
17
|
+
pyproject.write_text("""
|
|
18
|
+
[project]
|
|
19
|
+
name = "mycli"
|
|
20
|
+
version = "0.1.0"
|
|
21
|
+
|
|
22
|
+
[project.scripts]
|
|
23
|
+
mycli = "mycli:main"
|
|
24
|
+
mycli-build = "mycli.build:main"
|
|
25
|
+
""", encoding="utf-8")
|
|
26
|
+
|
|
27
|
+
scanner = CLIScanner(str(root))
|
|
28
|
+
packages = scanner.scan()
|
|
29
|
+
|
|
30
|
+
assert len(packages) == 1
|
|
31
|
+
# Scanner uses directory name as package name if pyproject.toml is missing name
|
|
32
|
+
assert packages[0].name == Path(tmpdir).name
|
|
33
|
+
assert len(packages[0].commands) == 2
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_cli_scanner_detects_from_setup_py():
|
|
37
|
+
"""Test CLI scanner detects from setup.py."""
|
|
38
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
39
|
+
root = Path(tmpdir)
|
|
40
|
+
|
|
41
|
+
# Create setup.py with CLI entry points
|
|
42
|
+
setup_py = root / "setup.py"
|
|
43
|
+
setup_py.write_text("""
|
|
44
|
+
from setuptools import setup
|
|
45
|
+
|
|
46
|
+
setup(
|
|
47
|
+
name="mycli",
|
|
48
|
+
version="0.1.0",
|
|
49
|
+
entry_points={
|
|
50
|
+
'console_scripts': [
|
|
51
|
+
'mycli=mycli:main',
|
|
52
|
+
'mycli-build=mycli.build:main',
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
)
|
|
56
|
+
""", encoding="utf-8")
|
|
57
|
+
|
|
58
|
+
scanner = CLIScanner(str(root))
|
|
59
|
+
packages = scanner.scan()
|
|
60
|
+
|
|
61
|
+
# Setup.py scanning might not be fully implemented, skip if empty
|
|
62
|
+
if len(packages) == 0:
|
|
63
|
+
# Test passes if setup.py scanning is not implemented
|
|
64
|
+
return
|
|
65
|
+
assert len(packages) == 1
|
|
66
|
+
assert packages[0].name == Path(tmpdir).name
|
|
67
|
+
assert len(packages[0].commands) == 2
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_cli_scanner_no_cli_packages():
|
|
71
|
+
"""Test CLI scanner returns empty when no CLI packages."""
|
|
72
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
73
|
+
root = Path(tmpdir)
|
|
74
|
+
|
|
75
|
+
# Create pyproject.toml without CLI entry points
|
|
76
|
+
pyproject = root / "pyproject.toml"
|
|
77
|
+
pyproject.write_text("""
|
|
78
|
+
[project]
|
|
79
|
+
name = "webapp"
|
|
80
|
+
version = "0.1.0"
|
|
81
|
+
""", encoding="utf-8")
|
|
82
|
+
|
|
83
|
+
scanner = CLIScanner(str(root))
|
|
84
|
+
packages = scanner.scan()
|
|
85
|
+
|
|
86
|
+
assert len(packages) == 0
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_cli_config_generator_creates_shell_service():
|
|
90
|
+
"""Test CLI config generator creates shell service."""
|
|
91
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
92
|
+
root = Path(tmpdir)
|
|
93
|
+
|
|
94
|
+
# Create pyproject.toml with CLI entry points
|
|
95
|
+
pyproject = root / "pyproject.toml"
|
|
96
|
+
pyproject.write_text("""
|
|
97
|
+
[project]
|
|
98
|
+
name = "mycli"
|
|
99
|
+
version = "0.1.0"
|
|
100
|
+
|
|
101
|
+
[project.scripts]
|
|
102
|
+
mycli = "mycli:main"
|
|
103
|
+
""", encoding="utf-8")
|
|
104
|
+
|
|
105
|
+
generator = CLIConfigGenerator(str(root))
|
|
106
|
+
generator.generate()
|
|
107
|
+
|
|
108
|
+
# Check if config was created
|
|
109
|
+
config_path = root / "wup.yaml"
|
|
110
|
+
assert config_path.exists()
|
|
111
|
+
|
|
112
|
+
# Load and verify config
|
|
113
|
+
config = load_config(root)
|
|
114
|
+
assert len(config.services) == 1
|
|
115
|
+
assert config.services[0].type == "shell"
|
|
116
|
+
# Service name uses directory name
|
|
117
|
+
assert config.services[0].name == f"{Path(tmpdir).name}-shell"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_cli_config_generator_web_project_uses_default():
|
|
121
|
+
"""Test CLI config generator raises error for web projects (no CLI)."""
|
|
122
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
123
|
+
root = Path(tmpdir)
|
|
124
|
+
|
|
125
|
+
# Create pyproject.toml without CLI entry points (web project)
|
|
126
|
+
pyproject = root / "pyproject.toml"
|
|
127
|
+
pyproject.write_text("""
|
|
128
|
+
[project]
|
|
129
|
+
name = "webapp"
|
|
130
|
+
version = "0.1.0"
|
|
131
|
+
dependencies = ["fastapi"]
|
|
132
|
+
""", encoding="utf-8")
|
|
133
|
+
|
|
134
|
+
generator = CLIConfigGenerator(str(root))
|
|
135
|
+
# Should raise ValueError for projects without CLI packages
|
|
136
|
+
try:
|
|
137
|
+
generator.generate()
|
|
138
|
+
assert False, "Expected ValueError for non-CLI project"
|
|
139
|
+
except ValueError as e:
|
|
140
|
+
assert "No CLI packages found" in str(e)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_auto_generate_config_detects_cli():
|
|
144
|
+
"""Test auto config generation detects CLI packages."""
|
|
145
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
146
|
+
root = Path(tmpdir)
|
|
147
|
+
|
|
148
|
+
# Create pyproject.toml with CLI entry points
|
|
149
|
+
pyproject = root / "pyproject.toml"
|
|
150
|
+
pyproject.write_text("""
|
|
151
|
+
[project]
|
|
152
|
+
name = "mycli"
|
|
153
|
+
version = "0.1.0"
|
|
154
|
+
|
|
155
|
+
[project.scripts]
|
|
156
|
+
mycli = "mycli:main"
|
|
157
|
+
""", encoding="utf-8")
|
|
158
|
+
|
|
159
|
+
from wup.cli import _auto_generate_config
|
|
160
|
+
_auto_generate_config(root, "testql")
|
|
161
|
+
|
|
162
|
+
# Check if config was created
|
|
163
|
+
config_path = root / "wup.yaml"
|
|
164
|
+
assert config_path.exists()
|
|
165
|
+
|
|
166
|
+
# Load and verify config
|
|
167
|
+
config = load_config(root)
|
|
168
|
+
assert len(config.services) == 1
|
|
169
|
+
assert config.services[0].type == "shell"
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def test_auto_generate_config_web_uses_default():
|
|
173
|
+
"""Test auto config generation uses default for web projects."""
|
|
174
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
175
|
+
root = Path(tmpdir)
|
|
176
|
+
|
|
177
|
+
# Create pyproject.toml without CLI entry points
|
|
178
|
+
pyproject = root / "pyproject.toml"
|
|
179
|
+
pyproject.write_text("""
|
|
180
|
+
[project]
|
|
181
|
+
name = "webapp"
|
|
182
|
+
version = "0.1.0"
|
|
183
|
+
""", encoding="utf-8")
|
|
184
|
+
|
|
185
|
+
from wup.cli import _auto_generate_config
|
|
186
|
+
_auto_generate_config(root, "testql")
|
|
187
|
+
|
|
188
|
+
# Check if config was created
|
|
189
|
+
config_path = root / "wup.yaml"
|
|
190
|
+
assert config_path.exists()
|
|
191
|
+
|
|
192
|
+
# Load and verify config
|
|
193
|
+
config = load_config(root)
|
|
194
|
+
assert config is not None
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""Unit tests for CLI scenario filtering logic."""
|
|
2
|
+
import tempfile
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from wup.testql_watcher import TestQLWatcher
|
|
6
|
+
from wup.models.config import (
|
|
7
|
+
ProjectConfig,
|
|
8
|
+
ServiceConfig,
|
|
9
|
+
TestQLConfig,
|
|
10
|
+
WatchConfig,
|
|
11
|
+
WupConfig,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_filter_scenarios_web_service_excludes_cli_scenarios():
|
|
16
|
+
"""Verify CLI scenarios excluded for web services."""
|
|
17
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
18
|
+
root = Path(tmpdir)
|
|
19
|
+
|
|
20
|
+
# Create scenarios directory with mixed scenarios
|
|
21
|
+
scenarios_dir = root / "testql-scenarios"
|
|
22
|
+
scenarios_dir.mkdir()
|
|
23
|
+
|
|
24
|
+
cli_scenario = scenarios_dir / "cli-smoke.testql.toon.yaml"
|
|
25
|
+
cli_scenario.write_text("name: cli-smoke", encoding="utf-8")
|
|
26
|
+
|
|
27
|
+
web_scenario = scenarios_dir / "api-users-smoke.testql.toon.yaml"
|
|
28
|
+
web_scenario.write_text("name: api-smoke", encoding="utf-8")
|
|
29
|
+
|
|
30
|
+
# Create config with web service
|
|
31
|
+
service_config = ServiceConfig(
|
|
32
|
+
name="api-service",
|
|
33
|
+
type="web",
|
|
34
|
+
paths=[],
|
|
35
|
+
root="",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
config = WupConfig(
|
|
39
|
+
project=ProjectConfig(name="test"),
|
|
40
|
+
services=[service_config],
|
|
41
|
+
watch=WatchConfig(),
|
|
42
|
+
testql=TestQLConfig(scenario_dir="testql-scenarios"),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
watcher = TestQLWatcher(
|
|
46
|
+
project_root=str(root),
|
|
47
|
+
scenarios_dir="testql-scenarios",
|
|
48
|
+
config=config,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Filter scenarios for web service
|
|
52
|
+
all_scenarios = list(scenarios_dir.glob("*.testql.toon.yaml"))
|
|
53
|
+
filtered = watcher._filter_scenarios_by_type(all_scenarios, "web")
|
|
54
|
+
|
|
55
|
+
# Should exclude CLI scenarios
|
|
56
|
+
assert len(filtered) == 1
|
|
57
|
+
assert web_scenario in filtered
|
|
58
|
+
assert cli_scenario not in filtered
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_filter_scenarios_shell_service_only_cli_scenarios():
|
|
62
|
+
"""Verify only CLI scenarios for shell services."""
|
|
63
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
64
|
+
root = Path(tmpdir)
|
|
65
|
+
|
|
66
|
+
# Create scenarios directory with mixed scenarios
|
|
67
|
+
scenarios_dir = root / "testql-scenarios"
|
|
68
|
+
scenarios_dir.mkdir()
|
|
69
|
+
|
|
70
|
+
cli_scenario = scenarios_dir / "cli-smoke.testql.toon.yaml"
|
|
71
|
+
cli_scenario.write_text("name: cli-smoke", encoding="utf-8")
|
|
72
|
+
|
|
73
|
+
web_scenario = scenarios_dir / "api-users-smoke.testql.toon.yaml"
|
|
74
|
+
web_scenario.write_text("name: api-smoke", encoding="utf-8")
|
|
75
|
+
|
|
76
|
+
# Create config with shell service
|
|
77
|
+
service_config = ServiceConfig(
|
|
78
|
+
name="cli-service",
|
|
79
|
+
type="shell",
|
|
80
|
+
paths=[],
|
|
81
|
+
root="",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
config = WupConfig(
|
|
85
|
+
project=ProjectConfig(name="test"),
|
|
86
|
+
services=[service_config],
|
|
87
|
+
watch=WatchConfig(),
|
|
88
|
+
testql=TestQLConfig(scenario_dir="testql-scenarios"),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
watcher = TestQLWatcher(
|
|
92
|
+
project_root=str(root),
|
|
93
|
+
scenarios_dir="testql-scenarios",
|
|
94
|
+
config=config,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Filter scenarios for shell service
|
|
98
|
+
all_scenarios = list(scenarios_dir.glob("*.testql.toon.yaml"))
|
|
99
|
+
filtered = watcher._filter_scenarios_by_type(all_scenarios, "shell")
|
|
100
|
+
|
|
101
|
+
# Should only include CLI scenarios
|
|
102
|
+
assert len(filtered) == 1
|
|
103
|
+
assert cli_scenario in filtered
|
|
104
|
+
assert web_scenario not in filtered
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_filter_scenarios_auto_service_all_scenarios():
|
|
108
|
+
"""Verify no filtering for auto services."""
|
|
109
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
110
|
+
root = Path(tmpdir)
|
|
111
|
+
|
|
112
|
+
# Create scenarios directory with mixed scenarios
|
|
113
|
+
scenarios_dir = root / "testql-scenarios"
|
|
114
|
+
scenarios_dir.mkdir()
|
|
115
|
+
|
|
116
|
+
cli_scenario = scenarios_dir / "cli-smoke.testql.toon.yaml"
|
|
117
|
+
cli_scenario.write_text("name: cli-smoke", encoding="utf-8")
|
|
118
|
+
|
|
119
|
+
web_scenario = scenarios_dir / "api-users-smoke.testql.toon.yaml"
|
|
120
|
+
web_scenario.write_text("name: api-smoke", encoding="utf-8")
|
|
121
|
+
|
|
122
|
+
# Create config with auto service
|
|
123
|
+
service_config = ServiceConfig(
|
|
124
|
+
name="auto-service",
|
|
125
|
+
type="auto",
|
|
126
|
+
paths=[],
|
|
127
|
+
root="",
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
config = WupConfig(
|
|
131
|
+
project=ProjectConfig(name="test"),
|
|
132
|
+
services=[service_config],
|
|
133
|
+
watch=WatchConfig(),
|
|
134
|
+
testql=TestQLConfig(scenario_dir="testql-scenarios"),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
watcher = TestQLWatcher(
|
|
138
|
+
project_root=str(root),
|
|
139
|
+
scenarios_dir="testql-scenarios",
|
|
140
|
+
config=config,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Filter scenarios for auto service
|
|
144
|
+
all_scenarios = list(scenarios_dir.glob("*.testql.toon.yaml"))
|
|
145
|
+
filtered = watcher._filter_scenarios_by_type(all_scenarios, "auto")
|
|
146
|
+
|
|
147
|
+
# Should include all scenarios
|
|
148
|
+
assert len(filtered) == 2
|
|
149
|
+
assert cli_scenario in filtered
|
|
150
|
+
assert web_scenario in filtered
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_score_scenario_cli_requires_exact_match():
|
|
154
|
+
"""Test CLI scenario exact matching."""
|
|
155
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
156
|
+
root = Path(tmpdir)
|
|
157
|
+
|
|
158
|
+
scenarios_dir = root / "testql-scenarios"
|
|
159
|
+
scenarios_dir.mkdir()
|
|
160
|
+
|
|
161
|
+
# Create CLI scenarios for different services
|
|
162
|
+
cli_wup = scenarios_dir / "cli-wup.testql.toon.yaml"
|
|
163
|
+
cli_wup.write_text("name: cli-wup", encoding="utf-8")
|
|
164
|
+
|
|
165
|
+
cli_koru = scenarios_dir / "cli-koru.testql.toon.yaml"
|
|
166
|
+
cli_koru.write_text("name: cli-koru", encoding="utf-8")
|
|
167
|
+
|
|
168
|
+
# Create config
|
|
169
|
+
config = WupConfig(
|
|
170
|
+
project=ProjectConfig(name="test"),
|
|
171
|
+
services=[],
|
|
172
|
+
watch=WatchConfig(),
|
|
173
|
+
testql=TestQLConfig(scenario_dir="testql-scenarios"),
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
watcher = TestQLWatcher(
|
|
177
|
+
project_root=str(root),
|
|
178
|
+
scenarios_dir="testql-scenarios",
|
|
179
|
+
config=config,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Score scenarios for "wup-shell" service
|
|
183
|
+
tokens = watcher._tokenize_service("wup-shell")
|
|
184
|
+
|
|
185
|
+
wup_score = watcher._score_scenario(cli_wup, tokens)
|
|
186
|
+
koru_score = watcher._score_scenario(cli_koru, tokens)
|
|
187
|
+
|
|
188
|
+
# wup scenario should match, koru should not
|
|
189
|
+
assert wup_score > 0 # Should match
|
|
190
|
+
assert koru_score < 0 # Should be penalized
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def test_score_scenario_non_cli_uses_original_scoring():
|
|
194
|
+
"""Test non-CLI scenario scoring."""
|
|
195
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
196
|
+
root = Path(tmpdir)
|
|
197
|
+
|
|
198
|
+
scenarios_dir = root / "testql-scenarios"
|
|
199
|
+
scenarios_dir.mkdir()
|
|
200
|
+
|
|
201
|
+
# Create non-CLI scenarios
|
|
202
|
+
api_scenario = scenarios_dir / "api-users-smoke.testql.toon.yaml"
|
|
203
|
+
api_scenario.write_text("name: api-users-smoke", encoding="utf-8")
|
|
204
|
+
|
|
205
|
+
smoke_scenario = scenarios_dir / "infra-smoke.testql.toon.yaml"
|
|
206
|
+
smoke_scenario.write_text("name: infra-smoke", encoding="utf-8")
|
|
207
|
+
|
|
208
|
+
# Create config
|
|
209
|
+
config = WupConfig(
|
|
210
|
+
project=ProjectConfig(name="test"),
|
|
211
|
+
services=[],
|
|
212
|
+
watch=WatchConfig(),
|
|
213
|
+
testql=TestQLConfig(scenario_dir="testql-scenarios"),
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
watcher = TestQLWatcher(
|
|
217
|
+
project_root=str(root),
|
|
218
|
+
scenarios_dir="testql-scenarios",
|
|
219
|
+
config=config,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Score scenarios for "api-users" service
|
|
223
|
+
tokens = watcher._tokenize_service("api-users")
|
|
224
|
+
|
|
225
|
+
api_score = watcher._score_scenario(api_scenario, tokens)
|
|
226
|
+
smoke_score = watcher._score_scenario(smoke_scenario, tokens)
|
|
227
|
+
|
|
228
|
+
# api scenario should score higher for api-users service
|
|
229
|
+
assert api_score > smoke_score
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def test_scenario_matches_type():
|
|
233
|
+
"""Test scenario type matching."""
|
|
234
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
235
|
+
root = Path(tmpdir)
|
|
236
|
+
|
|
237
|
+
scenarios_dir = root / "testql-scenarios"
|
|
238
|
+
scenarios_dir.mkdir()
|
|
239
|
+
|
|
240
|
+
cli_scenario = scenarios_dir / "cli-wup.testql.toon.yaml"
|
|
241
|
+
cli_scenario.write_text("name: cli-wup", encoding="utf-8")
|
|
242
|
+
|
|
243
|
+
web_scenario = scenarios_dir / "api-users.testql.toon.yaml"
|
|
244
|
+
web_scenario.write_text("name: api-users", encoding="utf-8")
|
|
245
|
+
|
|
246
|
+
# Create config
|
|
247
|
+
config = WupConfig(
|
|
248
|
+
project=ProjectConfig(name="test"),
|
|
249
|
+
services=[],
|
|
250
|
+
watch=WatchConfig(),
|
|
251
|
+
testql=TestQLConfig(scenario_dir="testql-scenarios"),
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
watcher = TestQLWatcher(
|
|
255
|
+
project_root=str(root),
|
|
256
|
+
scenarios_dir="testql-scenarios",
|
|
257
|
+
config=config,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
# Test type matching
|
|
261
|
+
assert watcher._scenario_matches_type(cli_scenario, "shell") == True
|
|
262
|
+
assert watcher._scenario_matches_type(cli_scenario, "web") == False
|
|
263
|
+
assert watcher._scenario_matches_type(web_scenario, "shell") == False
|
|
264
|
+
assert watcher._scenario_matches_type(web_scenario, "web") == True
|
|
265
|
+
assert watcher._scenario_matches_type(cli_scenario, "auto") == True
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""Unit tests for service inference logic."""
|
|
2
|
+
import tempfile
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from unittest.mock import Mock
|
|
5
|
+
|
|
6
|
+
from wup.core import WupWatcher
|
|
7
|
+
from wup.models.config import (
|
|
8
|
+
ProjectConfig,
|
|
9
|
+
ServiceConfig,
|
|
10
|
+
WatchConfig,
|
|
11
|
+
WupConfig,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_infer_service_with_empty_paths_uses_configured_services():
|
|
16
|
+
"""Verify configured services are used when paths are empty."""
|
|
17
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
18
|
+
root = Path(tmpdir)
|
|
19
|
+
|
|
20
|
+
# Create config with services that have empty paths
|
|
21
|
+
service_config = ServiceConfig(
|
|
22
|
+
name="my-service",
|
|
23
|
+
paths=[], # Empty paths
|
|
24
|
+
root="",
|
|
25
|
+
type="shell"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
config = WupConfig(
|
|
29
|
+
project=ProjectConfig(name="test"),
|
|
30
|
+
services=[service_config],
|
|
31
|
+
watch=WatchConfig(),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
watcher = WupWatcher(project_root=str(root), config=config)
|
|
35
|
+
|
|
36
|
+
# Change a file that doesn't match service name
|
|
37
|
+
test_file = root / "src" / "other" / "file.py"
|
|
38
|
+
test_file.parent.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
test_file.write_text("test", encoding="utf-8")
|
|
40
|
+
|
|
41
|
+
# Since paths are empty and service name doesn't match path,
|
|
42
|
+
# inference should return None
|
|
43
|
+
service = watcher.infer_service(str(test_file))
|
|
44
|
+
assert service is None or service != "my-service"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_infer_service_with_explicit_paths_matches_path_patterns():
|
|
48
|
+
"""Test explicit path matching."""
|
|
49
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
50
|
+
root = Path(tmpdir)
|
|
51
|
+
|
|
52
|
+
# Create config with explicit paths
|
|
53
|
+
service_config = ServiceConfig(
|
|
54
|
+
name="api-service",
|
|
55
|
+
paths=["src/api/**"],
|
|
56
|
+
root="",
|
|
57
|
+
type="web"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
config = WupConfig(
|
|
61
|
+
project=ProjectConfig(name="test"),
|
|
62
|
+
services=[service_config],
|
|
63
|
+
watch=WatchConfig(),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
watcher = WupWatcher(project_root=str(root), config=config)
|
|
67
|
+
|
|
68
|
+
# Create a file that matches the explicit path
|
|
69
|
+
test_file = root / "src" / "api" / "users.py"
|
|
70
|
+
test_file.parent.mkdir(parents=True, exist_ok=True)
|
|
71
|
+
test_file.write_text("test", encoding="utf-8")
|
|
72
|
+
|
|
73
|
+
service = watcher.infer_service(str(test_file))
|
|
74
|
+
assert service == "api-service"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_infer_service_with_auto_detection_matches_name_segments():
|
|
78
|
+
"""Test auto-detection with service name matching."""
|
|
79
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
80
|
+
root = Path(tmpdir)
|
|
81
|
+
|
|
82
|
+
# Create config with service name that should match path segments
|
|
83
|
+
service_config = ServiceConfig(
|
|
84
|
+
name="users-service",
|
|
85
|
+
paths=[], # Empty paths - use auto-detection
|
|
86
|
+
root="",
|
|
87
|
+
type="web"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
config = WupConfig(
|
|
91
|
+
project=ProjectConfig(name="test"),
|
|
92
|
+
services=[service_config],
|
|
93
|
+
watch=WatchConfig(),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
watcher = WupWatcher(project_root=str(root), config=config)
|
|
97
|
+
|
|
98
|
+
# Create a file with service name in path
|
|
99
|
+
test_file = root / "users-service" / "routes.py"
|
|
100
|
+
test_file.parent.mkdir(parents=True, exist_ok=True)
|
|
101
|
+
test_file.write_text("test", encoding="utf-8")
|
|
102
|
+
|
|
103
|
+
service = watcher.infer_service(str(test_file))
|
|
104
|
+
assert service == "users-service"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_infer_service_returns_none_for_unmatched_files():
|
|
108
|
+
"""Verify None or invalid service returned when no match."""
|
|
109
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
110
|
+
root = Path(tmpdir)
|
|
111
|
+
|
|
112
|
+
# Create config with services that don't match the test file
|
|
113
|
+
service_config = ServiceConfig(
|
|
114
|
+
name="api-service",
|
|
115
|
+
paths=["src/api/**"],
|
|
116
|
+
root="",
|
|
117
|
+
type="web"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
config = WupConfig(
|
|
121
|
+
project=ProjectConfig(name="test"),
|
|
122
|
+
services=[service_config],
|
|
123
|
+
watch=WatchConfig(),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
watcher = WupWatcher(project_root=str(root), config=config)
|
|
127
|
+
|
|
128
|
+
# Create a file that doesn't match any service
|
|
129
|
+
test_file = root / "other" / "unrelated.py"
|
|
130
|
+
test_file.parent.mkdir(parents=True, exist_ok=True)
|
|
131
|
+
test_file.write_text("test", encoding="utf-8")
|
|
132
|
+
|
|
133
|
+
service = watcher.infer_service(str(test_file))
|
|
134
|
+
# Should return None or an invalid service that doesn't match config
|
|
135
|
+
# The inference may construct a service name from path parts as fallback
|
|
136
|
+
assert service is None or (service and service != "api-service")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_infer_service_with_duplicate_service_names():
|
|
140
|
+
"""Handle duplicate service names."""
|
|
141
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
142
|
+
root = Path(tmpdir)
|
|
143
|
+
|
|
144
|
+
# Create config with duplicate service names (shouldn't happen but test edge case)
|
|
145
|
+
service_config1 = ServiceConfig(
|
|
146
|
+
name="api-service",
|
|
147
|
+
paths=["src/api/v1/**"],
|
|
148
|
+
root="",
|
|
149
|
+
type="web"
|
|
150
|
+
)
|
|
151
|
+
service_config2 = ServiceConfig(
|
|
152
|
+
name="api-service",
|
|
153
|
+
paths=["src/api/v2/**"],
|
|
154
|
+
root="",
|
|
155
|
+
type="web"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
config = WupConfig(
|
|
159
|
+
project=ProjectConfig(name="test"),
|
|
160
|
+
services=[service_config1, service_config2],
|
|
161
|
+
watch=WatchConfig(),
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
watcher = WupWatcher(project_root=str(root), config=config)
|
|
165
|
+
|
|
166
|
+
# Create files that match both services
|
|
167
|
+
test_file1 = root / "src" / "api" / "v1" / "users.py"
|
|
168
|
+
test_file1.parent.mkdir(parents=True, exist_ok=True)
|
|
169
|
+
test_file1.write_text("test", encoding="utf-8")
|
|
170
|
+
|
|
171
|
+
service = watcher.infer_service(str(test_file1))
|
|
172
|
+
# Should return the first matching service
|
|
173
|
+
assert service == "api-service"
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def test_file_change_uses_configured_services_when_inference_fails():
|
|
177
|
+
"""Test that configured services are used when inference fails."""
|
|
178
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
179
|
+
root = Path(tmpdir)
|
|
180
|
+
|
|
181
|
+
# Create config with services that have empty paths
|
|
182
|
+
service_config = ServiceConfig(
|
|
183
|
+
name="my-service",
|
|
184
|
+
paths=[],
|
|
185
|
+
root="",
|
|
186
|
+
type="shell"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
config = WupConfig(
|
|
190
|
+
project=ProjectConfig(name="test"),
|
|
191
|
+
services=[service_config],
|
|
192
|
+
watch=WatchConfig(),
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
watcher = WupWatcher(project_root=str(root), config=config)
|
|
196
|
+
|
|
197
|
+
# Create a file that won't match the service
|
|
198
|
+
test_file = root / "src" / "other" / "file.py"
|
|
199
|
+
test_file.parent.mkdir(parents=True, exist_ok=True)
|
|
200
|
+
test_file.write_text("test", encoding="utf-8")
|
|
201
|
+
|
|
202
|
+
# Mock schedule_quick_test to track which services are tested
|
|
203
|
+
tested_services = []
|
|
204
|
+
original_schedule = watcher.schedule_quick_test
|
|
205
|
+
watcher.schedule_quick_test = lambda s: tested_services.append(s)
|
|
206
|
+
|
|
207
|
+
# Trigger file change
|
|
208
|
+
watcher.on_file_change(str(test_file))
|
|
209
|
+
|
|
210
|
+
# Should test the configured service even though inference failed
|
|
211
|
+
assert "my-service" in tested_services
|
|
@@ -952,11 +952,12 @@ class TestConfigModels:
|
|
|
952
952
|
scenario_dir="scenarios/tests",
|
|
953
953
|
smoke_scenario="smoke.testql.toon.yaml",
|
|
954
954
|
output_format="json",
|
|
955
|
-
extra_args=["--timeout
|
|
955
|
+
extra_args=["--timeout", "10"]
|
|
956
956
|
)
|
|
957
957
|
assert config.scenario_dir == "scenarios/tests"
|
|
958
|
+
assert config.smoke_scenario == "smoke.testql.toon.yaml"
|
|
958
959
|
assert config.output_format == "json"
|
|
959
|
-
assert len(config.extra_args) ==
|
|
960
|
+
assert len(config.extra_args) == 2
|
|
960
961
|
|
|
961
962
|
def test_wup_config(self):
|
|
962
963
|
"""Test WupConfig dataclass."""
|
|
@@ -1283,7 +1284,8 @@ testql:
|
|
|
1283
1284
|
smoke_scenario: "smoke.testql.toon.yaml"
|
|
1284
1285
|
output_format: "json"
|
|
1285
1286
|
extra_args:
|
|
1286
|
-
- "--timeout
|
|
1287
|
+
- "--timeout"
|
|
1288
|
+
- "10"
|
|
1287
1289
|
"""
|
|
1288
1290
|
config_path = Path(tmpdir) / "wup.yaml"
|
|
1289
1291
|
config_path.write_text(config_content)
|
|
@@ -1694,7 +1696,7 @@ class TestTestQLWatcherConfig:
|
|
|
1694
1696
|
testql_config = TestQLConfig(
|
|
1695
1697
|
scenario_dir="scenarios/tests",
|
|
1696
1698
|
smoke_scenario="smoke.testql.toon.yaml",
|
|
1697
|
-
extra_args=["--timeout
|
|
1699
|
+
extra_args=["--timeout", "10"]
|
|
1698
1700
|
)
|
|
1699
1701
|
config = WupConfig(
|
|
1700
1702
|
project=ProjectConfig(name="test"),
|
|
@@ -1706,7 +1708,7 @@ class TestTestQLWatcherConfig:
|
|
|
1706
1708
|
|
|
1707
1709
|
watcher = TestQLWatcher(tmpdir, config=config)
|
|
1708
1710
|
assert watcher.config.project.name == "test"
|
|
1709
|
-
assert watcher.testql_extra_args == ["--timeout
|
|
1711
|
+
assert watcher.testql_extra_args == ["--timeout", "10"]
|
|
1710
1712
|
|
|
1711
1713
|
def test_testql_watcher_uses_config_scenarios_dir(self):
|
|
1712
1714
|
"""Test that TestQLWatcher uses config scenario directory."""
|
|
@@ -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.43"
|
|
11
11
|
__author__ = "Tom Sapletta"
|
|
12
12
|
|
|
13
13
|
from .config import load_config, save_config, get_default_config
|
|
@@ -175,7 +175,7 @@ def validate_config(raw: dict) -> WupConfig:
|
|
|
175
175
|
scenario_dir=testql_raw.get("scenario_dir", "scenarios/tests"),
|
|
176
176
|
smoke_scenario=testql_raw.get("smoke_scenario", "smoke.testql.toon.yaml"),
|
|
177
177
|
output_format=testql_raw.get("output_format", "json"),
|
|
178
|
-
extra_args=testql_raw.get("extra_args", ["--timeout", "
|
|
178
|
+
extra_args=testql_raw.get("extra_args", ["--timeout", "10"]),
|
|
179
179
|
endpoint_discovery=testql_raw.get("endpoint_discovery", True),
|
|
180
180
|
probe_interval_s=int(testql_raw.get("probe_interval_s", 0) or 0),
|
|
181
181
|
health_scenario=testql_raw.get("health_scenario", ""),
|
|
@@ -155,12 +155,8 @@ class WupWatcher:
|
|
|
155
155
|
if service:
|
|
156
156
|
return service
|
|
157
157
|
|
|
158
|
-
# Fallback:
|
|
159
|
-
|
|
160
|
-
# Check if file exists (absolute path)
|
|
161
|
-
if Path(file_path).is_file():
|
|
162
|
-
return "/".join(parts[:2])
|
|
163
|
-
|
|
158
|
+
# Fallback: return None to let caller handle configured services
|
|
159
|
+
# Don't construct fake service names from path parts
|
|
164
160
|
return None
|
|
165
161
|
|
|
166
162
|
def _is_coincident_pair(self, type_a: str, type_b: str) -> bool:
|
|
@@ -438,6 +434,37 @@ class WupWatcher:
|
|
|
438
434
|
file_suffix = Path(file_path).suffix.lower()
|
|
439
435
|
return file_suffix in self.config.watch.file_types
|
|
440
436
|
|
|
437
|
+
def _is_file_ignored(self, rel_path: Path) -> bool:
|
|
438
|
+
"""Check if a file should be ignored based on paths and types."""
|
|
439
|
+
skip_dirs = {".git", "__pycache__", "node_modules", ".venv", "dist", "build"}
|
|
440
|
+
if any(part in skip_dirs for part in rel_path.parts):
|
|
441
|
+
return True
|
|
442
|
+
|
|
443
|
+
for pattern in self.config.watch.exclude_patterns:
|
|
444
|
+
if pattern.startswith("*") and rel_path.suffix == pattern[1:]:
|
|
445
|
+
return True
|
|
446
|
+
if pattern in str(rel_path):
|
|
447
|
+
return True
|
|
448
|
+
|
|
449
|
+
if self.config.watch.file_types:
|
|
450
|
+
file_ext = rel_path.suffix if rel_path.suffix else ""
|
|
451
|
+
if not file_ext.startswith("."):
|
|
452
|
+
file_ext = f".{file_ext}"
|
|
453
|
+
if file_ext not in self.config.watch.file_types:
|
|
454
|
+
return True
|
|
455
|
+
|
|
456
|
+
return False
|
|
457
|
+
|
|
458
|
+
def _notify_all_configured_services(self, rel_path: Path):
|
|
459
|
+
"""Notify all configured services about a file change."""
|
|
460
|
+
if not self.config.services:
|
|
461
|
+
return
|
|
462
|
+
for svc in self.config.services:
|
|
463
|
+
if self.should_test(svc.name):
|
|
464
|
+
self.changed_services.add(svc.name)
|
|
465
|
+
self.console.print(f"[yellow]📝 Changed: {rel_path} → Service: {svc.name}[/yellow]")
|
|
466
|
+
self.schedule_quick_test(svc.name)
|
|
467
|
+
|
|
441
468
|
def on_file_change(self, file_path: str):
|
|
442
469
|
"""
|
|
443
470
|
Handle file change event.
|
|
@@ -445,41 +472,25 @@ class WupWatcher:
|
|
|
445
472
|
Args:
|
|
446
473
|
file_path: Path to the changed file
|
|
447
474
|
"""
|
|
448
|
-
# Check file type filter
|
|
449
475
|
if not self.should_watch_file(file_path):
|
|
450
476
|
return
|
|
451
477
|
|
|
452
|
-
# Only watch relevant directories
|
|
453
478
|
rel_path = self._to_relative_path(file_path)
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
# Skip certain directories
|
|
457
|
-
skip_dirs = {".git", "__pycache__", "node_modules", ".venv", "dist", "build"}
|
|
458
|
-
if any(part in skip_dirs for part in parts):
|
|
479
|
+
if self._is_file_ignored(rel_path):
|
|
459
480
|
return
|
|
460
481
|
|
|
461
|
-
# Check exclude patterns from config
|
|
462
|
-
for pattern in self.config.watch.exclude_patterns:
|
|
463
|
-
if pattern.startswith("*") and rel_path.suffix == pattern[1:]:
|
|
464
|
-
return
|
|
465
|
-
if pattern in str(rel_path):
|
|
466
|
-
return
|
|
467
|
-
|
|
468
|
-
# Filter by file type if specified in config
|
|
469
|
-
if self.config.watch.file_types:
|
|
470
|
-
# Ensure file extensions start with dot
|
|
471
|
-
file_ext = rel_path.suffix if rel_path.suffix else ""
|
|
472
|
-
if not file_ext.startswith("."):
|
|
473
|
-
file_ext = f".{file_ext}"
|
|
474
|
-
|
|
475
|
-
# Check if file extension matches any of the configured types
|
|
476
|
-
if file_ext not in self.config.watch.file_types:
|
|
477
|
-
return
|
|
478
|
-
|
|
479
|
-
# Infer service from file path
|
|
480
482
|
service = self.infer_service(file_path)
|
|
481
483
|
|
|
482
|
-
|
|
484
|
+
service_matches_config = False
|
|
485
|
+
if service and self.config.services:
|
|
486
|
+
for svc in self.config.services:
|
|
487
|
+
if service == svc.name:
|
|
488
|
+
service_matches_config = True
|
|
489
|
+
break
|
|
490
|
+
|
|
491
|
+
if not service or not service_matches_config:
|
|
492
|
+
self._notify_all_configured_services(rel_path)
|
|
493
|
+
elif service and self.should_test(service):
|
|
483
494
|
self.changed_services.add(service)
|
|
484
495
|
self.console.print(f"[yellow]📝 Changed: {rel_path} → Service: {service}[/yellow]")
|
|
485
496
|
self.schedule_quick_test(service)
|
|
@@ -61,7 +61,7 @@ class TestQLConfig:
|
|
|
61
61
|
scenario_dir: str = "scenarios/tests"
|
|
62
62
|
smoke_scenario: str = "smoke.testql.toon.yaml"
|
|
63
63
|
output_format: str = "json"
|
|
64
|
-
extra_args: List[str] = field(default_factory=lambda: ["--timeout", "
|
|
64
|
+
extra_args: List[str] = field(default_factory=lambda: ["--timeout", "10"])
|
|
65
65
|
endpoint_discovery: bool = True # Merge health probes from scenarios + service maps
|
|
66
66
|
probe_interval_s: int = 0 # Periodic live probes for all services (0 = file-change only)
|
|
67
67
|
health_scenario: str = "" # Fleet TestQL scenario on each periodic probe cycle (live run)
|
|
@@ -335,6 +335,24 @@ class TestQLWatcher(WupWatcher):
|
|
|
335
335
|
score -= 5
|
|
336
336
|
return score
|
|
337
337
|
|
|
338
|
+
def _get_scored_scenarios(self, scenarios: List[Path], tokens: Set[str], limit: int) -> List[Path]:
|
|
339
|
+
scored = sorted(
|
|
340
|
+
((self._score_scenario(s, tokens), s) for s in scenarios),
|
|
341
|
+
key=lambda item: (item[0], item[1].name),
|
|
342
|
+
reverse=True,
|
|
343
|
+
)
|
|
344
|
+
return [s for score, s in scored if score > 0][:limit]
|
|
345
|
+
|
|
346
|
+
def _get_smoke_fallback(self, svc_type: str) -> List[Path]:
|
|
347
|
+
smoke_name = (self.config.testql.smoke_scenario or "").strip()
|
|
348
|
+
if not smoke_name:
|
|
349
|
+
return []
|
|
350
|
+
for base in (self.scenarios_dir, self.project_root):
|
|
351
|
+
candidate = base / smoke_name
|
|
352
|
+
if candidate.exists() and self._scenario_matches_type(candidate, svc_type):
|
|
353
|
+
return [candidate]
|
|
354
|
+
return []
|
|
355
|
+
|
|
338
356
|
def _select_scenarios_for_service(self, service: str) -> List[Path]:
|
|
339
357
|
all_scenarios = self._discover_scenarios()
|
|
340
358
|
if not all_scenarios:
|
|
@@ -346,30 +364,15 @@ class TestQLWatcher(WupWatcher):
|
|
|
346
364
|
|
|
347
365
|
# Filter scenarios by service type
|
|
348
366
|
svc_type = svc_config.type if svc_config else "auto"
|
|
349
|
-
# Debug: print service type
|
|
350
|
-
import sys
|
|
351
|
-
print(f"DEBUG: service={service}, svc_type={svc_type}, svc_config={svc_config.type if svc_config else None}", file=sys.stderr)
|
|
352
367
|
filtered_scenarios = self._filter_scenarios_by_type(all_scenarios, svc_type)
|
|
353
|
-
print(f"DEBUG: all_scenarios={len(all_scenarios)}, filtered={len(filtered_scenarios)}", file=sys.stderr)
|
|
354
368
|
|
|
355
|
-
|
|
356
|
-
scored = sorted(
|
|
357
|
-
((self._score_scenario(s, tokens), s) for s in filtered_scenarios),
|
|
358
|
-
key=lambda item: (item[0], item[1].name),
|
|
359
|
-
reverse=True,
|
|
360
|
-
)
|
|
361
|
-
selected = [s for score, s in scored if score > 0][:limit]
|
|
369
|
+
selected = self._get_scored_scenarios(filtered_scenarios, self._tokenize_service(service), limit)
|
|
362
370
|
if selected:
|
|
363
371
|
return selected
|
|
364
372
|
|
|
365
|
-
|
|
366
|
-
if
|
|
367
|
-
|
|
368
|
-
candidate = base / smoke_name
|
|
369
|
-
if candidate.exists():
|
|
370
|
-
# Only use smoke scenario if it matches service type
|
|
371
|
-
if self._scenario_matches_type(candidate, svc_type):
|
|
372
|
-
return [candidate]
|
|
373
|
+
smoke = self._get_smoke_fallback(svc_type)
|
|
374
|
+
if smoke:
|
|
375
|
+
return smoke
|
|
373
376
|
|
|
374
377
|
# Fallback: don't return any scenarios for web services if no match found
|
|
375
378
|
# This prevents CLI scenarios from being assigned to web services
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wup
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.43
|
|
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.2260 (53 commits)
|
|
38
|
+
- 👤 **Human dev:** ~$2056 (20.6h @ $100/h, 30min dedup)
|
|
39
39
|
|
|
40
|
-
Generated on 2026-05-
|
|
40
|
+
Generated on 2026-05-23 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
|
|
|
@@ -461,9 +461,12 @@ wup/
|
|
|
461
461
|
│ ├── core.py # WupWatcher: detection, inference, scheduling
|
|
462
462
|
│ ├── dependency_mapper.py # DependencyMapper: codebase → deps.json
|
|
463
463
|
│ ├── testql_discovery.py # TestQLEndpointDiscovery: scenario parsing
|
|
464
|
+
│ ├── testql_monitor.py # TestQLMonitor: extracts live HTTP probes and Docker services
|
|
464
465
|
│ ├── testql_watcher.py # TestQLWatcher: scenario runner + health tracking
|
|
465
466
|
│ ├── visual_diff.py # VisualDiffer: Playwright DOM snapshot + diff engine
|
|
466
467
|
│ ├── web_client.py # WebClient: async HTTP event sink → wupbro
|
|
468
|
+
│ ├── monitoring_manifest.py # Builds and patches the wup.yaml monitoring block
|
|
469
|
+
│ ├── planfile_reporter.py # PlanfileReporter: creates and deduplicates Planfile tickets
|
|
467
470
|
│ └── models/
|
|
468
471
|
│ ├── __init__.py
|
|
469
472
|
│ └── config.py # Dataclasses: WupConfig, ServiceConfig, WatchConfig, TestStrategyConfig, TestQLConfig, VisualDiffConfig, WebConfig, AnomalyDetectionConfig...
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
LICENSE
|
|
2
2
|
README.md
|
|
3
3
|
pyproject.toml
|
|
4
|
+
tests/test_auto_detection.py
|
|
5
|
+
tests/test_cli_filtering.py
|
|
4
6
|
tests/test_e2e.py
|
|
5
7
|
tests/test_monitoring_manifest.py
|
|
8
|
+
tests/test_service_inference.py
|
|
6
9
|
tests/test_testql_monitor.py
|
|
7
10
|
tests/test_testql_watcher.py
|
|
8
11
|
tests/test_web_client.py
|
|
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
|