wup 0.1.9__tar.gz → 0.1.10__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.1.9/wup.egg-info → wup-0.1.10}/PKG-INFO +5 -5
- {wup-0.1.9 → wup-0.1.10}/README.md +4 -4
- {wup-0.1.9 → wup-0.1.10}/pyproject.toml +1 -1
- {wup-0.1.9 → wup-0.1.10}/wup/__init__.py +1 -1
- wup-0.1.10/wup/testql_watcher.py +247 -0
- {wup-0.1.9 → wup-0.1.10/wup.egg-info}/PKG-INFO +5 -5
- {wup-0.1.9 → wup-0.1.10}/wup.egg-info/SOURCES.txt +1 -0
- {wup-0.1.9 → wup-0.1.10}/LICENSE +0 -0
- {wup-0.1.9 → wup-0.1.10}/setup.cfg +0 -0
- {wup-0.1.9 → wup-0.1.10}/tests/test_wup.py +0 -0
- {wup-0.1.9 → wup-0.1.10}/wup/cli.py +0 -0
- {wup-0.1.9 → wup-0.1.10}/wup/core.py +0 -0
- {wup-0.1.9 → wup-0.1.10}/wup/dependency_mapper.py +0 -0
- {wup-0.1.9 → wup-0.1.10}/wup.egg-info/dependency_links.txt +0 -0
- {wup-0.1.9 → wup-0.1.10}/wup.egg-info/entry_points.txt +0 -0
- {wup-0.1.9 → wup-0.1.10}/wup.egg-info/requires.txt +0 -0
- {wup-0.1.9 → wup-0.1.10}/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.1.
|
|
3
|
+
Version: 0.1.10
|
|
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
|
|
@@ -28,17 +28,17 @@ Dynamic: license-file
|
|
|
28
28
|
|
|
29
29
|
## AI Cost Tracking
|
|
30
30
|
|
|
31
|
-
    
|
|
32
|
+
  
|
|
33
33
|
|
|
34
|
-
- 🤖 **LLM usage:** $0.
|
|
34
|
+
- 🤖 **LLM usage:** $0.6000 (4 commits)
|
|
35
35
|
- 👤 **Human dev:** ~$200 (2.0h @ $100/h, 30min dedup)
|
|
36
36
|
|
|
37
37
|
Generated on 2026-04-29 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
38
38
|
|
|
39
39
|
---
|
|
40
40
|
|
|
41
|
-
    
|
|
42
42
|
|
|
43
43
|
**WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
|
|
44
44
|
|
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
|
|
4
4
|
## AI Cost Tracking
|
|
5
5
|
|
|
6
|
-
    
|
|
7
|
+
  
