Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ jobs:
**/*.md
!**/target/**
!**/dist/**
- name: Install interrogate
run: |
python -m pip install --user uv==0.11.19
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
"$HOME/.local/bin/uv" tool install interrogate==1.7.0
- name: Lint
run: make lint
- name: Install cargo-nextest
Expand All @@ -49,6 +54,11 @@ jobs:
format: lcov
features: dev-worker
with-default-features: "false"
- name: Loom Concurrency Tests (non-blocking)
if: ${{ always() && matrix.privilege == 'unprivileged' }}
run: make test-loom
timeout-minutes: 1
continue-on-error: true
Comment thread
leynos marked this conversation as resolved.
- name: Install cargo-nextest and test (root)
if: ${{ matrix.privilege == 'root' }}
run: |
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
target/
**/*.rs.bk
.grepai/
.memdb/
8 changes: 7 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: help all clean test build release release-archive lint fmt check-fmt markdownlint nixie typecheck
.PHONY: help all clean test test-loom build release release-archive lint fmt check-fmt markdownlint nixie typecheck

APP ?= pg_embedded_setup_unpriv
CARGO ?= cargo
Expand Down Expand Up @@ -26,6 +26,8 @@ CLIPPY_FLAGS ?= --all-targets --all-features -- -D warnings
RUSTDOC_FLAGS ?= --cfg docsrs -D warnings
MDLINT ?= markdownlint-cli2
NIXIE ?= nixie
INTERROGATE ?= interrogate
PY_DOCSTRING_COVERAGE ?= 100

build: ## Build debug binary
$(CARGO) build $(BUILD_JOBS) --bin "$(APP)"
Expand All @@ -43,6 +45,9 @@ test: ## Run tests with warnings treated as errors
RUSTFLAGS="-D warnings" $(CARGO) nextest run --all-targets --all-features $(BUILD_JOBS)
RUSTFLAGS="-D warnings" $(CARGO) nextest run --tests --workspace --no-default-features --features dev-worker $(BUILD_JOBS)

test-loom: ## Run Loom concurrency tests
$(CARGO) test --features "loom-tests" --lib -- --ignored

release-archive: ## Package release binaries for cargo-binstall
@test -n "$(TARGET)" || (echo "TARGET is required" >&2; exit 1)
@test "$(MANIFEST_VERSION)" = "$(VERSION)" || \
Expand All @@ -57,6 +62,7 @@ release-archive: ## Package release binaries for cargo-binstall
rm -rf "$(RELEASE_ARCHIVE_DIR)"

lint: ## Run Clippy with warnings denied
$(INTERROGATE) --fail-under $(PY_DOCSTRING_COVERAGE) .
RUSTDOCFLAGS="$(RUSTDOC_FLAGS)" $(CARGO) doc --workspace --no-deps $(BUILD_JOBS)
$(CARGO) clippy $(CLIPPY_FLAGS)

Expand Down
12 changes: 9 additions & 3 deletions docs/developers-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,19 @@ can install those published assets on Linux `x86_64` and `aarch64`.
Loom-based checks for `ScopedEnv` are opt-in and only compile when the
`loom-tests` feature is enabled. The Loom tests are marked `#[ignore]`, and
`make test` keeps them dormant: the nextest run uses `--all-features`, while
the follow-up `cargo test` run disables default features (enabling `dev-worker`
only). Run the Loom suite with:
the follow-up `cargo nextest run` disables default features (enabling
`dev-worker` only). Run the Loom suite locally with:

```sh
cargo test --features "loom-tests" --lib -- --ignored
make test-loom
```

The scheduler budget in `src/env/loom_tests.rs` currently uses
`max_threads = 3`, `max_branches = 64`, and `preemption_bound = Some(3)`. The
three bounds jointly constrain the search space so the suite stays tractable.
Changing any of them requires justification, and may need matching CI timeout
adjustments.

## Further reading

- `tests/e2e_postgresql_embedded_diesel.rs` – example of combining the helper
Expand Down
12 changes: 12 additions & 0 deletions src/env/loom_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,20 @@ loom::lazy_static! {
static ref LOOM_ENV_LOCK: loom::sync::Mutex<()> = loom::sync::Mutex::new(());
}

/// Provides the Loom-backed environment lock used by these model checks.
struct LoomEnvLock;

impl EnvLockOps for LoomEnvLock {
type Guard = loom::sync::MutexGuard<'static, ()>;

/// Acquires the modelled environment mutex for a scoped environment guard.
fn lock_env_mutex() -> Self::Guard {
LOOM_ENV_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}

/// Leaves poisoning recovery to Loom's modelled mutex implementation.
fn ensure_lock_is_clean() {}
}

Expand All @@ -31,20 +34,23 @@ loom::thread_local! {
RefCell::new(ThreadStateInner::new());
}

/// Enters a scoped environment frame using Loom thread-local state.
fn enter_scope_loom(vars: Vec<(OsString, Option<OsString>)>) -> usize {
LOOM_THREAD_STATE.with(|cell| {
let mut state = cell.borrow_mut();
state.enter_scope(vars)
})
}

/// Exits a scoped environment frame from Loom thread-local state.
fn exit_scope_loom(index: usize) {
LOOM_THREAD_STATE.with(|cell| {
let mut state = cell.borrow_mut();
state.exit_scope(index);
});
}

/// Applies test environment changes through the Loom state hooks.
fn apply_loom(vars: &[(String, Option<String>)]) -> ScopedEnv {
let owned: Vec<(OsString, Option<OsString>)> = vars
.iter()
Expand All @@ -53,17 +59,22 @@ fn apply_loom(vars: &[(String, Option<String>)]) -> ScopedEnv {
ScopedEnv::apply_owned_with_state(owned, enter_scope_loom, exit_scope_loom)
}

/// Runs a bounded Loom model for the scoped environment lock scenarios.
fn run_loom_model<F>(f: F)
where
F: Fn() + Send + Sync + 'static,
{
let mut builder = loom::model::Builder::new();
// These bounds keep the scheduler search tractable enough for routine CI.
// Increasing the preemption bound in particular needs a matching runtime
// budget review; see the developer guide for details.
builder.max_threads = 3;
builder.max_branches = 64;
builder.preemption_bound = Some(3);
builder.check(f);
}

/// Verifies that concurrent scoped environments cannot overlap.
#[test]
#[ignore = "requires Loom model checking"]
fn scoped_env_serialises_concurrent_scopes() {
Expand Down Expand Up @@ -95,6 +106,7 @@ fn scoped_env_serialises_concurrent_scopes() {
});
}

/// Verifies that nested scopes on one thread keep the lock reentrant.
#[test]
#[ignore = "requires Loom model checking"]
fn scoped_env_allows_reentrant_scopes_on_one_thread() {
Expand Down
2 changes: 2 additions & 0 deletions tests/test_workflow_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ def run_act(
*,
artifact_dir: Path,
) -> tuple[int, Path, str]:
"""Run an `act` job and return its exit code, artefact directory, and logs."""
if shutil.which("act") is None:
pytest.skip("act CLI not installed")
artifact_dir.mkdir(parents=True, exist_ok=True)
Expand Down Expand Up @@ -52,6 +53,7 @@ def run_act(


def test_workflow_produces_expected_artefact_and_logs(tmp_path: Path) -> None:
"""Verify the self-test workflow writes its artefact and greeting logs."""
artifact_dir = tmp_path / "act-artifacts"
code, artdir, logs = run_act(artifact_dir=artifact_dir)
assert code == 0, f"act failed:\n{logs}"
Expand Down
Loading