ci: address review — use --dist=load, per-module teardown, exclude setpriv
CI / lint (pull_request) Failing after 0s
CI / build-and-test (pull_request) Skipped
CI / sanitizers (address) (pull_request) Skipped
CI / sanitizers (undefined) (pull_request) Skipped
CI / fuzz-build (pull_request) Skipped
CI / coverage (pull_request) Skipped
CI / valgrind (pull_request) Skipped
CI / lint (pull_request) Failing after 0s
CI / build-and-test (pull_request) Skipped
CI / sanitizers (address) (pull_request) Skipped
CI / sanitizers (undefined) (pull_request) Skipped
CI / fuzz-build (pull_request) Skipped
CI / coverage (pull_request) Skipped
CI / valgrind (pull_request) Skipped
- Replace --dist=loadgroup (a no-op: it only groups by xdist_group marks) with --dist=load, and make module teardowns remove only their own SOURCE_DIR/DEST_DIR (never the shared worker-keyed TEST_DATA_DIR) so interleaved modules can't wipe each other's fixtures. Add a session-scoped cleanup of the per-worker dir. (loadfile grouped the whole test_features.py module onto one worker and slowed the full suite to 761s vs 224s with load.) - Mark the two setpriv privilege tests with a 'setpriv' marker and run the full merge suite as -m "not setpriv", so the dev/main gate can't go red on the env-dependent /root-traversal tests regardless of the runner uid. - Add a TLS basic test to the ci subset, add workflow_dispatch for on-demand full runs, and fix trailing newlines. - Measured: smoke -m ci = 28 passed/39s; full -n4 = 280 passed/11 skipped/1 xpassed in 224s (was 860s serial).
This commit is contained in:
@@ -4,6 +4,7 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches: [main, dev]
|
branches: [main, dev]
|
||||||
pull_request:
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
@@ -42,11 +43,11 @@ jobs:
|
|||||||
|
|
||||||
- name: Integration Tests (PR smoke subset)
|
- name: Integration Tests (PR smoke subset)
|
||||||
if: github.event_name == 'pull_request'
|
if: github.event_name == 'pull_request'
|
||||||
run: python3 -m pytest tests/integration/ -n 4 --dist=loadgroup -m ci --durations=25 --tb=short -q
|
run: python3 -m pytest tests/integration/ -n 4 --dist=load -m ci --durations=25 --tb=short -q
|
||||||
|
|
||||||
- name: Integration Tests (full suite)
|
- name: Integration Tests (full suite)
|
||||||
if: github.event_name == 'push'
|
if: github.event_name == 'push'
|
||||||
run: python3 -m pytest tests/integration/ -n 4 --dist=loadgroup --durations=25 --tb=short -q
|
run: python3 -m pytest tests/integration/ -n 4 --dist=load -m "not setpriv" --durations=25 --tb=short -q
|
||||||
|
|
||||||
sanitizers:
|
sanitizers:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -133,4 +134,4 @@ jobs:
|
|||||||
- name: Valgrind Memcheck
|
- name: Valgrind Memcheck
|
||||||
run: valgrind --leak-check=full --show-leak-kinds=definite --error-exitcode=1 ./build/tests
|
run: valgrind --leak-check=full --show-leak-kinds=definite --error-exitcode=1 ./build/tests
|
||||||
env:
|
env:
|
||||||
FASTSYNC_UNDER_VALGRIND: "1"
|
FASTSYNC_UNDER_VALGRIND: "1"
|
||||||
|
|||||||
@@ -20,12 +20,12 @@ docker build -t fastsync-ci:local .
|
|||||||
|
|
||||||
# Build, run unit tests, and run integration tests inside the container
|
# Build, run unit tests, and run integration tests inside the container
|
||||||
docker run --rm -v "$PWD:/workspace" -w /workspace fastsync-ci:local \
|
docker run --rm -v "$PWD:/workspace" -w /workspace fastsync-ci:local \
|
||||||
sh -c 'cmake -B build -S . && cmake --build build -j$(nproc) && ./build/tests && python3 -m pytest tests/integration/ -n 4 --dist=loadgroup'
|
sh -c 'cmake -B build -S . && cmake --build build -j$(nproc) && ./build/tests && python3 -m pytest tests/integration/ -n 4 --dist=load'
|
||||||
|
|
||||||
# Avoid root-owned build/ artifacts by matching your host UID/GID
|
# Avoid root-owned build/ artifacts by matching your host UID/GID
|
||||||
docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/workspace" \
|
docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/workspace" \
|
||||||
-w /workspace fastsync-ci:local \
|
-w /workspace fastsync-ci:local \
|
||||||
sh -c 'cmake -B build -S . && cmake --build build -j$(nproc) && ./build/tests && python3 -m pytest tests/integration/ -n 4 --dist=loadgroup'
|
sh -c 'cmake -B build -S . && cmake --build build -j$(nproc) && ./build/tests && python3 -m pytest tests/integration/ -n 4 --dist=load'
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Note:** The first `cmake configure` (`cmake -B build -S .`) fetches xxHash from GitHub via `FetchContent` — network access is required. Subsequent reconfigures reuse the cached source.
|
> **Note:** The first `cmake configure` (`cmake -B build -S .`) fetches xxHash from GitHub via `FetchContent` — network access is required. Subsequent reconfigures reuse the cached source.
|
||||||
@@ -41,7 +41,7 @@ cmake -B build -S . -DSANITIZER=address # AddressSanitizer (ASan)
|
|||||||
cmake -B build -S . -DSANITIZER=thread # ThreadSanitizer (TSan)
|
cmake -B build -S . -DSANITIZER=thread # ThreadSanitizer (TSan)
|
||||||
```
|
```
|
||||||
|
|
||||||
The CI workflow (`.gitea/workflows/ci.yaml`) runs lint (clang-format, cppcheck), then a **fast PR gate** — build + unit + a representative subset of integration tests marked `@pytest.mark.ci`, parallelized with pytest-xdist (`-n 4`). The full coverage jobs (full integration suite, sanitizer, fuzz, coverage, valgrind) run **only on push to `dev`/`main`**; pull requests skip them to keep PR CI under ~3 minutes.
|
The CI workflow (`.gitea/workflows/ci.yaml`) runs lint (clang-format, cppcheck), then a **fast PR gate** — build + unit + a representative subset of integration tests marked `@pytest.mark.ci`, parallelized with pytest-xdist (`-n 4 --dist=load`). The full coverage jobs (full integration suite as `-m "not setpriv"`, sanitizer, fuzz, coverage, valgrind) run **only on push to `dev`/`main`**; pull requests skip them to keep PR CI under ~3 minutes. The two `setpriv` privilege tests are excluded from CI via a marker because their result depends on the runner/container uid and host mount permissions.
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
@@ -53,8 +53,8 @@ cmake -B build -S . && cmake --build build -j$(nproc)
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
./build/tests # unit tests
|
./build/tests # unit tests
|
||||||
python3 -m pytest tests/integration/ -n 4 --dist=loadgroup # full integration suite
|
python3 -m pytest tests/integration/ -n 4 --dist=load -m "not setpriv" # full integration suite (CI excludes env-dependent privilege tests)
|
||||||
python3 -m pytest tests/integration/ -n 4 --dist=loadgroup -m ci # PR-gate subset only
|
python3 -m pytest tests/integration/ -n 4 --dist=load -m ci # PR-gate subset only
|
||||||
```
|
```
|
||||||
|
|
||||||
## CI Workflow — Waiting for Results
|
## CI Workflow — Waiting for Results
|
||||||
|
|||||||
+4
-1
@@ -1,4 +1,7 @@
|
|||||||
[pytest]
|
[pytest]
|
||||||
; Fast integration subset run on every pull request (see .gitea/workflows/ci.yaml).
|
; Fast integration subset run on every pull request (see .gitea/workflows/ci.yaml).
|
||||||
markers =
|
markers =
|
||||||
ci: fast, representative integration tests run on the PR CI gate
|
ci: fast, representative integration tests run on the PR CI gate
|
||||||
|
setpriv: privilege-dependent tests (drop to an unprivileged user); excluded
|
||||||
|
from CI because their result depends on the runner/container uid and the
|
||||||
|
host mount permissions, but run locally as root
|
||||||
|
|||||||
+16
-2
@@ -1,17 +1,31 @@
|
|||||||
"""Shared pytest configuration for integration tests."""
|
"""Shared pytest configuration for integration tests."""
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "integration"))
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "integration"))
|
||||||
|
|
||||||
from common import ServerManager
|
from common import ServerManager, TEST_DATA_DIR
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def shared_server():
|
def shared_server():
|
||||||
"""One server for the entire test session. Avoids 27+ server start/stop cycles."""
|
"""One server for the entire test session. Avoids 27+ server start/stop cycles.
|
||||||
|
|
||||||
|
Under pytest-xdist this session fixture is instantiated once per worker
|
||||||
|
process, so each worker gets its own server on an ephemeral port."""
|
||||||
server = ServerManager()
|
server = ServerManager()
|
||||||
server.start()
|
server.start()
|
||||||
yield server
|
yield server
|
||||||
server.stop()
|
server.stop()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session", autouse=True)
|
||||||
|
def _cleanup_worker_test_data():
|
||||||
|
"""Remove this (worker-keyed) TEST_DATA_DIR at the end of the session.
|
||||||
|
|
||||||
|
Modules clean only their own rows; this final pass ensures the per-worker
|
||||||
|
directory never lingers in the working tree."""
|
||||||
|
yield
|
||||||
|
shutil.rmtree(TEST_DATA_DIR, ignore_errors=True)
|
||||||
|
|||||||
@@ -25,7 +25,11 @@ def setup_test_data():
|
|||||||
generate_test_files(SOURCE_DIR, full=False)
|
generate_test_files(SOURCE_DIR, full=False)
|
||||||
clean_dir(DEST_DIR)
|
clean_dir(DEST_DIR)
|
||||||
yield
|
yield
|
||||||
shutil.rmtree(TEST_DATA_DIR, ignore_errors=True)
|
# Remove only this module's own dirs. Under pytest-xdist the whole
|
||||||
|
# (worker-keyed) TEST_DATA_DIR is shared with concurrently-interleaved
|
||||||
|
# modules, so never rmtree it here.
|
||||||
|
shutil.rmtree(SOURCE_DIR, ignore_errors=True)
|
||||||
|
shutil.rmtree(DEST_DIR, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
class TestDryRun:
|
class TestDryRun:
|
||||||
@@ -2855,6 +2859,7 @@ class TestDeletePolicy:
|
|||||||
"--clear-groups"] + cmd, text=True, capture_output=True)
|
"--clear-groups"] + cmd, text=True, capture_output=True)
|
||||||
|
|
||||||
@pytest.mark.parametrize("mt", [False, True])
|
@pytest.mark.parametrize("mt", [False, True])
|
||||||
|
@pytest.mark.setpriv
|
||||||
def test_ignore_errors_keeps_deletion_active_on_scan_error(self, mt):
|
def test_ignore_errors_keeps_deletion_active_on_scan_error(self, mt):
|
||||||
"""A source I/O error (unreadable subdirectory) aborts the run so no
|
"""A source I/O error (unreadable subdirectory) aborts the run so no
|
||||||
deletion happens by default; --ignore-errors continues, still transfers
|
deletion happens by default; --ignore-errors continues, still transfers
|
||||||
@@ -2901,6 +2906,7 @@ class TestDeletePolicy:
|
|||||||
|
|
||||||
@pytest.mark.parametrize("mt", [False, True])
|
@pytest.mark.parametrize("mt", [False, True])
|
||||||
@pytest.mark.parametrize("timing", ["--delete", "--delete-before"])
|
@pytest.mark.parametrize("timing", ["--delete", "--delete-before"])
|
||||||
|
@pytest.mark.setpriv
|
||||||
def test_ignore_errors_unreadable_root_never_deletes(self, mt, timing):
|
def test_ignore_errors_unreadable_root_never_deletes(self, mt, timing):
|
||||||
"""An unreadable SOURCE ROOT must never be treated as a skippable scan
|
"""An unreadable SOURCE ROOT must never be treated as a skippable scan
|
||||||
error: with --ignore-errors the sequential scanner treats the root as
|
error: with --ignore-errors the sequential scanner treats the root as
|
||||||
|
|||||||
@@ -60,7 +60,8 @@ def setup_test_data():
|
|||||||
generate_test_files(SOURCE_DIR, full=False)
|
generate_test_files(SOURCE_DIR, full=False)
|
||||||
clean_dir(DEST_DIR)
|
clean_dir(DEST_DIR)
|
||||||
yield
|
yield
|
||||||
shutil.rmtree(TEST_DATA_DIR, ignore_errors=True)
|
shutil.rmtree(SOURCE_DIR, ignore_errors=True)
|
||||||
|
shutil.rmtree(DEST_DIR, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
def _run_ssh_test(name, flags, expected_missing=None):
|
def _run_ssh_test(name, flags, expected_missing=None):
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ def setup_test_data():
|
|||||||
generate_test_files(SOURCE_DIR, full=False)
|
generate_test_files(SOURCE_DIR, full=False)
|
||||||
clean_dir(DEST_DIR)
|
clean_dir(DEST_DIR)
|
||||||
yield
|
yield
|
||||||
shutil.rmtree(TEST_DATA_DIR, ignore_errors=True)
|
shutil.rmtree(SOURCE_DIR, ignore_errors=True)
|
||||||
|
shutil.rmtree(DEST_DIR, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
def _run_tcp_test(name, port, flags, use_metadata=True, posix=False):
|
def _run_tcp_test(name, port, flags, use_metadata=True, posix=False):
|
||||||
|
|||||||
@@ -92,10 +92,12 @@ def setup_test_data():
|
|||||||
generate_test_files(SOURCE_DIR, full=False)
|
generate_test_files(SOURCE_DIR, full=False)
|
||||||
clean_dir(DEST_DIR)
|
clean_dir(DEST_DIR)
|
||||||
yield
|
yield
|
||||||
shutil.rmtree(TEST_DATA_DIR, ignore_errors=True)
|
shutil.rmtree(SOURCE_DIR, ignore_errors=True)
|
||||||
|
shutil.rmtree(DEST_DIR, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
class TestTLSBasic:
|
class TestTLSBasic:
|
||||||
|
@pytest.mark.ci
|
||||||
def test_tls_server_client(self, certs):
|
def test_tls_server_client(self, certs):
|
||||||
"""Basic TLS: server with cert/key, client with cert/key + CA."""
|
"""Basic TLS: server with cert/key, client with cert/key + CA."""
|
||||||
clean_dir(DEST_DIR)
|
clean_dir(DEST_DIR)
|
||||||
|
|||||||
Reference in New Issue
Block a user