|
|
8
8
|
|
|
9
|
-
- 🤖 **LLM usage:** $0.
|
|
9
|
+
- 🤖 **LLM usage:** $0.6000 (4 commits)
|
|
10
10
|
- 👤 **Human dev:** ~$200 (2.0h @ $100/h, 30min dedup)
|
|
11
11
|
|
|
12
12
|
Generated on 2026-04-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
|
|
|
@@ -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.1.
|
|
10
|
+
__version__ = "0.1.10"
|
|
11
11
|
__author__ = "Tom Sapletta"
|
|
12
12
|
|
|
13
13
|
from .core import WupWatcher
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""TestQL integration for WUP watcher."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
import subprocess
|
|
9
|
+
import time
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Dict, List, Optional, Sequence
|
|
12
|
+
from urllib import error, request
|
|
13
|
+
|
|
14
|
+
from .core import WupWatcher
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BrowserNotifier:
|
|
18
|
+
"""Send watcher events to browser-facing service and local file."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, service_url: Optional[str], events_file: Path):
|
|
21
|
+
self.service_url = service_url
|
|
22
|
+
self.events_file = events_file
|
|
23
|
+
self.events_file.parent.mkdir(parents=True, exist_ok=True)
|
|
24
|
+
|
|
25
|
+
def notify(self, payload: Dict) -> None:
|
|
26
|
+
payload_with_ts = {"timestamp": int(time.time()), **payload}
|
|
27
|
+
self.events_file.write_text(json.dumps(payload_with_ts, indent=2), encoding="utf-8")
|
|
28
|
+
|
|
29
|
+
if not self.service_url:
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
body = json.dumps(payload_with_ts).encode("utf-8")
|
|
33
|
+
req = request.Request(
|
|
34
|
+
self.service_url,
|
|
35
|
+
data=body,
|
|
36
|
+
headers={"Content-Type": "application/json"},
|
|
37
|
+
method="POST",
|
|
38
|
+
)
|
|
39
|
+
try:
|
|
40
|
+
with request.urlopen(req, timeout=5):
|
|
41
|
+
return
|
|
42
|
+
except (error.URLError, TimeoutError):
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TestQLWatcher(WupWatcher):
|
|
47
|
+
"""WUP watcher running selective TestQL scenarios for changed services."""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
project_root: str,
|
|
52
|
+
scenarios_dir: str = "testql-scenarios",
|
|
53
|
+
testql_bin: str = "testql",
|
|
54
|
+
track_dir: str = ".wup/tracks",
|
|
55
|
+
browser_service_url: Optional[str] = None,
|
|
56
|
+
quick_limit: int = 3,
|
|
57
|
+
**kwargs,
|
|
58
|
+
):
|
|
59
|
+
super().__init__(project_root=project_root, **kwargs)
|
|
60
|
+
self.scenarios_dir = self.project_root / scenarios_dir
|
|
61
|
+
self.testql_bin = testql_bin
|
|
62
|
+
self.quick_limit = quick_limit
|
|
63
|
+
self.track_dir = self.project_root / track_dir
|
|
64
|
+
self.track_dir.mkdir(parents=True, exist_ok=True)
|
|
65
|
+
self.browser_notifier = BrowserNotifier(
|
|
66
|
+
service_url=browser_service_url,
|
|
67
|
+
events_file=self.project_root / ".wup" / "browser-events" / "latest.json",
|
|
68
|
+
)
|
|
69
|
+
self.last_track_path: Optional[Path] = None
|
|
70
|
+
|
|
71
|
+
def _tokenize_service(self, service: str) -> List[str]:
|
|
72
|
+
raw_tokens = re.split(r"[^a-zA-Z0-9]+", service.lower())
|
|
73
|
+
return [token for token in raw_tokens if len(token) >= 3]
|
|
74
|
+
|
|
75
|
+
def _discover_scenarios(self) -> List[Path]:
|
|
76
|
+
if not self.scenarios_dir.exists():
|
|
77
|
+
return []
|
|
78
|
+
return sorted(self.scenarios_dir.rglob("*.testql.toon.yaml"))
|
|
79
|
+
|
|
80
|
+
def _select_scenarios_for_service(self, service: str) -> List[Path]:
|
|
81
|
+
all_scenarios = self._discover_scenarios()
|
|
82
|
+
if not all_scenarios:
|
|
83
|
+
return []
|
|
84
|
+
|
|
85
|
+
tokens = self._tokenize_service(service)
|
|
86
|
+
scored: List[tuple[int, Path]] = []
|
|
87
|
+
|
|
88
|
+
for scenario in all_scenarios:
|
|
89
|
+
name = scenario.name.lower()
|
|
90
|
+
score = 0
|
|
91
|
+
if any(token in name for token in tokens):
|
|
92
|
+
score += 3
|
|
93
|
+
if "api" in name or "endpoint" in name:
|
|
94
|
+
score += 2
|
|
95
|
+
if "infra" in name or "smoke" in name:
|
|
96
|
+
score += 1
|
|
97
|
+
scored.append((score, scenario))
|
|
98
|
+
|
|
99
|
+
scored.sort(key=lambda item: (item[0], item[1].name), reverse=True)
|
|
100
|
+
selected = [scenario for score, scenario in scored if score > 0]
|
|
101
|
+
|
|
102
|
+
if selected:
|
|
103
|
+
return selected
|
|
104
|
+
|
|
105
|
+
return all_scenarios[: self.quick_limit]
|
|
106
|
+
|
|
107
|
+
def _run_testql(self, args: Sequence[str], timeout: int) -> subprocess.CompletedProcess:
|
|
108
|
+
cmd = [self.testql_bin, *args]
|
|
109
|
+
try:
|
|
110
|
+
return subprocess.run(
|
|
111
|
+
cmd,
|
|
112
|
+
cwd=str(self.project_root),
|
|
113
|
+
capture_output=True,
|
|
114
|
+
text=True,
|
|
115
|
+
timeout=timeout,
|
|
116
|
+
)
|
|
117
|
+
except FileNotFoundError:
|
|
118
|
+
fallback_cmd = ["python3", "-m", "testql.cli", *args]
|
|
119
|
+
return subprocess.run(
|
|
120
|
+
fallback_cmd,
|
|
121
|
+
cwd=str(self.project_root),
|
|
122
|
+
capture_output=True,
|
|
123
|
+
text=True,
|
|
124
|
+
timeout=timeout,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def _write_track(self, *, service: str, stage: str, scenario: Optional[Path], result: subprocess.CompletedProcess) -> Path:
|
|
128
|
+
ts = int(time.time())
|
|
129
|
+
safe_service = service.replace("/", "_").replace("\\", "_")
|
|
130
|
+
scenario_name = scenario.name if scenario else "unknown"
|
|
131
|
+
stderr_line = (result.stderr or "").strip().splitlines()[:1]
|
|
132
|
+
stdout_line = (result.stdout or "").strip().splitlines()[:1]
|
|
133
|
+
|
|
134
|
+
payload = {
|
|
135
|
+
"service": service,
|
|
136
|
+
"stage": stage,
|
|
137
|
+
"scenario": str(scenario) if scenario else None,
|
|
138
|
+
"command": result.args,
|
|
139
|
+
"returncode": result.returncode,
|
|
140
|
+
"stderr_head": stderr_line[0] if stderr_line else "",
|
|
141
|
+
"stdout_head": stdout_line[0] if stdout_line else "",
|
|
142
|
+
"track": {
|
|
143
|
+
"file": str(scenario) if scenario else "",
|
|
144
|
+
"line": 1,
|
|
145
|
+
"hint": scenario_name,
|
|
146
|
+
},
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
track_path = self.track_dir / f"{ts}_{safe_service}_{stage}.json"
|
|
150
|
+
track_path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
|
151
|
+
self.last_track_path = track_path
|
|
152
|
+
|
|
153
|
+
self.browser_notifier.notify(
|
|
154
|
+
{
|
|
155
|
+
"type": "wup_testql_error",
|
|
156
|
+
"service": service,
|
|
157
|
+
"stage": stage,
|
|
158
|
+
"track_file": str(track_path),
|
|
159
|
+
"scenario": str(scenario) if scenario else None,
|
|
160
|
+
"message": payload["stderr_head"] or payload["stdout_head"] or "TestQL command failed",
|
|
161
|
+
}
|
|
162
|
+
)
|
|
163
|
+
return track_path
|
|
164
|
+
|
|
165
|
+
async def run_quick_test(self, service: str, endpoints: List[str]) -> bool:
|
|
166
|
+
scenarios = self._select_scenarios_for_service(service)[: self.quick_limit]
|
|
167
|
+
if not scenarios:
|
|
168
|
+
self.console.print(f"[yellow]⚠ No TestQL scenarios found for {service}[/yellow]")
|
|
169
|
+
return True
|
|
170
|
+
|
|
171
|
+
self.console.print(
|
|
172
|
+
f"[cyan]🧪 Quick TestQL for {service} ({len(scenarios)} scenarios / {len(endpoints)} endpoints)[/cyan]"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
for scenario in scenarios:
|
|
176
|
+
result = self._run_testql(["run", str(scenario), "--dry-run"], timeout=60)
|
|
177
|
+
if result.returncode != 0:
|
|
178
|
+
track_path = self._write_track(
|
|
179
|
+
service=service,
|
|
180
|
+
stage="quick",
|
|
181
|
+
scenario=scenario,
|
|
182
|
+
result=result,
|
|
183
|
+
)
|
|
184
|
+
self.console.print(
|
|
185
|
+
f"[red]✗ Quick failed: {scenario.name} | track: {track_path}[/red]"
|
|
186
|
+
)
|
|
187
|
+
return False
|
|
188
|
+
|
|
189
|
+
self.console.print(f"[green]✓ Quick TestQL passed for {service}[/green]")
|
|
190
|
+
return True
|
|
191
|
+
|
|
192
|
+
async def run_detail_test(self, service: str, endpoints: List[str]) -> Dict:
|
|
193
|
+
scenarios = self._select_scenarios_for_service(service)
|
|
194
|
+
results = {
|
|
195
|
+
"service": service,
|
|
196
|
+
"total_scenarios": len(scenarios),
|
|
197
|
+
"passed": 0,
|
|
198
|
+
"failed": 0,
|
|
199
|
+
"failed_scenarios": [],
|
|
200
|
+
"track_files": [],
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
self.console.print(
|
|
204
|
+
f"[cyan]🔍 Detail TestQL for {service} ({len(scenarios)} scenarios / {len(endpoints)} endpoints)[/cyan]"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
for scenario in scenarios:
|
|
208
|
+
result = self._run_testql(["run", str(scenario), "--output", "json"], timeout=180)
|
|
209
|
+
if result.returncode == 0:
|
|
210
|
+
results["passed"] += 1
|
|
211
|
+
continue
|
|
212
|
+
|
|
213
|
+
results["failed"] += 1
|
|
214
|
+
results["failed_scenarios"].append(str(scenario))
|
|
215
|
+
track_path = self._write_track(
|
|
216
|
+
service=service,
|
|
217
|
+
stage="detail",
|
|
218
|
+
scenario=scenario,
|
|
219
|
+
result=result,
|
|
220
|
+
)
|
|
221
|
+
results["track_files"].append(str(track_path))
|
|
222
|
+
self.console.print(
|
|
223
|
+
f"[red]✗ Detail failed: {scenario.name} | track: {track_path}[/red]"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
if results["failed"] == 0:
|
|
227
|
+
self.console.print(
|
|
228
|
+
f"[green]✓ Detail TestQL passed for {service} ({results['passed']} scenarios)[/green]"
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
return results
|
|
232
|
+
|
|
233
|
+
async def process_changed_file_once(self, file_path: str) -> Dict:
|
|
234
|
+
self.on_file_change(file_path)
|
|
235
|
+
|
|
236
|
+
processed = 0
|
|
237
|
+
while self.test_queue and processed < 4:
|
|
238
|
+
await self.process_test_queue_once()
|
|
239
|
+
processed += 1
|
|
240
|
+
await asyncio.sleep(0)
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
"file": file_path,
|
|
244
|
+
"processed_items": processed,
|
|
245
|
+
"remaining_queue": len(self.test_queue),
|
|
246
|
+
"last_track_path": str(self.last_track_path) if self.last_track_path else None,
|
|
247
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wup
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.10
|
|
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
|
|
@@ -28,17 +28,17 @@ Dynamic: license-file
|
|
|
28
28
|
|
|
29
29
|
## AI Cost Tracking
|
|
30
30
|
|
|
31
|
-
    
|
|
32
|
+
  
|
|
33
33
|
|
|
34
|
-
- 🤖 **LLM usage:** $0.
|
|
34
|
+
- 🤖 **LLM usage:** $0.6000 (4 commits)
|
|
35
35
|
- 👤 **Human dev:** ~$200 (2.0h @ $100/h, 30min dedup)
|
|
36
36
|
|
|
37
37
|
Generated on 2026-04-29 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
38
38
|
|
|
39
39
|
---
|
|
40
40
|
|
|
41
|
-
    
|
|
42
42
|
|
|
43
43
|
**WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
|
|
44
44
|
|
{wup-0.1.9 → wup-0.1.10}/LICENSE
RENAMED
|
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
|