wup 0.2.14__tar.gz → 0.2.15__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wup
3
- Version: 0.2.14
3
+ Version: 0.2.15
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: Apache-2.0
@@ -29,17 +29,17 @@ Dynamic: license-file
29
29
 
30
30
  ## AI Cost Tracking
31
31
 
32
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.14-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
33
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$2.25-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-3.2h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
32
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.15-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
33
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$2.40-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-4.1h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
34
34
 
35
- - 🤖 **LLM usage:** $2.2500 (15 commits)
36
- - 👤 **Human dev:** ~$324 (3.2h @ $100/h, 30min dedup)
35
+ - 🤖 **LLM usage:** $2.4000 (16 commits)
36
+ - 👤 **Human dev:** ~$411 (4.1h @ $100/h, 30min dedup)
37
37
 
38
38
  Generated on 2026-04-29 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
39
39
 
40
40
  ---
41
41
 
42
- ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.14-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
42
+ ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.15-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
43
43
 
44
44
  **WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
45
45
 
@@ -285,56 +285,86 @@ export WUP_CPU_THROTTLE=0.5
285
285
  export WUP_DEBOUNCE=3
286
286
  ```
287
287
 
288
- ## Integration with Test Frameworks
289
-
290
- WUP is designed to work with any test framework. The current implementation includes placeholder test methods that you can customize:
291
-
292
- ```python
293
- # In wup/core.py, customize these methods:
294
-
295
- async def run_quick_test(self, service: str, endpoints: List[str]) -> bool:
296
- # Integrate with your test framework (pytest, unittest, TestQL, etc.)
297
- # Example:
298
- result = subprocess.run([
299
- "pytest", f"tests/{service}/test_smoke.py",
300
- "--maxfail=1", "-q"
301
- ])
302
- return result.returncode == 0
303
-
304
- async def run_detail_test(self, service: str, endpoints: List[str]) -> Dict:
305
- # Run full test suite with blame reporting
306
- # Example:
307
- result = subprocess.run([
308
- "pytest", f"tests/{service}/",
309
- "--cov", f"app/{service}",
310
- "--cov-report=json"
311
- ])
312
- return parse_coverage_report("coverage.json")
288
+ ## Visual DOM Diff
289
+
290
+ WUP optionally scans configured pages with Playwright after each successful quick test, compares the DOM structure to the previous snapshot, and reports significant changes.
291
+
292
+ ### Setup
293
+
294
+ ```bash
295
+ pip install playwright
296
+ playwright install chromium
297
+ ```
298
+
299
+ ### Configuration
300
+
301
+ ```yaml
302
+ visual_diff:
303
+ enabled: true
304
+ base_url: "http://localhost:8100" # or leave empty and set WUP_BASE_URL env var
305
+ base_url_env: "WUP_BASE_URL"
306
+ delay_seconds: 5.0 # wait after file change before scanning
307
+ max_depth: 10 # DOM snapshot depth
308
+ pages:
309
+ - "/health"
310
+ - "/dashboard"
311
+ pages_from_endpoints: true # also scan endpoints from testql config
312
+ threshold_added: 3 # min node additions to report as "changed"
313
+ threshold_removed: 3
314
+ threshold_changed: 5
315
+ headless: true
316
+ ```
317
+
318
+ Or set the base URL in `.wup.env` in the project root (not committed to git):
319
+
320
+ ```bash
321
+ # .wup.env
322
+ WUP_BASE_URL=http://localhost:8100
313
323
  ```
314
324
 
325
+ ### Output
326
+
327
+ - **Snapshots** — `.wup/visual-snapshots/<service>/<page>.json`
328
+ - **Diff events** — `.wup/visual-diffs/<service>/<page>.jsonl` (appended on each change)
329
+
330
+ Visible in `wup status` as a "Visual DOM diffs" section.
331
+
332
+ ### Graceful degradation
333
+
334
+ If Playwright is not installed, the visual diff module logs a warning and skips scanning — it does **not** break the watcher.
335
+
315
336
  ## Project Structure
316
337
 
317
338
  ```
318
339
  wup/
319
340
  ├── wup/
320
- │ ├── __init__.py # Package exports
321
- │ ├── config.py # Configuration loader
322
- │ ├── models/
323
- ├── __init__.py # Models package
324
- │ └── config.py # Configuration dataclasses
325
- │ ├── core.py # WupWatcher implementation
326
- │ ├── dependency_mapper.py # Dependency mapping logic
327
- │ ├── testql_watcher.py # TestQL integration
328
- │ └── cli.py # CLI interface
341
+ │ ├── __init__.py # Package exports
342
+ │ ├── cli.py # CLI: watch, map-deps, status, init, testql-endpoints
343
+ │ ├── config.py # Config loading/saving + .wup.env support
344
+ │ ├── core.py # WupWatcher: detection, inference, scheduling
345
+ ├── dependency_mapper.py # DependencyMapper: codebase → deps.json
346
+ │ ├── testql_discovery.py # TestQLEndpointDiscovery: scenario parsing
347
+ │ ├── testql_watcher.py # TestQLWatcher: scenario runner + health tracking
348
+ │ ├── visual_diff.py # VisualDiffer: Playwright DOM snapshot + diff engine
349
+ │ └── models/
350
+ │ ├── __init__.py
351
+ │ └── config.py # Dataclasses: WupConfig, VisualDiffConfig, TestQLConfig...
329
352
  ├── tests/
330
- └── test_wup.py # Unit tests
353
+ ├── test_wup.py # unit/integration tests (incl. VisualDiffer, config)
354
+ │ ├── test_testql_watcher.py # TestQLWatcher + VisualDiffer integration tests
355
+ │ └── test_e2e.py # end-to-end CLI tests
356
+ ├── examples/
357
+ │ ├── fastapi-app/ # FastAPI example project
358
+ │ ├── flask-app/ # Flask example project
359
+ │ ├── multi-service/ # Multi-service example
360
+ │ ├── testql_demo.py # TestQL simulation demo
361
+ │ ├── testql_integration.py # Custom TestQLWatcher + visual diff example
362
+ │ └── visual_diff_demo.py # Visual DOM diff demo (no Playwright required)
331
363
  ├── docs/
332
- ├── 2.md # Refactoring documentation
333
- ├── 3.md # Configuration plan
334
- │ └── TESTQL_INTEGRATION.md # TestQL integration docs
335
- ├── wup.yaml.example # Example configuration
336
- ├── pyproject.toml # Package configuration
337
- └── README.md # This file
364
+ └── TESTQL_INTEGRATION.md # TestQL integration guide
365
+ ├── testql-scenarios/ # Auto-generated TestQL scenarios
366
+ ├── pyproject.toml # Package config (setuptools)
367
+ └── README.md
338
368
  ```
