Compare commits
No commits in common. "eca9b10e62df015ad7a75d42407cde0db75d37ad" and "2586d458b2f8a1144f10c4f6bbc66cbe8ddc8bbc" have entirely different histories.
eca9b10e62
...
2586d458b2
@ -26,7 +26,7 @@ jobs:
|
||||
COVERAGE_PROCESS_START: ${{ github.workspace }}/.coveragerc
|
||||
PLAYWRIGHT_HEADLESS: "1"
|
||||
run: |
|
||||
python -m coverage run -m pytest tests/ -n auto --dist loadfile --tb=line
|
||||
python -m coverage run -m pytest tests/ -p no:xdist --tb=line
|
||||
|
||||
- name: Build coverage report
|
||||
if: always()
|
||||
|
||||
10
AGENTS.md
10
AGENTS.md
@ -6,10 +6,8 @@
|
||||
make install # pip install -e .
|
||||
make dev # uvicorn --reload on port 10500, backlog 4096
|
||||
make prod # uvicorn with 2 workers, backlog 8192 (production)
|
||||
make test # Playwright integration + unit tests, parallel via xdist (-n auto --dist loadfile), fail-fast -x
|
||||
make test-serial # same tests, single process (debugging)
|
||||
make test-headed # same tests in visible browsers, parallel (one window per worker)
|
||||
make test-headed-serial # visible browser, single process (one window)
|
||||
make test # Playwright integration + unit tests (fail-fast -x)
|
||||
make test-headed # same tests in visible browser
|
||||
make locust # Locust load test (interactive web UI)
|
||||
make locust-headless # Locust in headless CLI mode (for CI)
|
||||
```
|
||||
@ -23,7 +21,7 @@ make locust-headless # Locust in headless CLI mode (for CI)
|
||||
- **Auth:** `session` cookie, plus `X-API-KEY` / `Authorization: Bearer <api_key>` / HTTP Basic (username-or-email:password) - all resolved in `get_current_user`. PBKDF2-SHA256 via passlib. No JWT. Every user has an `api_key` (uuid7), set at signup and backfilled in `init_db`/`devplace apikey backfill`. Docs site at `/docs` (`routers/docs.py`, `DOCS_PAGES` registry; FastAPI's Swagger is disabled so `/docs` is free).
|
||||
- **Static:** `devplacepy/static/` mounted at `/static`
|
||||
- **Templates:** `devplacepy/templates/`. Shared `templates` instance from `devplacepy.templating` - all routers import from there, do NOT create their own.
|
||||
- **Ports:** 10500 (dev), 10501+ (tests; each xdist worker gets `10501 + worker index`)
|
||||
- **Ports:** 10500 (dev), 10501 (tests)
|
||||
- **Username:** letters, numbers, hyphens, underscores only. 3-32 chars.
|
||||
- **Password:** minimum 6 chars.
|
||||
- **Avatars:** Multiavatar-based (local SVG generation, zero network). URL: `/avatar/multiavatar/{seed}`. Generation takes <5ms. Fallback to initial-based SVG on error. Cache is in-memory (cleared on restart).
|
||||
@ -1353,7 +1351,7 @@ Failures at any step block the workflow. Never skip a failed step.
|
||||
|
||||
## CI/CD
|
||||
|
||||
Gitea Actions workflow at `.gitea/workflows/test.yaml` runs on every push/PR to `master`. It installs dependencies plus Playwright Chromium, runs the full test suite under coverage in parallel (`coverage run -m pytest -n auto --dist loadfile`; per-worker `.coverage.*` files combine via `.coveragerc` `parallel=true`), publishes the coverage HTML as an artifact on every run, and uploads failure screenshots when a test fails. The CI must be green before merging. Changes move through automated DTAP streets: Development (`make dev`), Test (the CI suite + coverage on `master`), Acceptance (the `master` to `production` promotion via `make deploy`), and Production (the Docker Compose stack). Only CI-green `master` commits are promoted to `production`.
|
||||
Gitea Actions workflow at `.gitea/workflows/test.yaml` runs on every push/PR to `master`. It installs dependencies plus Playwright Chromium, runs the full test suite under coverage (`coverage run -m pytest -p no:xdist`), publishes the coverage HTML as an artifact on every run, and uploads failure screenshots when a test fails. The CI must be green before merging. Changes move through automated DTAP streets: Development (`make dev`), Test (the CI suite + coverage on `master`), Acceptance (the `master` to `production` promotion via `make deploy`), and Production (the Docker Compose stack). Only CI-green `master` commits are promoted to `production`.
|
||||
|
||||
## SEO Implementation
|
||||
|
||||
|
||||
@ -13,10 +13,8 @@ make install # pip install -e .
|
||||
make ppy # build the single shared container image (ppy:latest); run once before launching instances
|
||||
make dev # uvicorn --reload on port 10500
|
||||
make prod # uvicorn 2 workers, port 10500 (backlog 8192)
|
||||
make test # Playwright + unit tests, headless, parallel (pytest-xdist -n auto, distributed per file), -x fail-fast
|
||||
make test-serial # same tests in a single process (no xdist) for debugging
|
||||
make test-headed # same tests in visible browsers, parallel (one Chromium window per worker)
|
||||
make test-headed-serial # visible browser, single process (one window) for debugging
|
||||
make test # Playwright + unit tests, headless, -x fail-fast
|
||||
make test-headed # same tests in visible browser
|
||||
make locust # Locust load test, interactive web UI
|
||||
make locust-headless # Locust CLI mode for CI
|
||||
```
|
||||
@ -46,7 +44,7 @@ devplace containers prune-builds # remove legacy per-project images + clear doc
|
||||
devplace containers gc-workspaces # remove workspace dirs with no instances
|
||||
```
|
||||
|
||||
Tests run on ports 10501+ (xdist worker offset) with a per-worker tempfile SQLite DB and a per-worker `DEVPLACE_DATA_DIR` (both keyed off `_xdist_port()`), and `DEVPLACE_DISABLE_SERVICES=1` so background services don't start. `make test` runs in parallel via pytest-xdist (`-n auto --dist loadfile`): each worker is one isolated DB + data dir + uvicorn subprocess + Chromium, and `loadfile` pins all tests in a file to one worker so session fixtures (`seeded_db`) and intra-file ordering hold. Override worker count with `make test TEST_WORKERS=4` (default `auto` = one per core); use `make test-serial` for single-process debugging.
|
||||
Tests run on ports 10501+ (xdist worker offset) with a tempfile SQLite DB and `DEVPLACE_DISABLE_SERVICES=1` so background services don't start.
|
||||
|
||||
## Environment variables
|
||||
|
||||
|
||||
16
Makefile
16
Makefile
@ -6,10 +6,8 @@ LOCUST_USERS ?= 20
|
||||
LOCUST_SPAWN_RATE ?= 5
|
||||
LOCUST_RUN_TIME ?= 120s
|
||||
DEVPLACE_RATE_LIMIT ?= 1000000
|
||||
TEST_WORKERS ?= auto
|
||||
TEST_DIST ?= loadfile
|
||||
|
||||
.PHONY: install dev clean test test-serial test-headed test-headed-serial coverage coverage-headed coverage-html locust locust-headless
|
||||
.PHONY: install dev clean test test-headed coverage coverage-headed coverage-html locust locust-headless
|
||||
|
||||
install:
|
||||
pip install -e .
|
||||
@ -21,21 +19,15 @@ prod:
|
||||
DEVPLACE_WEB_WORKERS=2 uvicorn devplacepy.main:app --host 0.0.0.0 --port 10500 --workers 2 --backlog 8192 --proxy-headers --forwarded-allow-ips '*'
|
||||
|
||||
test:
|
||||
PLAYWRIGHT_HEADLESS=1 python -m pytest tests/ -n $(TEST_WORKERS) --dist $(TEST_DIST) --tb=line -x
|
||||
|
||||
test-serial:
|
||||
PLAYWRIGHT_HEADLESS=1 python -m pytest tests/ -p no:xdist -v --tb=line -x
|
||||
PLAYWRIGHT_HEADLESS=1 python -m pytest tests/ -v --tb=line -x
|
||||
|
||||
test-headed:
|
||||
PLAYWRIGHT_HEADLESS=0 python -m pytest tests/ -n $(TEST_WORKERS) --dist $(TEST_DIST) --tb=line -x
|
||||
|
||||
test-headed-serial:
|
||||
PLAYWRIGHT_HEADLESS=0 python -m pytest tests/ -p no:xdist -v --tb=line -x
|
||||
PLAYWRIGHT_HEADLESS=0 python -m pytest tests/ -v --tb=line -x
|
||||
|
||||
coverage:
|
||||
rm -f .coverage .coverage.*
|
||||
COVERAGE_PROCESS_START=$(CURDIR)/.coveragerc PLAYWRIGHT_HEADLESS=1 \
|
||||
python -m coverage run -m pytest tests/ -n $(TEST_WORKERS) --dist $(TEST_DIST) --tb=line
|
||||
python -m coverage run -m pytest tests/ -p no:xdist --tb=line
|
||||
python -m coverage combine
|
||||
python -m coverage report
|
||||
|
||||
|
||||
@ -549,14 +549,11 @@ SQLite is synchronous by design and will never be made async. It is more than fa
|
||||
|
||||
- **419 tests** across 34 files: Playwright integration + in-process unit tests
|
||||
- Playwright (NOT pytest-playwright plugin - conflicts, uninstall it)
|
||||
- Runs in parallel via pytest-xdist (`make test` = `-n auto --dist loadfile`); each worker is an isolated uvicorn subprocess on port 10501+ with its own temp database and `DEVPLACE_DATA_DIR`
|
||||
- `loadfile` keeps every test in a file on one worker so session fixtures and ordering hold; cap workers with `make test TEST_WORKERS=4`
|
||||
- `make test-serial` runs everything in one process for debugging
|
||||
- Server starts as subprocess on port 10501 with isolated temp database
|
||||
- Test users `alice_test` / `bob_test` seeded via HTTP at session start
|
||||
- Tests stop at first failure (`-x` flag)
|
||||
- Failure screenshots auto-save to `/tmp/devplace_test_screenshots/`
|
||||
- Headed mode: `make test-headed` (parallel, one Chromium window per worker); `make test-headed-serial` for a single window
|
||||
- Cap headed windows the same way: `make test-headed TEST_WORKERS=2`
|
||||
- Headed mode: `PLAYWRIGHT_HEADLESS=0 make test`
|
||||
|
||||
### Key test patterns
|
||||
|
||||
@ -637,7 +634,7 @@ The app runs correctly under multiple Uvicorn workers (independent processes sha
|
||||
Gitea Actions workflow at `.gitea/workflows/test.yaml`:
|
||||
- Runs on push/PR to `master`
|
||||
- Sets up Python 3.13, installs dependencies + Playwright Chromium
|
||||
- Runs the full test suite under coverage in parallel (`coverage run -m pytest -n auto --dist loadfile`); `.coveragerc` `parallel=true` lets the per-worker `.coverage.*` files combine
|
||||
- Runs the full test suite under coverage (`coverage run -m pytest -p no:xdist`)
|
||||
- Builds and publishes the coverage HTML as an artifact on every run
|
||||
- Uploads failure screenshots as artifacts when a test fails
|
||||
|
||||
|
||||
@ -1,45 +0,0 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
import { Http } from "./Http.js";
|
||||
import { Toast } from "./Toast.js";
|
||||
|
||||
class CustomizationToggle {
|
||||
constructor() {
|
||||
this.card = document.querySelector("[data-customization]");
|
||||
if (!this.card) return;
|
||||
this.card
|
||||
.querySelectorAll("[data-customization-form]")
|
||||
.forEach((form) => this.bind(form));
|
||||
}
|
||||
|
||||
bind(form) {
|
||||
form.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
const button = form.querySelector("[data-customization-toggle]");
|
||||
const field = form.querySelector("input[name='value']");
|
||||
const category = button.dataset.customizationToggle;
|
||||
button.disabled = true;
|
||||
try {
|
||||
const result = await Http.send(form.action, { value: field.value });
|
||||
this.render(button, field, category, result);
|
||||
} catch (error) {
|
||||
console.error("customization toggle failed", error);
|
||||
Toast.flash(button, "Error", 1500);
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render(button, field, category, result) {
|
||||
const column =
|
||||
category === "global" ? "cust_disable_global" : "cust_disable_pagetype";
|
||||
const disabled = Boolean(result.data && result.data[column]);
|
||||
field.value = disabled ? "1" : "0";
|
||||
button.textContent = disabled ? "Enable" : "Disable";
|
||||
button.classList.toggle("btn-primary", disabled);
|
||||
}
|
||||
}
|
||||
|
||||
window.CustomizationToggle = CustomizationToggle;
|
||||
export { CustomizationToggle };
|
||||
@ -28,9 +28,7 @@ BASE_URL = f"http://127.0.0.1:{PORT}"
|
||||
|
||||
_TEST_DB = tempfile.NamedTemporaryFile(suffix=f"_{PORT}.db", delete=False)
|
||||
_TEST_DB.close()
|
||||
_TEST_DATA_DIR = Path(tempfile.mkdtemp(suffix=f"_data_{PORT}"))
|
||||
os.environ["DEVPLACE_DATABASE_URL"] = f"sqlite:///{_TEST_DB.name}"
|
||||
os.environ["DEVPLACE_DATA_DIR"] = str(_TEST_DATA_DIR)
|
||||
os.environ["SECRET_KEY"] = "test-secret-key"
|
||||
os.environ["DEVPLACE_DISABLE_SERVICES"] = "1"
|
||||
os.environ["DEVPLACE_RATE_LIMIT"] = "1000000"
|
||||
@ -104,9 +102,6 @@ def test_db_path():
|
||||
os.unlink(_TEST_DB.name)
|
||||
except OSError:
|
||||
pass
|
||||
import shutil
|
||||
|
||||
shutil.rmtree(_TEST_DATA_DIR, ignore_errors=True)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user