339
369
 
340
370
  ## Development
@@ -343,23 +373,33 @@ wup/
343
373
 
344
374
  ```bash
345
375
  # Run all tests
346
- pytest
376
+ python3 -m pytest tests/ -v
377
+
378
+ # Run specific suite
379
+ python3 -m pytest tests/test_wup.py -v
380
+ python3 -m pytest tests/test_testql_watcher.py -v
347
381
 
348
382
  # Run with coverage
349
- pytest --cov=wup
383
+ python3 -m pytest tests/ --cov=wup
384
+ ```
385
+
386
+ ### Examples
387
+
388
+ ```bash
389
+ # Visual diff demo (no Playwright required)
390
+ python3 examples/visual_diff_demo.py
391
+
392
+ # With live page scan (requires playwright)
393
+ python3 examples/visual_diff_demo.py http://localhost:8100/health
350
394
 
351
- # Run specific test
352
- pytest tests/test_wup.py::TestDependencyMapper::test_init
395
+ # TestQL + visual diff integration
396
+ python3 examples/testql_integration.py /path/to/project
353
397
  ```
354
398
 
355
- ### Building for Distribution
399
+ ### Building & Publishing
356
400
 
357
401
  ```bash
358
- # Build wheel and source distribution
359
402
  python -m build
360
-
361
- # Install from dist
362
- pip install dist/wup-0.1.6-py3-none-any.whl
363
403
  ```
364
404
 
365
405
  ## License
@@ -3,17 +3,17 @@
3
3
 
4
4
  ## AI Cost Tracking
5
5
 
6
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.14-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$2.25-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-3.2h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
6
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.15-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$2.40-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-4.1h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
8
8
 
9
- - 🤖 **LLM usage:** $2.2500 (15 commits)
10
- - 👤 **Human dev:** ~$324 (3.2h @ $100/h, 30min dedup)
9
+ - 🤖 **LLM usage:** $2.4000 (16 commits)
10
+ - 👤 **Human dev:** ~$411 (4.1h @ $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
- ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.14-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
16
+ ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.15-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
17
17
 
18
18
  **WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
19
19
 
@@ -259,56 +259,86 @@ export WUP_CPU_THROTTLE=0.5
259
259
  export WUP_DEBOUNCE=3
260
260
  ```
261
261
 
262
- ## Integration with Test Frameworks
263
-
264
- WUP is designed to work with any test framework. The current implementation includes placeholder test methods that you can customize:
265
-
266
- ```python
267
- # In wup/core.py, customize these methods:
268
-
269
- async def run_quick_test(self, service: str, endpoints: List[str]) -> bool:
270
- # Integrate with your test framework (pytest, unittest, TestQL, etc.)
271
- # Example:
272
- result = subprocess.run([
273
- "pytest", f"tests/{service}/test_smoke.py",
274
- "--maxfail=1", "-q"
275
- ])
276
- return result.returncode == 0
277
-
278
- async def run_detail_test(self, service: str, endpoints: List[str]) -> Dict:
279
- # Run full test suite with blame reporting
280
- # Example:
281
- result = subprocess.run([
282
- "pytest", f"tests/{service}/",
283
- "--cov", f"app/{service}",
284
- "--cov-report=json"
285
- ])
286
- return parse_coverage_report("coverage.json")
262
+ ## Visual DOM Diff
263
+
264
+ WUP optionally scans configured pages with Playwright after each successful quick test, compares the DOM structure to the previous snapshot, and reports significant changes.
265
+
266
+ ### Setup
267
+
268
+ ```bash
269
+ pip install playwright
270
+ playwright install chromium
271
+ ```
272
+
273
+ ### Configuration
274
+
275
+ ```yaml
276
+ visual_diff:
277
+ enabled: true
278
+ base_url: "http://localhost:8100" # or leave empty and set WUP_BASE_URL env var
279
+ base_url_env: "WUP_BASE_URL"
280
+ delay_seconds: 5.0 # wait after file change before scanning
281
+ max_depth: 10 # DOM snapshot depth
282
+ pages:
283
+ - "/health"
284
+ - "/dashboard"
285
+ pages_from_endpoints: true # also scan endpoints from testql config
286
+ threshold_added: 3 # min node additions to report as "changed"
287
+ threshold_removed: 3
288
+ threshold_changed: 5
289
+ headless: true
290
+ ```
291
+
292
+ Or set the base URL in `.wup.env` in the project root (not committed to git):
293
+
294
+ ```bash
295
+ # .wup.env
296
+ WUP_BASE_URL=http://localhost:8100
287
297
  ```
288
298
 
299
+ ### Output
300
+
301
+ - **Snapshots** — `.wup/visual-snapshots/<service>/<page>.json`
302
+ - **Diff events** — `.wup/visual-diffs/<service>/<page>.jsonl` (appended on each change)
303
+
304
+ Visible in `wup status` as a "Visual DOM diffs" section.
305
+
306
+ ### Graceful degradation
307
+
308
+ If Playwright is not installed, the visual diff module logs a warning and skips scanning — it does **not** break the watcher.
309
+
289
310
  ## Project Structure
290
311
 
291
312
  ```
292
313
  wup/
293
314
  ├── wup/
294
- │ ├── __init__.py # Package exports
295
- │ ├── config.py # Configuration loader
296
- │ ├── models/
297
- ├── __init__.py # Models package
298
- │ └── config.py # Configuration dataclasses
299
- │ ├── core.py # WupWatcher implementation
300
- │ ├── dependency_mapper.py # Dependency mapping logic
301
- │ ├── testql_watcher.py # TestQL integration
302
- │ └── cli.py # CLI interface
315
+ │ ├── __init__.py # Package exports
316
+ │ ├── cli.py # CLI: watch, map-deps, status, init, testql-endpoints
317
+ │ ├── config.py # Config loading/saving + .wup.env support
318
+ │ ├── core.py # WupWatcher: detection, inference, scheduling
319
+ ├── dependency_mapper.py # DependencyMapper: codebase → deps.json
320
+ │ ├── testql_discovery.py # TestQLEndpointDiscovery: scenario parsing
321
+ │ ├── testql_watcher.py # TestQLWatcher: scenario runner + health tracking
322
+ │ ├── visual_diff.py # VisualDiffer: Playwright DOM snapshot + diff engine
323
+ │ └── models/
324
+ │ ├── __init__.py
325
+ │ └── config.py # Dataclasses: WupConfig, VisualDiffConfig, TestQLConfig...
303
326
  ├── tests/
304
- └── test_wup.py # Unit tests
327
+ ├── test_wup.py # unit/integration tests (incl. VisualDiffer, config)
328
+ │ ├── test_testql_watcher.py # TestQLWatcher + VisualDiffer integration tests
329
+ │ └── test_e2e.py # end-to-end CLI tests
330
+ ├── examples/
331
+ │ ├── fastapi-app/ # FastAPI example project
332
+ │ ├── flask-app/ # Flask example project
333
+ │ ├── multi-service/ # Multi-service example
334
+ │ ├── testql_demo.py # TestQL simulation demo
335
+ │ ├── testql_integration.py # Custom TestQLWatcher + visual diff example
336
+ │ └── visual_diff_demo.py # Visual DOM diff demo (no Playwright required)
305
337
  ├── docs/
306
- ├── 2.md # Refactoring documentation
307
- ├── 3.md # Configuration plan
308
- │ └── TESTQL_INTEGRATION.md # TestQL integration docs
309
- ├── wup.yaml.example # Example configuration
310
- ├── pyproject.toml # Package configuration
311
- └── README.md # This file
338
+ └── TESTQL_INTEGRATION.md # TestQL integration guide
339
+ ├── testql-scenarios/ # Auto-generated TestQL scenarios
340
+ ├── pyproject.toml # Package config (setuptools)
341
+ └── README.md
312
342
  ```
313
343
 
314
344
  ## Development
@@ -317,23 +347,33 @@ wup/
317
347
 
318
348
  ```bash
319
349
  # Run all tests
320
- pytest
350
+ python3 -m pytest tests/ -v
351
+
352
+ # Run specific suite
353
+ python3 -m pytest tests/test_wup.py -v
354
+ python3 -m pytest tests/test_testql_watcher.py -v
321
355
 
322
356
  # Run with coverage
323
- pytest --cov=wup
357
+ python3 -m pytest tests/ --cov=wup
358
+ ```
359
+
360
+ ### Examples
361
+
362
+ ```bash
363
+ # Visual diff demo (no Playwright required)
364
+ python3 examples/visual_diff_demo.py
365
+
366
+ # With live page scan (requires playwright)
367
+ python3 examples/visual_diff_demo.py http://localhost:8100/health
324
368
 
325
- # Run specific test
326
- pytest tests/test_wup.py::TestDependencyMapper::test_init
369
+ # TestQL + visual diff integration
370
+ python3 examples/testql_integration.py /path/to/project
327
371
  ```
328
372
 
329
- ### Building for Distribution
373
+ ### Building & Publishing
330
374
 
331
375
  ```bash
332
- # Build wheel and source distribution
333
376
  python -m build
334
-
335
- # Install from dist
336
- pip install dist/wup-0.1.6-py3-none-any.whl
337
377
  ```
338
378
 
339
379
  ## License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "wup"
7
- version = "0.2.14"
7
+ version = "0.2.15"
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.9"
@@ -6,7 +6,7 @@ from pathlib import Path
6
6
  from subprocess import CompletedProcess
7
7
 
8
8
  from wup.testql_watcher import TestQLWatcher
9
- from wup.models.config import WupConfig, ProjectConfig, TestQLConfig
9
+ from wup.models.config import WupConfig, ProjectConfig, TestQLConfig, VisualDiffConfig
10
10
 
11
11
 
12
12
  def test_process_changed_file_creates_track_on_failure():
@@ -199,3 +199,57 @@ def test_service_health_transitions_are_persisted():
199
199
  statuses = [event.get("status") for event in events if event.get("service") == "connect-config"]
200
200
  assert "down" in statuses
201
201
  assert "up" in statuses
202
+
203
+
204
+ def test_visual_differ_disabled_by_default():
205
+ """visual_differ exists but is disabled (no-op) when visual_diff.enabled=False."""
206
+ with tempfile.TemporaryDirectory() as tmpdir:
207
+ root = Path(tmpdir)
208
+ cfg = WupConfig(
209
+ project=ProjectConfig(name="demo"),
210
+ testql=TestQLConfig(scenario_dir="testql-scenarios"),
211
+ visual_diff=VisualDiffConfig(enabled=False),
212
+ )
213
+
214
+ watcher = TestQLWatcher(
215
+ project_root=str(root),
216
+ deps_file=str(root / "deps.json"),
217
+ scenarios_dir="testql-scenarios",
218
+ track_dir=".wup/tracks",
219
+ config=cfg,
220
+ )
221
+
222
+ # Differ is created but flagged disabled — run_for_service() must be a no-op
223
+ assert watcher.visual_differ is not None
224
+ assert watcher.visual_differ.cfg.enabled is False
225
+ results = asyncio.run(watcher.visual_differ.run_for_service("svc", ["/x"]))
226
+ assert results == []
227
+
228
+
229
+ def test_visual_differ_initialized_when_enabled():
230
+ """When visual_diff.enabled=True, TestQLWatcher.visual_differ is a VisualDiffer."""
231
+ from wup.visual_diff import VisualDiffer
232
+
233
+ with tempfile.TemporaryDirectory() as tmpdir:
234
+ root = Path(tmpdir)
235
+ cfg = WupConfig(
236
+ project=ProjectConfig(name="demo"),
237
+ testql=TestQLConfig(scenario_dir="testql-scenarios"),
238
+ visual_diff=VisualDiffConfig(
239
+ enabled=True,
240
+ base_url="http://localhost:9000",
241
+ pages=["/dashboard"],
242
+ ),
243
+ )
244
+
245
+ watcher = TestQLWatcher(
246
+ project_root=str(root),
247
+ deps_file=str(root / "deps.json"),
248
+ scenarios_dir="testql-scenarios",
249
+ track_dir=".wup/tracks",
250
+ config=cfg,
251
+ )
252
+
253
+ assert isinstance(watcher.visual_differ, VisualDiffer)
254
+ assert watcher.visual_differ.cfg.enabled is True
255
+ assert watcher.visual_differ.base_url == "http://localhost:9000"
@@ -18,8 +18,10 @@ from wup.models.config import (
18
18
  NotifyConfig,
19
19
  ServiceTestConfig,
20
20
  ProjectConfig,
21
+ VisualDiffConfig,
21
22
  )
22
23
  from wup.testql_watcher import TestQLWatcher
24
+ from wup.visual_diff import VisualDiffer, _diff_snapshots, _page_slug, _resolve_base_url
23
25
 
24
26
 
25
27
  class TestDependencyMapper:
@@ -921,6 +923,162 @@ class TestConfigModels:
921
923
  assert config.project.name == "test"
922
924
  assert len(config.services) == 1
923
925
  assert config.services[0].name == "users"
926
+ # visual_diff is auto-populated with defaults
927
+ assert isinstance(config.visual_diff, VisualDiffConfig)
928
+ assert config.visual_diff.enabled is False
929
+
930
+ def test_visual_diff_config_defaults(self):
931
+ """Test VisualDiffConfig has sensible defaults."""
932
+ cfg = VisualDiffConfig()
933
+ assert cfg.enabled is False
934
+ assert cfg.base_url == ""
935
+ assert cfg.base_url_env == "WUP_BASE_URL"
936
+ assert cfg.delay_seconds == 5.0
937
+ assert cfg.max_depth == 10
938
+ assert cfg.snapshot_dir == ".wup/visual-snapshots"
939
+ assert cfg.diff_dir == ".wup/visual-diffs"
940
+ assert cfg.pages == []
941
+ assert cfg.pages_from_endpoints is True
942
+ assert cfg.headless is True
943
+
944
+ def test_visual_diff_config_custom(self):
945
+ """Test VisualDiffConfig with custom values."""
946
+ cfg = VisualDiffConfig(
947
+ enabled=True,
948
+ base_url="http://localhost:9000",
949
+ pages=["/dashboard", "/users"],
950
+ threshold_added=10,
951
+ )
952
+ assert cfg.enabled is True
953
+ assert cfg.base_url == "http://localhost:9000"
954
+ assert cfg.pages == ["/dashboard", "/users"]
955
+ assert cfg.threshold_added == 10
956
+
957
+
958
+ class TestVisualDiffer:
959
+ """Tests for the VisualDiffer class (no Playwright required)."""
960
+
961
+ def test_resolve_base_url_from_config(self):
962
+ cfg = VisualDiffConfig(base_url="http://localhost:8080/")
963
+ assert _resolve_base_url(cfg) == "http://localhost:8080"
964
+
965
+ def test_resolve_base_url_from_env(self, monkeypatch):
966
+ cfg = VisualDiffConfig(base_url="", base_url_env="MY_VD_URL")
967
+ monkeypatch.setenv("MY_VD_URL", "http://env-host:1234/")
968
+ assert _resolve_base_url(cfg) == "http://env-host:1234"
969
+
970
+ def test_resolve_base_url_empty(self, monkeypatch):
971
+ cfg = VisualDiffConfig(base_url="", base_url_env="WUP_VD_NONE")
972
+ monkeypatch.delenv("WUP_VD_NONE", raising=False)
973
+ assert _resolve_base_url(cfg) == ""
974
+
975
+ def test_page_slug(self):
976
+ assert _page_slug("http://x/api/v1/users") == "api_v1_users"
977
+ assert _page_slug("http://x/") == "root"
978
+
979
+ def test_pages_for_service_explicit(self):
980
+ with tempfile.TemporaryDirectory() as tmpdir:
981
+ cfg = VisualDiffConfig(
982
+ base_url="http://localhost:8080",
983
+ pages=["/dashboard"],
984
+ pages_from_endpoints=False,
985
+ )
986
+ differ = VisualDiffer(tmpdir, cfg)
987
+ pages = differ._pages_for_service("users", ["/api/users"])
988
+ assert pages == ["http://localhost:8080/dashboard"]
989
+
990
+ def test_pages_for_service_from_endpoints(self):
991
+ with tempfile.TemporaryDirectory() as tmpdir:
992
+ cfg = VisualDiffConfig(
993
+ base_url="http://localhost:8080",
994
+ pages_from_endpoints=True,
995
+ )
996
+ differ = VisualDiffer(tmpdir, cfg)
997
+ pages = differ._pages_for_service("users", ["/api/users", "/api/users/1"])
998
+ assert "http://localhost:8080/api/users" in pages
999
+ assert "http://localhost:8080/api/users/1" in pages
1000
+
1001
+ def test_pages_for_service_fallback(self):
1002
+ with tempfile.TemporaryDirectory() as tmpdir:
1003
+ cfg = VisualDiffConfig(
1004
+ base_url="http://localhost:8080",
1005
+ pages_from_endpoints=False,
1006
+ )
1007
+ differ = VisualDiffer(tmpdir, cfg)
1008
+ pages = differ._pages_for_service("users", [])
1009
+ assert pages == ["http://localhost:8080/users"]
1010
+
1011
+ def test_pages_for_service_absolute_url_passthrough(self):
1012
+ with tempfile.TemporaryDirectory() as tmpdir:
1013
+ cfg = VisualDiffConfig(
1014
+ base_url="http://localhost:8080",
1015
+ pages=["https://example.com/health"],
1016
+ pages_from_endpoints=False,
1017
+ )
1018
+ differ = VisualDiffer(tmpdir, cfg)
1019
+ pages = differ._pages_for_service("svc", [])
1020
+ assert pages == ["https://example.com/health"]
1021
+
1022
+ def test_diff_snapshots_baseline(self):
1023
+ new = {"tag": "HTML", "children": [{"tag": "BODY"}]}
1024
+ diff = _diff_snapshots(None, new, max_depth=5,
1025
+ threshold_added=1, threshold_removed=1, threshold_changed=1)
1026
+ assert diff["status"] == "new"
1027
+
1028
+ def test_diff_snapshots_identical(self):
1029
+ snap = {"tag": "HTML", "children": [{"tag": "BODY", "id": "main"}]}
1030
+ diff = _diff_snapshots(snap, snap, max_depth=5,
1031
+ threshold_added=1, threshold_removed=1, threshold_changed=1)
1032
+ assert diff["status"] == "ok"
1033
+ assert diff["counts"]["added"] == 0
1034
+ assert diff["counts"]["removed"] == 0
1035
+
1036
+ def test_diff_snapshots_changed(self):
1037
+ old = {"tag": "HTML", "children": [{"tag": "BODY"}]}
1038
+ new = {"tag": "HTML", "children": [
1039
+ {"tag": "BODY"},
1040
+ {"tag": "DIV", "id": "added1"},
1041
+ {"tag": "DIV", "id": "added2"},
1042
+ {"tag": "DIV", "id": "added3"},
1043
+ ]}
1044
+ diff = _diff_snapshots(old, new, max_depth=5,
1045
+ threshold_added=3, threshold_removed=10, threshold_changed=10)
1046
+ assert diff["status"] == "changed"
1047
+ assert diff["counts"]["added"] >= 3
1048
+
1049
+ def test_run_for_service_disabled_returns_empty(self):
1050
+ """When disabled, run_for_service should be a no-op."""
1051
+ import asyncio
1052
+ with tempfile.TemporaryDirectory() as tmpdir:
1053
+ cfg = VisualDiffConfig(enabled=False)
1054
+ differ = VisualDiffer(tmpdir, cfg)
1055
+ results = asyncio.run(differ.run_for_service("svc", ["/x"]))
1056
+ assert results == []
1057
+
1058
+ def test_get_recent_diffs_empty(self):
1059
+ with tempfile.TemporaryDirectory() as tmpdir:
1060
+ cfg = VisualDiffConfig()
1061
+ differ = VisualDiffer(tmpdir, cfg)
1062
+ assert differ.get_recent_diffs() == []
1063
+
1064
+ def test_get_recent_diffs_filters_by_age(self):
1065
+ import time
1066
+ with tempfile.TemporaryDirectory() as tmpdir:
1067
+ cfg = VisualDiffConfig(diff_dir=".wup/visual-diffs")
1068
+ differ = VisualDiffer(tmpdir, cfg)
1069
+ now = int(time.time())
1070
+ diff_file = differ.diff_dir / "svc" / "page.jsonl"
1071
+ diff_file.parent.mkdir(parents=True, exist_ok=True)
1072
+ diff_file.write_text(
1073
+ json.dumps({"timestamp": now, "service": "svc", "url": "/x",
1074
+ "diff": {"status": "changed"}}) + "\n"
1075
+ + json.dumps({"timestamp": now - 9999, "service": "svc", "url": "/y",
1076
+ "diff": {"status": "changed"}}) + "\n",
1077
+ encoding="utf-8",
1078
+ )
1079
+ recent = differ.get_recent_diffs(seconds=300)
1080
+ assert len(recent) == 1
1081
+ assert recent[0]["url"] == "/x"
924
1082
 
925
1083
 
926
1084
  class TestConfigLoader:
@@ -1073,6 +1231,126 @@ project:
1073
1231
  with pytest.raises(ValueError, match="project.name"):
1074
1232
  load_config(Path(tmpdir), config_path)
1075
1233
 
1234
+ def test_save_and_load_visual_diff_config(self):
1235
+ """Test that visual_diff section is correctly saved and reloaded."""
1236
+ with tempfile.TemporaryDirectory() as tmpdir:
1237
+ config = WupConfig(
1238
+ project=ProjectConfig(name="vd-test"),
1239
+ visual_diff=VisualDiffConfig(
1240
+ enabled=True,
1241
+ base_url="http://localhost:9000",
1242
+ pages=["/health", "/users"],
1243
+ delay_seconds=2.5,
1244
+ max_depth=6,
1245
+ threshold_added=7,
1246
+ threshold_removed=4,
1247
+ threshold_changed=8,
1248
+ headless=False,
1249
+ ),
1250
+ )
1251
+ config_path = Path(tmpdir) / "wup.yaml"
1252
+ save_config(config, config_path)
1253
+
1254
+ loaded = load_config(Path(tmpdir), config_path)
1255
+ vd = loaded.visual_diff
1256
+ assert vd.enabled is True
1257
+ assert vd.base_url == "http://localhost:9000"
1258
+ assert vd.pages == ["/health", "/users"]
1259
+ assert vd.delay_seconds == 2.5
1260
+ assert vd.max_depth == 6
1261
+ assert vd.threshold_added == 7
1262
+ assert vd.threshold_removed == 4
1263
+ assert vd.threshold_changed == 8
1264
+ assert vd.headless is False
1265
+
1266
+ def test_load_config_visual_diff_from_yaml(self):
1267
+ """Test parsing visual_diff section from raw YAML."""
1268
+ with tempfile.TemporaryDirectory() as tmpdir:
1269
+ yaml_content = """
1270
+ project:
1271
+ name: "my-app"
1272
+ visual_diff:
1273
+ enabled: true
1274
+ base_url: "http://localhost:8100"
1275
+ base_url_env: "MY_BASE_URL"
1276
+ delay_seconds: 3.0
1277
+ max_depth: 8
1278
+ snapshot_dir: ".wup/snaps"
1279
+ diff_dir: ".wup/diffs"
1280
+ pages:
1281
+ - "/dashboard"
1282
+ - "/settings"
1283
+ pages_from_endpoints: false
1284
+ threshold_added: 5
1285
+ threshold_removed: 2
1286
+ threshold_changed: 9
1287
+ headless: false
1288
+ """
1289
+ config_path = Path(tmpdir) / "wup.yaml"
1290
+ config_path.write_text(yaml_content)
1291
+ config = load_config(Path(tmpdir), config_path)
1292
+ vd = config.visual_diff
1293
+ assert vd.enabled is True
1294
+ assert vd.base_url == "http://localhost:8100"
1295
+ assert vd.base_url_env == "MY_BASE_URL"
1296
+ assert vd.delay_seconds == 3.0
1297
+ assert vd.max_depth == 8
1298
+ assert vd.snapshot_dir == ".wup/snaps"
1299
+ assert vd.diff_dir == ".wup/diffs"
1300
+ assert vd.pages == ["/dashboard", "/settings"]
1301
+ assert vd.pages_from_endpoints is False
1302
+ assert vd.threshold_added == 5
1303
+ assert vd.threshold_removed == 2
1304
+ assert vd.threshold_changed == 9
1305
+ assert vd.headless is False
1306
+
1307
+ def test_load_config_visual_diff_defaults_when_section_absent(self):
1308
+ """visual_diff section absent → all defaults used."""
1309
+ with tempfile.TemporaryDirectory() as tmpdir:
1310
+ config_path = Path(tmpdir) / "wup.yaml"
1311
+ config_path.write_text("project:\n name: x\n")
1312
+ config = load_config(Path(tmpdir), config_path)
1313
+ vd = config.visual_diff
1314
+ assert vd.enabled is False
1315
+ assert vd.base_url == ""
1316
+ assert vd.max_depth == 10
1317
+ assert vd.pages == []
1318
+ assert vd.headless is True
1319
+
1320
+ def test_load_dotenv_sets_env_var(self):
1321
+ """_load_dotenv should load .wup.env into os.environ."""
1322
+ import os
1323
+ from wup.config import _load_dotenv
1324
+
1325
+ with tempfile.TemporaryDirectory() as tmpdir:
1326
+ root = Path(tmpdir)
1327
+ env_file = root / ".wup.env"
1328
+ env_file.write_text('WUP_TEST_DUMMY_VAR=hello_wup\n# comment\n', encoding="utf-8")
1329
+ existing = os.environ.pop("WUP_TEST_DUMMY_VAR", None)
1330
+ try:
1331
+ _load_dotenv(root)
1332
+ assert os.environ.get("WUP_TEST_DUMMY_VAR") == "hello_wup"
1333
+ finally:
1334
+ os.environ.pop("WUP_TEST_DUMMY_VAR", None)
1335
+ if existing is not None:
1336
+ os.environ["WUP_TEST_DUMMY_VAR"] = existing
1337
+
1338
+ def test_load_dotenv_does_not_overwrite_existing(self):
1339
+ """_load_dotenv must NOT overwrite already-set env vars."""
1340
+ import os
1341
+ from wup.config import _load_dotenv
1342
+
1343
+ with tempfile.TemporaryDirectory() as tmpdir:
1344
+ root = Path(tmpdir)
1345
+ env_file = root / ".wup.env"
1346
+ env_file.write_text('WUP_TEST_NOOVERWRITE=from_file\n', encoding="utf-8")
1347
+ os.environ["WUP_TEST_NOOVERWRITE"] = "original"
1348
+ try:
1349
+ _load_dotenv(root)
1350
+ assert os.environ["WUP_TEST_NOOVERWRITE"] == "original"
1351
+ finally:
1352
+ os.environ.pop("WUP_TEST_NOOVERWRITE", None)
1353
+
1076
1354
 
1077
1355
  class TestConfigIntegration:
1078
1356
  """Tests for configuration integration with WupWatcher."""
@@ -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.14"
10
+ __version__ = "0.2.15"
11
11
  __author__ = "Tom Sapletta"
12
12
 
13
13
  from .config import load_config, save_config, get_default_config
@@ -183,12 +183,31 @@ def validate_config(raw: dict) -> WupConfig:
183
183
 
184
184
  # Parse visual_diff config
185
185
  vd_raw = raw.get("visual_diff", {})
186
+ env_visual_enabled = os.environ.get("WUP_VISUAL_DIFF_ENABLED")
187
+ env_visual_delay = os.environ.get("WUP_VISUAL_DIFF_DELAY_SECONDS")
188
+ env_visual_depth = os.environ.get("WUP_VISUAL_DIFF_MAX_DEPTH")
189
+
190
+ if env_visual_enabled is None:
191
+ visual_enabled = vd_raw.get("enabled", False)
192
+ else:
193
+ visual_enabled = env_visual_enabled.strip().lower() in {"1", "true", "yes", "on"}
194
+
195
+ if env_visual_delay is None:
196
+ visual_delay = float(vd_raw.get("delay_seconds", 5.0))
197
+ else:
198
+ visual_delay = float(env_visual_delay)
199
+
200
+ if env_visual_depth is None:
201
+ visual_depth = int(vd_raw.get("max_depth", 10))
202
+ else:
203
+ visual_depth = int(env_visual_depth)
204
+
186
205
  visual_diff = VisualDiffConfig(
187
- enabled=vd_raw.get("enabled", False),
206
+ enabled=visual_enabled,
188
207
  base_url=vd_raw.get("base_url", ""),
189
208
  base_url_env=vd_raw.get("base_url_env", "WUP_BASE_URL"),
190
- delay_seconds=float(vd_raw.get("delay_seconds", 5.0)),
191
- max_depth=int(vd_raw.get("max_depth", 10)),
209
+ delay_seconds=visual_delay,
210
+ max_depth=visual_depth,
192
211
  snapshot_dir=vd_raw.get("snapshot_dir", ".wup/visual-snapshots"),
193
212
  diff_dir=vd_raw.get("diff_dir", ".wup/visual-diffs"),
194
213
  pages=vd_raw.get("pages", []),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wup
3
- Version: 0.2.14
3
+ Version: 0.2.15
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: Apache-2.0
@@ -29,17 +29,17 @@ Dynamic: license-file
29
29
 
30
30
  ## AI Cost Tracking
31
31
 
32
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.14-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
33
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$2.25-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-3.2h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
32
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.15-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
33
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$2.40-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-4.1h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
34
34
 
35
- - 🤖 **LLM usage:** $2.2500 (15 commits)
36
- - 👤 **Human dev:** ~$324 (3.2h @ $100/h, 30min dedup)
35
+ - 🤖 **LLM usage:** $2.4000 (16 commits)
36
+ - 👤 **Human dev:** ~$411 (4.1h @ $100/h, 30min dedup)
37
37
 
38
38
  Generated on 2026-04-29 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
39
39
 
40
40
  ---
41
41
 
42
- ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.14-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
42
+ ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.15-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
43
43
 
44
44
  **WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
45
45
 
@@ -285,56 +285,86 @@ export WUP_CPU_THROTTLE=0.5
285
285
  export WUP_DEBOUNCE=3
286
286
  ```
287
287
 
288
- ## Integration with Test Frameworks
289
-
290
- WUP is designed to work with any test framework. The current implementation includes placeholder test methods that you can customize:
291
-
292
- ```python
293
- # In wup/core.py, customize these methods:
294
-
295
- async def run_quick_test(self, service: str, endpoints: List[str]) -> bool:
296
- # Integrate with your test framework (pytest, unittest, TestQL, etc.)
297
- # Example:
298
- result = subprocess.run([
299
- "pytest", f"tests/{service}/test_smoke.py",
300
- "--maxfail=1", "-q"
301
- ])
302
- return result.returncode == 0
303
-
304
- async def run_detail_test(self, service: str, endpoints: List[str]) -> Dict:
305
- # Run full test suite with blame reporting
306
- # Example:
307
- result = subprocess.run([
308
- "pytest", f"tests/{service}/",
309
- "--cov", f"app/{service}",
310
- "--cov-report=json"
311
- ])
312
- return parse_coverage_report("coverage.json")
288
+ ## Visual DOM Diff
289
+
290
+ WUP optionally scans configured pages with Playwright after each successful quick test, compares the DOM structure to the previous snapshot, and reports significant changes.
291
+
292
+ ### Setup
293
+
294
+ ```bash
295
+ pip install playwright
296
+ playwright install chromium
297
+ ```
298
+
299
+ ### Configuration
300
+
301
+ ```yaml
302
+ visual_diff:
303
+ enabled: true
304
+ base_url: "http://localhost:8100" # or leave empty and set WUP_BASE_URL env var
305
+ base_url_env: "WUP_BASE_URL"
306
+ delay_seconds: 5.0 # wait after file change before scanning
307
+ max_depth: 10 # DOM snapshot depth
308
+ pages:
309
+ - "/health"
310
+ - "/dashboard"
311
+ pages_from_endpoints: true # also scan endpoints from testql config
312
+ threshold_added: 3 # min node additions to report as "changed"
313
+ threshold_removed: 3
314
+ threshold_changed: 5
315
+ headless: true
316
+ ```
317
+
318
+ Or set the base URL in `.wup.env` in the project root (not committed to git):
319
+
320
+ ```bash
321
+ # .wup.env
322
+ WUP_BASE_URL=http://localhost:8100
313
323
  ```
314
324
 
325
+ ### Output
326
+
327
+ - **Snapshots** — `.wup/visual-snapshots/<service>/<page>.json`
328
+ - **Diff events** — `.wup/visual-diffs/<service>/<page>.jsonl` (appended on each change)
329
+
330
+ Visible in `wup status` as a "Visual DOM diffs" section.
331
+
332
+ ### Graceful degradation
333
+
334
+ If Playwright is not installed, the visual diff module logs a warning and skips scanning — it does **not** break the watcher.
335
+
315
336
  ## Project Structure
316
337
 
317
338
  ```
318
339
  wup/
319
340
  ├── wup/
320
- │ ├── __init__.py # Package exports
321
- │ ├── config.py # Configuration loader
322
- │ ├── models/
323
- ├── __init__.py # Models package
324
- │ └── config.py # Configuration dataclasses
325
- │ ├── core.py # WupWatcher implementation
326
- │ ├── dependency_mapper.py # Dependency mapping logic
327
- │ ├── testql_watcher.py # TestQL integration
328
- │ └── cli.py # CLI interface
341
+ │ ├── __init__.py # Package exports
342
+ │ ├── cli.py # CLI: watch, map-deps, status, init, testql-endpoints
343
+ │ ├── config.py # Config loading/saving + .wup.env support
344
+ │ ├── core.py # WupWatcher: detection, inference, scheduling
345
+ ├── dependency_mapper.py # DependencyMapper: codebase → deps.json
346
+ │ ├── testql_discovery.py # TestQLEndpointDiscovery: scenario parsing
347
+ │ ├── testql_watcher.py # TestQLWatcher: scenario runner + health tracking
348
+ │ ├── visual_diff.py # VisualDiffer: Playwright DOM snapshot + diff engine
349
+ │ └── models/
350
+ │ ├── __init__.py
351
+ │ └── config.py # Dataclasses: WupConfig, VisualDiffConfig, TestQLConfig...
329
352
  ├── tests/
330
- └── test_wup.py # Unit tests
353
+ ├── test_wup.py # unit/integration tests (incl. VisualDiffer, config)
354
+ │ ├── test_testql_watcher.py # TestQLWatcher + VisualDiffer integration tests
355
+ │ └── test_e2e.py # end-to-end CLI tests
356
+ ├── examples/
357
+ │ ├── fastapi-app/ # FastAPI example project
358
+ │ ├── flask-app/ # Flask example project
359
+ │ ├── multi-service/ # Multi-service example
360
+ │ ├── testql_demo.py # TestQL simulation demo
361
+ │ ├── testql_integration.py # Custom TestQLWatcher + visual diff example
362
+ │ └── visual_diff_demo.py # Visual DOM diff demo (no Playwright required)
331
363
  ├── docs/
332
- ├── 2.md # Refactoring documentation
333
- ├── 3.md # Configuration plan
334
- │ └── TESTQL_INTEGRATION.md # TestQL integration docs
335
- ├── wup.yaml.example # Example configuration
336
- ├── pyproject.toml # Package configuration
337
- └── README.md # This file
364
+ └── TESTQL_INTEGRATION.md # TestQL integration guide
365
+ ├── testql-scenarios/ # Auto-generated TestQL scenarios
366
+ ├── pyproject.toml # Package config (setuptools)
367
+ └── README.md
338
368
  ```
339
369
 
340
370
  ## Development
@@ -343,23 +373,33 @@ wup/
343
373
 
344
374
  ```bash
345
375
  # Run all tests
346
- pytest
376
+ python3 -m pytest tests/ -v
377
+
378
+ # Run specific suite
379
+ python3 -m pytest tests/test_wup.py -v
380
+ python3 -m pytest tests/test_testql_watcher.py -v
347
381
 
348
382
  # Run with coverage
349
- pytest --cov=wup
383
+ python3 -m pytest tests/ --cov=wup
384
+ ```
385
+
386
+ ### Examples
387
+
388
+ ```bash
389
+ # Visual diff demo (no Playwright required)
390
+ python3 examples/visual_diff_demo.py
391
+
392
+ # With live page scan (requires playwright)
393
+ python3 examples/visual_diff_demo.py http://localhost:8100/health
350
394
 
351
- # Run specific test
352
- pytest tests/test_wup.py::TestDependencyMapper::test_init
395
+ # TestQL + visual diff integration
396
+ python3 examples/testql_integration.py /path/to/project
353
397
  ```
354
398
 
355
- ### Building for Distribution
399
+ ### Building & Publishing
356
400
 
357
401
  ```bash
358
- # Build wheel and source distribution
359
402
  python -m build
360
-
361
- # Install from dist
362
- pip install dist/wup-0.1.6-py3-none-any.whl
363
403
  ```
364
404
 
365
405
  ## License
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