diff --git a/.agents/skills/watch-github-actions/SKILL.md b/.agents/skills/watch-github-actions/SKILL.md index d1f00347ad..4bded9b2cd 100644 --- a/.agents/skills/watch-github-actions/SKILL.md +++ b/.agents/skills/watch-github-actions/SKILL.md @@ -127,6 +127,8 @@ gh run list --json databaseId,status,headBranch,url --jq '.[] | {id: .databaseId For tmachine Kubernetes conformance failures, inspect the `tmachine-diagnostics-*` artifact. The K3s report includes Agent Sandbox controller state, leader leases, events, and current and previous controller logs. The conformance playbook checks reconciliation after guest boot with a temporary scheduling-gated Sandbox probe before running tests. Distinguish a failed controller probe from a failed conformance scenario; gateway connectivity and controller Deployment availability do not prove reconciliation works. +For the `e2e-suites` lane (`.github/workflows/e2e-suites.yml`), the last lines of the job log give the runner's result: PASS, FAIL with the failed tests, or ERROR with the step that broke. Then inspect the `e2e-results----` artifact, which holds `junit.xml`, `nextest.log` and, after a failure, the guest's `diagnostics/`. + `Pre-release` temporarily omits Codex Security from its `Security Scan` job. `Release` uses the passing prerelease qualification result. Other scanners and qualification gates still run. Use the standalone diff --git a/.github/workflows/branch-checks.yml b/.github/workflows/branch-checks.yml index 218d3d2657..f5af915600 100644 --- a/.github/workflows/branch-checks.yml +++ b/.github/workflows/branch-checks.yml @@ -222,6 +222,9 @@ jobs: cargo fmt --manifest-path examples/supervisor-middleware-content-guard/Cargo.toml --all -- --check cargo fmt --manifest-path examples/isolation-backend-v8/Cargo.toml --all -- --check cargo fmt --manifest-path tests/testkit/Cargo.toml --all -- --check + cargo fmt --manifest-path tests/tmachine/Cargo.toml --all -- --check + cargo fmt --manifest-path tests/runner/Cargo.toml --all -- --check + cargo fmt --manifest-path tests/suites/conformance/Cargo.toml -p sandbox -- --check cargo-lockfiles: name: Cargo lockfiles @@ -239,7 +242,9 @@ jobs: - name: Verify Cargo lockfiles shell: nix develop -c bash -euo pipefail {0} - run: tasks/scripts/check-cargo-lockfiles.sh + run: | + tasks/scripts/check-cargo-lockfiles.sh + uv run tasks/scripts/check_cargo_lockfile_alignment.py rust-lint: name: Rust lint (${{ matrix.system }}) @@ -279,6 +284,9 @@ jobs: cargo clippy --locked --manifest-path examples/supervisor-middleware-content-guard/Cargo.toml --all-targets -- -D warnings cargo clippy --locked --manifest-path examples/isolation-backend-v8/Cargo.toml --all-targets -- -D warnings cargo clippy --locked --manifest-path tests/testkit/Cargo.toml --all-targets -- -D warnings + cargo clippy --locked --manifest-path tests/tmachine/Cargo.toml --all-targets -- -D warnings + cargo clippy --locked --manifest-path tests/runner/Cargo.toml --all-targets -- -D warnings + cargo clippy --locked --manifest-path tests/suites/conformance/Cargo.toml -p sandbox --all-targets -- -D warnings rust-test: name: Rust test (${{ matrix.system }}) @@ -328,6 +336,8 @@ jobs: cargo nextest run --locked --profile ci --workspace --features openshell-server/test-support cargo nextest run --locked --config-file .config/nextest.toml --profile ci --manifest-path examples/supervisor-middleware-content-guard/Cargo.toml cargo nextest run --locked --config-file .config/nextest.toml --profile ci --manifest-path tests/testkit/Cargo.toml + cargo nextest run --locked --config-file .config/nextest.toml --profile ci --manifest-path tests/tmachine/Cargo.toml + cargo nextest run --locked --config-file .config/nextest.toml --profile ci --manifest-path tests/runner/Cargo.toml rust-postgres-test: name: Rust PostgreSQL tests diff --git a/.github/workflows/branch-e2e.yml b/.github/workflows/branch-e2e.yml index db4c96b936..31d96ea62d 100644 --- a/.github/workflows/branch-e2e.yml +++ b/.github/workflows/branch-e2e.yml @@ -303,6 +303,21 @@ jobs: {"environment":"fedora-podman-rootless","installer":"binaries","testsuite":"driver-podman"} ] + # Run the testkit suites in tests/suites/ on tmachine. The environments and + # suites are listed in e2e-suites.yml. Not yet in core-e2e-result.needs: it + # starts out advisory. + e2e-suites: + needs: [pr_metadata, prepare-integration] + if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true' + permissions: + actions: read + contents: read + packages: read + uses: ./.github/workflows/e2e-suites.yml + with: + source-sha: ${{ needs.prepare-integration.outputs.source_sha }} + integration-inputs-artifact-id: ${{ needs.prepare-integration.outputs.integration_inputs_artifact_id }} + docker-e2e: needs: [pr_metadata, build-binaries, build-images] if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true' diff --git a/.github/workflows/e2e-suites.yml b/.github/workflows/e2e-suites.yml new file mode 100644 index 0000000000..eb0b8807a2 --- /dev/null +++ b/.github/workflows/e2e-suites.yml @@ -0,0 +1,129 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Runs testkit suites in tmachine environments on CI. Each matrix row names a +# tmachine environment and installer, the family (a workspace under +# tests/suites/) and the package in it, and the runner label for the +# environment's architecture. `openshell-test-runner` builds the tests, boots +# the environment, runs them inside it and checks the results. +# +# Modeled on the E2E Suites workflow in NVIDIA/OpenShell#4629 by Evan Lezar. + +name: E2E Suites + +on: + workflow_call: + inputs: + source-sha: + description: Source revision of the prepared candidate artifacts + required: true + type: string + integration-inputs-artifact-id: + description: Immutable ID of the prepared integration inputs artifact + required: true + type: string + +permissions: + actions: read + contents: read + packages: read + +# github.workflow is the caller's name here, so scope the group to this +# workflow to avoid colliding with (and cancelling) the caller's other jobs. +concurrency: + group: ${{ github.workflow }}-e2e-suites-${{ github.ref }} + cancel-in-progress: true + +jobs: + suites: + name: E2E suites (${{ matrix.family }}/${{ matrix.package }}, ${{ matrix.environment }}, ${{ matrix.installer }}) + strategy: + fail-fast: false + matrix: + include: + # Only amd64 for now: CI has no arm64 runner with KVM for tmachine, + # and the caller only builds amd64 inputs. + - environment: ubuntu-docker-rootful + installer: deb + family: conformance + package: sandbox + runner: ubuntu-24.04 + runs-on: ${{ matrix.runner }} + timeout-minutes: 90 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ inputs['source-sha'] }} + persist-credentials: false + + - name: Enable KVM access + run: | + set -euo pipefail + if [[ ! -c /dev/kvm ]]; then + echo "::error::The runner did not expose /dev/kvm" + exit 1 + fi + sudo chmod 0666 /dev/kvm + exec 3<>/dev/kvm + exec 3>&- + + - uses: ./.github/actions/setup-nix + + - name: Cache tmachine disks + uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0 + with: + path: ~/.cache/tmachine + # tmachine keys its disk layers on a hash of the Ansible tree. + # actions/cache doesn't save after an exact key hit, so a fixed key + # would rebuild the changed layers on every run. Key on the playbooks + # and fall back to the newest entry for the same runner. + key: tmachine-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('tests/tmachine/ansible/**', 'tests/tmachine/images.nix') }} + restore-keys: | + tmachine-${{ runner.os }}-${{ runner.arch }}- + + - name: Download integration inputs + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + artifact-ids: ${{ inputs['integration-inputs-artifact-id'] }} + path: artifacts + + - name: Run the suites in this environment + env: + ENVIRONMENT: ${{ matrix.environment }} + INSTALLER: ${{ matrix.installer }} + FAMILY: ${{ matrix.family }} + PACKAGE: ${{ matrix.package }} + run: | + # Retry dependency preparation, then run the suites once. The runner + # builds the suites for the guest, so it needs no prepared archives. + nix build --no-link .#openshell-test-runner || nix build --no-link .#openshell-test-runner + nix run .#openshell-test-runner -- \ + --environment "${ENVIRONMENT}" \ + --installer "${INSTALLER}" \ + --family "${FAMILY}" \ + --package "${PACKAGE}" \ + --results artifacts/test-results + + # The results directory holds the JUnit report (junit.xml), the nextest + # log, and the guest's diagnostics when the run failed. + - name: Upload results + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: e2e-results-${{ matrix.family }}-${{ matrix.package }}-${{ matrix.environment }}-${{ matrix.installer }} + path: artifacts/test-results + if-no-files-found: ignore + retention-days: 7 + + result: + name: E2E suites result + needs: [suites] + if: always() + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: ./.github/actions/check-job-results + with: + results: ${{ toJSON(needs) }} diff --git a/AGENTS.md b/AGENTS.md index d1652c2606..2f7840dcb8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -63,6 +63,9 @@ Do not rely on this file for a full inventory. The detailed public and contribut | `sdk/typescript/` | TypeScript SDK | Native Connect client, curated sandbox API, and generated protobuf types | | `proto/` | Protobuf definitions | gRPC service contracts | | `tests/testkit/` | E2E testkit | Support library and `#[testkit::test]` attribute for SDK-driven E2E suites | +| `tests/suites/` | E2E suites | One Cargo workspace per test family; `conformance/sandbox` is the first testkit suite | +| `tests/tmachine/` | Test machines | QEMU machines provisioned with Ansible, and the `Session` API that drives them | +| `tests/runner/` | Test runner | `openshell-test-runner`: builds a family's tests, runs them in one tmachine environment, checks the JUnit results | | `deploy/` | Docker, Helm, K8s | Dockerfiles, Helm chart, manifests | | `docs/` | Published docs | MDX pages, navigation, and content assets | | `fern/` | Docs site config | Fern site config, components, and theme assets | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 33495d9153..88276c1ab0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -345,6 +345,9 @@ These are the primary `mise` tasks for day-to-day development: | `proto/` | Protocol buffer definitions and [public API conventions](proto/README.md) | | `tasks/` | `mise` task definitions and build scripts | | `tests/testkit/` | Support library for SDK-driven end-to-end suites | +| `tests/suites/` | End-to-end suites, one Cargo workspace per family | +| `tests/tmachine/` | QEMU test machines provisioned with Ansible, and the `Session` API that drives them | +| `tests/runner/` | `openshell-test-runner`: runs a test family in a tmachine environment and checks the results | | `deploy/` | Dockerfiles, Helm chart, Kubernetes manifests | | `docs/` | Published Fern docs source, navigation, and content assets | | `fern/` | Fern site config, components, and theme assets | diff --git a/TESTING.md b/TESTING.md index e6754df317..677fb3adac 100644 --- a/TESTING.md +++ b/TESTING.md @@ -17,6 +17,9 @@ python/openshell/ # Python unit tests (*_test.py suffix) e2e/python/ # Python E2E tests (test_*.py prefix) e2e/rust/ # Rust CLI E2E tests tests/testkit/ # Support library for SDK-driven E2E suites +tests/suites/conformance/ # Testkit suites (the `sandbox` crate) beside the older CLI conformance crate +tests/tmachine/ # QEMU test machines, Ansible playbooks, and the Session API +tests/runner/ # openshell-test-runner: runs a test family in a tmachine environment ``` ## Rust Tests @@ -140,6 +143,71 @@ by `OPENSHELL_BIN`. See [tests/testkit/README.md](tests/testkit/README.md) for how to write and run tests. +Suites live under `tests/suites//`. Each family is a Cargo workspace +with its own lockfile. `tests/suites/conformance/sandbox` is the first testkit +suite; it has one smoke test. Run it against a local Docker gateway with: + +```shell +mise run e2e:testkit +``` + +That task runs `e2e/with-docker-gateway.sh`, which starts a gateway and exports +`OPENSHELL_GATEWAY` and `OPENSHELL_BIN`, so the suite runs the same way the +runner runs it in a guest. + +Workspaces that build root crates through path dependencies must lock shared +crates at the versions in the root `Cargo.lock`; otherwise CI can build against +dependencies the Nix toolchain doesn't support. `mise run rust:lockfiles:check` +verifies this, and `uv run tasks/scripts/check_cargo_lockfile_alignment.py --fix` +re-locks a workspace from the root lockfile. Add new testkit workspaces to +`ALIGNED_WORKSPACES` in that script. + +### Running suites in a tmachine environment (`tests/runner/`) + +`openshell-test-runner` runs a test family inside a tmachine VM, against the +OpenShell that the VM's installer set up. It is adapted from the runner in +NVIDIA/OpenShell#4629 by Evan Lezar. + +```shell +nix run .#openshell-test-runner -- \ + --environment ubuntu-docker-rootful --installer deb \ + --family conformance --package sandbox +``` + +It: + +1. builds the family's packages on the host as a nextest archive for the + guest's musl target, with `testkit-ready`; +2. boots the tmachine environment with the installer (this needs KVM, and + tmachine only boots guests of the host's architecture); +3. copies the archive in, waits for the gateway with `testkit-ready`, and runs + the tests with the guest's `cargo-nextest`, pointing them at the installed + CLI (`OPENSHELL_BIN`) and the gateway the installer registered + (`OPENSHELL_GATEWAY`, `--gateway`, default `openshell`); +4. writes `junit.xml`, `nextest.log` and, after a failure, the guest's + diagnostics to `artifacts/test-results/` (`--results`); +5. shuts the VM down, also on failure or SIGTERM. + +A run passes if nextest succeeds and its JUnit report shows at least one test +and no failures. `--filter ` narrows the tests with a nextest +filterset, and `--keep` leaves the VM running until you press Enter. Every SSH +and nextest step has a timeout. Two runs on one host need different +`TMACHINE_SSH_PORT` values (default 2222). The installer inputs, such as the +Debian package and the images, come from `artifacts/`, built by +`nix run .#build-artifacts` or downloaded by CI. + +A flaky or broken test is marked `#[ignore = ""]`, which takes it +out of every environment until it's fixed. + +**CI.** `.github/workflows/e2e-suites.yml` lists the environments and suites as +an explicit matrix and runs the runner once per row. Branch E2E calls it when +the `test:e2e` label is set. It is advisory for now. To run a suite in another +environment on CI, add a row. + +The older conformance, feature and driver lanes in `.github/workflows/` still +run through the tmachine testsuites in `tests/tmachine/config.nix`. They move +to the runner as their suites move to testkit. + ### Python E2E (`e2e/python/`) `mise run e2e:python` builds `openshell/e2e-python:dev` from diff --git a/flake.nix b/flake.nix index 7fac354cac..60ae691dc5 100644 --- a/flake.nix +++ b/flake.nix @@ -127,7 +127,7 @@ firmwarePkgs = testGuestPkgs; }; testMachines = import ./tests/tmachine/config.nix { - inherit pkgs toolchains; + inherit pkgs toolchains rustToolchain; qemuPkgs = tmachineRuntimePkgs; firmwarePkgs = tmachineRuntimePkgs; }; @@ -203,6 +203,7 @@ tmachine = testMachines.package; tmachine-config = testMachines.config; tmachine-unwrapped = testMachines.unwrapped; + openshell-test-runner = testMachines.testRunner; }; devShells = { diff --git a/tasks/rust.toml b/tasks/rust.toml index 682e478fca..cc9e7f661f 100644 --- a/tasks/rust.toml +++ b/tasks/rust.toml @@ -18,8 +18,11 @@ run_windows = "powershell -NoProfile -ExecutionPolicy Bypass -File tasks/scripts hide = true ["rust:lockfiles:check"] -description = "Verify all tracked Cargo lockfiles are current" -run = "tasks/scripts/check-cargo-lockfiles.sh" +description = "Verify all tracked Cargo lockfiles are current and aligned with the root" +run = [ + "tasks/scripts/check-cargo-lockfiles.sh", + "uv run tasks/scripts/check_cargo_lockfile_alignment.py", +] run_windows = "powershell -NoProfile -ExecutionPolicy Bypass -File tasks/scripts/check-cargo-lockfiles.ps1" hide = true @@ -34,6 +37,9 @@ run = [ "cargo clippy --manifest-path examples/supervisor-middleware-content-guard/Cargo.toml --all-targets -- -D warnings", "cargo clippy --manifest-path examples/isolation-backend-v8/Cargo.toml --all-targets -- -D warnings", "cargo clippy --manifest-path tests/testkit/Cargo.toml --all-targets -- -D warnings", + "cargo clippy --manifest-path tests/tmachine/Cargo.toml --all-targets -- -D warnings", + "cargo clippy --manifest-path tests/runner/Cargo.toml --all-targets -- -D warnings", + "cargo clippy --manifest-path tests/suites/conformance/Cargo.toml -p sandbox --all-targets -- -D warnings", ] run_windows = "powershell -NoProfile -ExecutionPolicy Bypass -File tasks/scripts/windows-msvc.ps1 lint native" hide = true @@ -47,6 +53,9 @@ run = [ "cargo fmt --manifest-path examples/supervisor-middleware-content-guard/Cargo.toml --all", "cargo fmt --manifest-path examples/isolation-backend-v8/Cargo.toml --all", "cargo fmt --manifest-path tests/testkit/Cargo.toml --all", + "cargo fmt --manifest-path tests/tmachine/Cargo.toml --all", + "cargo fmt --manifest-path tests/runner/Cargo.toml --all", + "cargo fmt --manifest-path tests/suites/conformance/Cargo.toml -p sandbox", ] hide = true @@ -59,6 +68,9 @@ run = [ "cargo fmt --manifest-path examples/supervisor-middleware-content-guard/Cargo.toml --all -- --check", "cargo fmt --manifest-path examples/isolation-backend-v8/Cargo.toml --all -- --check", "cargo fmt --manifest-path tests/testkit/Cargo.toml --all -- --check", + "cargo fmt --manifest-path tests/tmachine/Cargo.toml --all -- --check", + "cargo fmt --manifest-path tests/runner/Cargo.toml --all -- --check", + "cargo fmt --manifest-path tests/suites/conformance/Cargo.toml -p sandbox -- --check", ] hide = true diff --git a/tasks/scripts/check_cargo_lockfile_alignment.py b/tasks/scripts/check_cargo_lockfile_alignment.py new file mode 100644 index 0000000000..29e82588af --- /dev/null +++ b/tasks/scripts/check_cargo_lockfile_alignment.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# /// script +# requires-python = ">=3.11" +# /// + +"""Check that aligned Cargo workspaces lock shared crates like the root does. + +Some workspaces outside the root build root crates through path +dependencies, for example the testkit suites build `openshell-sdk`. Each +workspace has its own Cargo.lock, and `cargo --locked` only checks a lockfile +against its own manifest, so nothing stops the two lockfiles from resolving +the same crate to different versions. The root's versions are the ones CI and +the Nix toolchain are built for; a newer `aws-lc-sys`, for example, does not +link against the AWS-LC that Nix provides. + +This check fails when an aligned workspace locks a registry crate that the +root lockfile also contains at a version the root lockfile does not have. +With `--fix`, it re-locks each aligned workspace starting from the root +lockfile. +""" + +from __future__ import annotations + +import argparse +import re +import shutil +import subprocess +import sys +import tomllib +from dataclasses import dataclass +from pathlib import Path + +# Workspaces whose shared crates must match the root lockfile. Add a +# workspace here once its lockfile has been aligned with `--fix`. +ALIGNED_WORKSPACES = ( + "tests/testkit", + "tests/suites/conformance", +) + + +@dataclass(frozen=True) +class Mismatch: + workspace: str + name: str + version: str + root_versions: tuple[str, ...] + + def target(self) -> str | None: + """The root version semver-compatible with the locked one, if any.""" + key = compat_key(self.version) + candidates = [v for v in self.root_versions if compat_key(v) == key] + return max(candidates, key=version_tuple) if candidates else None + + +def version_tuple(version: str) -> tuple[int, ...]: + core = re.split(r"[-+]", version, maxsplit=1)[0] + return tuple(int(part) for part in core.split(".")) + + +def compat_key(version: str) -> tuple[int, ...]: + """Cargo's caret-compatibility class: the leftmost non-zero component.""" + major, minor, patch = (*version_tuple(version), 0, 0, 0)[:3] + if major: + return (major,) + if minor: + return (0, minor) + return (0, 0, patch) + + +def registry_versions(lockfile: Path) -> dict[str, set[str]]: + """Versions of registry (non-path) packages in a lockfile, by name.""" + data = tomllib.loads(lockfile.read_text()) + versions: dict[str, set[str]] = {} + for package in data.get("package", []): + if "source" in package: + versions.setdefault(package["name"], set()).add(package["version"]) + return versions + + +def mismatches(root: Path, workspace: str) -> list[Mismatch]: + root_versions = registry_versions(root / "Cargo.lock") + found = [] + for name, versions in sorted( + registry_versions(root / workspace / "Cargo.lock").items() + ): + shared = root_versions.get(name) + if shared is None: + continue + for version in sorted(versions - shared, key=version_tuple): + found.append( + Mismatch( + workspace, name, version, tuple(sorted(shared, key=version_tuple)) + ) + ) + return found + + +def fix(root: Path, workspace: str) -> list[Mismatch]: + """Re-lock the workspace starting from the root lockfile. + + Coupled crates such as the `futures-*` family pin each other, so they + cannot be moved one at a time. Seeding the workspace lockfile with the + root's lets Cargo keep every root version that still fits, add only the + crates the root does not have, and drop the rest. Returns what is still + misaligned afterwards. + """ + shutil.copyfile(root / "Cargo.lock", root / workspace / "Cargo.lock") + subprocess.run( + [ + "cargo", + "metadata", + "--format-version", + "1", + "--manifest-path", + str(root / workspace / "Cargo.toml"), + ], + check=True, + stdout=subprocess.DEVNULL, + ) + return mismatches(root, workspace) + + +def report(problems: list[Mismatch]) -> None: + for m in problems: + target = m.target() + hint = ( + f"; root has {target}" + if target + else f"; root has {', '.join(m.root_versions)}" + ) + print( + f"error: {m.workspace}: {m.name} {m.version} is not in the root Cargo.lock{hint}", + file=sys.stderr, + ) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument( + "--fix", action="store_true", help="update aligned lockfiles to root versions" + ) + args = parser.parse_args(argv) + + root = Path( + subprocess.run( + ["git", "rev-parse", "--show-toplevel"], + check=True, + capture_output=True, + text=True, + ).stdout.strip() + ) + problems: list[Mismatch] = [] + for workspace in ALIGNED_WORKSPACES: + print(f"Checking {workspace}/Cargo.lock against Cargo.lock") + problems += fix(root, workspace) if args.fix else mismatches(root, workspace) + + if problems: + report(problems) + if not args.fix: + print( + "Run `uv run tasks/scripts/check_cargo_lockfile_alignment.py --fix` to align them.", + file=sys.stderr, + ) + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tasks/scripts/check_cargo_lockfile_alignment_test.py b/tasks/scripts/check_cargo_lockfile_alignment_test.py new file mode 100644 index 0000000000..955f31db9e --- /dev/null +++ b/tasks/scripts/check_cargo_lockfile_alignment_test.py @@ -0,0 +1,85 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for the Cargo lockfile alignment check.""" + +from __future__ import annotations + +import importlib.util +import sys +from pathlib import Path + +_SPEC = importlib.util.spec_from_file_location( + "check_cargo_lockfile_alignment", + Path(__file__).with_name("check_cargo_lockfile_alignment.py"), +) +assert _SPEC and _SPEC.loader +alignment = importlib.util.module_from_spec(_SPEC) +sys.modules[_SPEC.name] = alignment +_SPEC.loader.exec_module(alignment) + +REGISTRY = 'source = "registry+https://github.com/rust-lang/crates.io-index"' + + +def lockfile(path: Path, packages: list[tuple[str, str, bool]]) -> None: + """Write a lockfile of (name, version, from_registry) packages.""" + entries = [] + for name, version, from_registry in packages: + entry = f'[[package]]\nname = "{name}"\nversion = "{version}"\n' + if from_registry: + entry += REGISTRY + "\n" + entries.append(entry) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text("version = 4\n\n" + "\n".join(entries)) + + +def test_compat_key_follows_cargo_caret_rules() -> None: + assert alignment.compat_key("1.52.1") == (1,) + assert alignment.compat_key("0.44.0") == (0, 44) + assert alignment.compat_key("0.0.3") == (0, 0, 3) + assert alignment.compat_key("1.0.4+wasi-0.2.12") == (1,) + + +def test_reports_shared_registry_crates_at_other_versions(tmp_path: Path) -> None: + lockfile( + tmp_path / "Cargo.lock", + [ + ("tokio", "1.52.1", True), + ("rand", "0.8.6", True), + ("rand", "0.9.4", True), + ("openshell-sdk", "0.0.0", False), + ], + ) + lockfile( + tmp_path / "ws" / "Cargo.lock", + [ + ("tokio", "1.53.1", True), + ("rand", "0.9.5", True), + ("rand", "0.8.6", True), + ("only-here", "2.0.0", True), + ("openshell-sdk", "0.0.0", False), + ], + ) + + found = alignment.mismatches(tmp_path, "ws") + + assert [(m.name, m.version, m.target()) for m in found] == [ + ("rand", "0.9.5", "0.9.4"), + ("tokio", "1.53.1", "1.52.1"), + ] + + +def test_no_target_without_a_compatible_root_version(tmp_path: Path) -> None: + lockfile(tmp_path / "Cargo.lock", [("syn", "1.0.109", True)]) + lockfile(tmp_path / "ws" / "Cargo.lock", [("syn", "2.0.119", True)]) + + (found,) = alignment.mismatches(tmp_path, "ws") + + assert found.target() is None + + +def test_aligned_lockfiles_pass(tmp_path: Path) -> None: + lockfile(tmp_path / "Cargo.lock", [("tokio", "1.52.1", True)]) + lockfile(tmp_path / "ws" / "Cargo.lock", [("tokio", "1.52.1", True)]) + + assert alignment.mismatches(tmp_path, "ws") == [] diff --git a/tasks/test.toml b/tasks/test.toml index a5a2a03928..c08578ff35 100644 --- a/tasks/test.toml +++ b/tasks/test.toml @@ -23,8 +23,13 @@ depends = [ "test:docs-website", "test:docs-nav", "test:maintainer-approval", + "test:cargo-lockfile-alignment", ] +["test:cargo-lockfile-alignment"] +description = "Test the Cargo lockfile alignment check" +run = "uv run --no-project --with pytest pytest tasks/scripts/check_cargo_lockfile_alignment_test.py" + ["test:docs-website"] description = "Test the docs-website sync script" # --no-project skips installing the OpenShell package; --with supplies the test @@ -120,6 +125,8 @@ run = [ "cargo test -p openshell-server --features test-support", "cargo nextest run --config-file .config/nextest.toml --manifest-path examples/supervisor-middleware-content-guard/Cargo.toml", "cargo nextest run --config-file .config/nextest.toml --manifest-path tests/testkit/Cargo.toml", + "cargo nextest run --config-file .config/nextest.toml --manifest-path tests/tmachine/Cargo.toml", + "cargo nextest run --config-file .config/nextest.toml --manifest-path tests/runner/Cargo.toml", ] run_windows = "powershell -NoProfile -ExecutionPolicy Bypass -File tasks/scripts/windows-msvc.ps1 test-precommit native" hide = true @@ -160,6 +167,10 @@ run = "OPENSHELL_TEST_CONTAINER_ENGINE=docker e2e/with-docker-gateway.sh cargo t description = "Run the shared OCI image feature suite against a Podman-backed gateway" run = "OPENSHELL_TEST_CONTAINER_ENGINE=podman e2e/with-podman-gateway.sh cargo test --locked --manifest-path tests/suites/features/Cargo.toml -p openshell-test-feature-oci-image -- --test-threads 1 --nocapture" +["e2e:testkit"] +description = "Run the testkit suites in tests/suites/conformance against a Docker-backed gateway" +run = "e2e/with-docker-gateway.sh cargo nextest run --locked --manifest-path tests/suites/conformance/Cargo.toml --workspace --exclude openshell-test-conformance-cli --test-threads 1 --no-fail-fast" + ["e2e:rust:push"] description = "Run CLI conformance and policy e2e tests against a Docker gateway that pushes configuration" depends = ["e2e:conformance:build"] diff --git a/tests/runner/Cargo.lock b/tests/runner/Cargo.lock new file mode 100644 index 0000000000..c1d4ae6100 --- /dev/null +++ b/tests/runner/Cargo.lock @@ -0,0 +1,682 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "annotate-snippets" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f211a51805bc641f3ad5b7664c77d2547af685cc33b4cd8d31964027a46f13f1" +dependencies = [ + "anstyle", + "memchr", + "unicode-width", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "blake3" +version = "1.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae" +dependencies = [ + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures", +] + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "clap" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "directories" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys", +] + +[[package]] +name = "encoding_rs" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e985e0451871ad22fb8d2b6b076e2028a502a0d3950998c2c5c0a4f9b5d9679" +dependencies = [ + "cfg-if", + "core_detect", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", +] + +[[package]] +name = "encoding_rs_io" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fba3fe847045ecff794b9c138293a80db914678c453ad63fbf0c6a9eb6e00b22" +dependencies = [ + "encoding_rs", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "find-msvc-tools" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "granit-parser" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e20f99e46474f56bd905c56e817ebddcf377a611f94c53ac4649e4d3fa3c0cd0" +dependencies = [ + "arraydeque", + "smallvec", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "libc" +version = "0.2.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" + +[[package]] +name = "libredox" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mio" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "openshell-test-runner" +version = "0.0.0" +dependencies = [ + "anyhow", + "clap", + "serde", + "serde_json", + "tempfile", + "tmachine", + "tokio", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "redox_users" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60dc65c0ff1a7ae1294b0c67b9f14baf70b644404010370171787bfac1038fc0" +dependencies = [ + "libredox", + "thiserror", +] + +[[package]] +name = "rustix" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-saphyr" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8050abb251097357e24aff63ba2c52a6309ecb7d23a5474023df960a02694d8" +dependencies = [ + "annotate-snippets", + "encoding_rs_io", + "granit-parser", + "num-traits", + "serde_core", + "smallvec", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "smallvec" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tmachine" +version = "0.1.0" +dependencies = [ + "anyhow", + "blake3", + "clap", + "directories", + "libc", + "serde", + "serde-saphyr", + "tempfile", + "tokio", +] + +[[package]] +name = "tokio" +version = "1.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/tests/runner/Cargo.toml b/tests/runner/Cargo.toml new file mode 100644 index 0000000000..f7ba12e51e --- /dev/null +++ b/tests/runner/Cargo.toml @@ -0,0 +1,27 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "openshell-test-runner" +description = "Runs an OpenShell test family in a tmachine environment" +version = "0.0.0" +edition = "2024" +rust-version = "1.94" +license = "Apache-2.0" +repository = "https://github.com/NVIDIA/OpenShell" +publish = false + +[[bin]] +name = "openshell-test-runner" +path = "src/main.rs" + +[dependencies] +anyhow = "1" +clap = { version = "4", features = ["derive"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tempfile = "3" +tmachine = { path = "../tmachine" } +tokio = { version = "1", features = ["macros", "rt"] } + +[workspace] diff --git a/tests/runner/src/bundle.rs b/tests/runner/src/bundle.rs new file mode 100644 index 0000000000..11536511f7 --- /dev/null +++ b/tests/runner/src/bundle.rs @@ -0,0 +1,202 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Building a family's test bundle for the guest, on the host. +//! +//! A bundle is a tar holding a nextest archive built for the guest target, +//! the family workspace's `Cargo.toml` files (for `--workspace-remap`), a +//! `nextest` config with the runner's `JUnit` profile, and `testkit-ready`, +//! which waits for the gateway before the tests run. The guest needs only the +//! `cargo-nextest` binary to run it. +//! +//! Building is separate from running, so a prebuilt bundle can be passed in +//! later instead. +//! +//! Adapted from the bundle builder in Evan Lezar's NVIDIA/OpenShell#4629. + +use std::path::{Path, PathBuf}; +use std::process::Command; + +use anyhow::{Context, Result, bail, ensure}; +use serde::Deserialize; +use tempfile::TempDir; + +/// The nextest profile the runner uses in the guest. +pub const PROFILE: &str = "openshell"; + +/// The readiness check every bundle carries, at the bundle root. +pub const READY_BINARY: &str = "testkit-ready"; + +/// Nextest flags a test as slow after the period and terminates it after +/// `terminate-after` periods, so a hung test fails after two minutes instead +/// of holding the lane until the job times out. +const NEXTEST_CONFIG: &str = r#"# Written by the OpenShell test runner. +[profile.openshell] +fail-fast = false +slow-timeout = { period = "30s", terminate-after = 4 } + +[profile.openshell.junit] +path = "junit.xml" +"#; + +pub struct Bundle { + pub path: PathBuf, + _dir: TempDir, +} + +/// The Rust target for guests. tmachine only boots guests of the host's +/// architecture, and static musl binaries don't depend on the guest's libc. +pub fn guest_target() -> Result<&'static str> { + if cfg!(target_arch = "x86_64") { + Ok("x86_64-unknown-linux-musl") + } else if cfg!(target_arch = "aarch64") { + Ok("aarch64-unknown-linux-musl") + } else { + bail!("tmachine can't boot guests on this host's architecture") + } +} + +/// Build `testkit-ready` for the guest and return its path. +pub fn build_ready(root: &Path, target: &str) -> Result { + let output = Command::new("cargo") + .args(["build", "--locked", "--manifest-path"]) + .arg(root.join("tests/testkit/Cargo.toml")) + .args(["-p", "openshell-testkit", "--bin", READY_BINARY]) + .args([ + "--target", + target, + "--message-format", + "json-render-diagnostics", + ]) + .stderr(std::process::Stdio::inherit()) + .output() + .context("run cargo build")?; + ensure!( + output.status.success(), + "build {READY_BINARY} exited with {}", + output.status + ); + built_executable(&output.stdout, READY_BINARY) + .with_context(|| format!("cargo build did not report a {READY_BINARY} executable")) +} + +/// The executable cargo reports building for the binary target `name`. +fn built_executable(messages: &[u8], name: &str) -> Option { + #[derive(Deserialize)] + struct Message { + reason: String, + target: Option, + executable: Option, + } + #[derive(Deserialize)] + struct Target { + name: String, + } + + serde_json::Deserializer::from_slice(messages) + .into_iter::() + .filter_map(std::result::Result::ok) + .filter(|message| message.reason == "compiler-artifact") + .filter(|message| { + message + .target + .as_ref() + .is_some_and(|target| target.name == name) + }) + .find_map(|message| message.executable) +} + +/// Build the bundle for a family workspace (`tests/suites/`). With no +/// `packages`, the archive holds every package in the workspace. `ready` is +/// the guest's `testkit-ready`. +pub fn build(workspace: &Path, packages: &[String], target: &str, ready: &Path) -> Result { + let dir = tempfile::tempdir().context("create bundle directory")?; + let stage = dir.path().join("stage"); + std::fs::create_dir(&stage)?; + let manifest = workspace.join("Cargo.toml"); + ensure!( + manifest.is_file(), + "{} is not a test family workspace", + workspace.display() + ); + let archive = stage.join("tests.tar.zst"); + + let mut command = Command::new("cargo"); + command + .args(["nextest", "archive", "--locked", "--manifest-path"]) + .arg(&manifest) + .args(["--target", target, "--archive-file"]) + .arg(&archive); + if packages.is_empty() { + command.arg("--workspace"); + } + for package in packages { + command.args(["-p", package]); + } + run(&mut command).with_context(|| format!("build the {} test archive", workspace.display()))?; + + copy_manifests(workspace, &stage)?; + std::fs::write(stage.join("nextest.toml"), NEXTEST_CONFIG)?; + std::fs::copy(ready, stage.join(READY_BINARY)) + .with_context(|| format!("copy {}", ready.display()))?; + + let path = dir.path().join("tests.tar"); + run(Command::new("tar") + .arg("-C") + .arg(&stage) + .arg("-cf") + .arg(&path) + .arg(".")) + .context("pack test bundle")?; + Ok(Bundle { path, _dir: dir }) +} + +/// Copy every `Cargo.toml` under `workspace` into `stage`, keeping relative +/// paths and skipping build output. +fn copy_manifests(workspace: &Path, stage: &Path) -> Result<()> { + fn walk(root: &Path, dir: &Path, stage: &Path) -> Result<()> { + for entry in std::fs::read_dir(dir).with_context(|| format!("read {}", dir.display()))? { + let path = entry?.path(); + let name = path.file_name().unwrap_or_default(); + if path.is_dir() { + if name != "target" && name != ".git" { + walk(root, &path, stage)?; + } + } else if name == "Cargo.toml" { + let dest = stage.join(path.strip_prefix(root)?); + std::fs::create_dir_all(dest.parent().unwrap_or(stage))?; + std::fs::copy(&path, &dest).with_context(|| format!("copy {}", path.display()))?; + } + } + Ok(()) + } + walk(workspace, workspace, stage) +} + +fn run(command: &mut Command) -> Result<()> { + let status = command + .status() + .with_context(|| format!("run {command:?}"))?; + if !status.success() { + bail!("{command:?} exited with {status}"); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn built_executable_finds_the_named_binary() { + let messages = br#"{"reason":"compiler-artifact","target":{"name":"testkit"},"executable":null} +{"reason":"compiler-artifact","target":{"name":"testkit-ready"},"executable":"/t/debug/testkit-ready"} +{"reason":"build-finished","success":true} +"#; + assert_eq!( + built_executable(messages, "testkit-ready"), + Some(PathBuf::from("/t/debug/testkit-ready")) + ); + assert_eq!(built_executable(messages, "other"), None); + } +} diff --git a/tests/runner/src/execute.rs b/tests/runner/src/execute.rs new file mode 100644 index 0000000000..a4d75d19b2 --- /dev/null +++ b/tests/runner/src/execute.rs @@ -0,0 +1,293 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Running a bundle in one tmachine environment: provision it, copy the +//! bundle in, wait for the gateway, run the tests, bring the results back, +//! and always tear it down. +//! +//! Tests find the gateway the way the CLI does, so the runner points them at +//! the one the installer registered with `OPENSHELL_GATEWAY` and at the +//! installed CLI with `OPENSHELL_BIN`. +//! +//! Adapted from the executor in Evan Lezar's NVIDIA/OpenShell#4629. + +use std::path::Path; +use std::process::Output; +use std::time::Duration; + +use anyhow::{Context, Result, ensure}; +use tmachine::Session; +use tmachine::config::Config; + +use crate::bundle::{Bundle, PROFILE, READY_BINARY}; +use crate::report::{Status, decide_status}; + +const GUEST_DIR: &str = "/var/tmp/openshell-tests"; +/// How long the gateway has to become healthy after the environment is up. +// Matches the installers' own wait: an HA k3s gateway booted from a cached +// disk can take minutes to report healthy. +const READY_TIMEOUT_SECS: u64 = 300; +/// How long the nextest run may take in total. Individual tests are bounded +/// by the nextest profile's slow-timeout; this catches a run that stops +/// making progress some other way. +const RUN_TIMEOUT: Duration = Duration::from_mins(45); +/// Slack beyond the readiness check's own timeout for SSH and process start. +const READY_SLACK: Duration = Duration::from_mins(1); + +/// One run: a bundle in one tmachine environment. +pub struct Run<'a> { + /// The tmachine environment, e.g. `ubuntu-docker-rootful`. + pub environment: &'a str, + /// The tmachine installer, e.g. `deb`. + pub installer: &'a str, + /// The gateway name the installer registers with the CLI. Without one, + /// tests use the CLI's active gateway. + pub gateway: Option<&'a str>, + pub bundle: &'a Bundle, + /// A nextest filterset (`-E`) narrowing the tests in the bundle. + pub filter: Option<&'a str>, + /// Where `junit.xml`, `nextest.log` and, after a failure, the guest's + /// `diagnostics/` go. + pub results: &'a Path, + /// Keep the VM running after the tests until Enter is pressed. + pub keep: bool, +} + +/// Run the tests. Every failure, including provisioning, becomes a status. +pub async fn run(config: &Config, run: &Run<'_>) -> Status { + let label = format!("{} / {}", run.environment, run.installer); + if let Err(error) = prepare_results(run.results) { + return Status::Error(format!("{error:#}")); + } + eprintln!("==> {label}: provisioning"); + let session = match provision(config, run.environment, run.installer).await { + Ok(session) => session, + Err(error) => return Status::Error(format!("provision environment: {error:#}")), + }; + + eprintln!("==> {label}: running tests"); + let status = match run_tests(&session, run).await { + Ok(status) => status, + Err(error) => Status::Error(format!("{error:#}")), + }; + if status.is_failure() + && let Err(error) = session + .collect_diagnostics(&run.results.join("diagnostics")) + .await + { + eprintln!("==> {label}: collecting diagnostics failed: {error:#}"); + } + + if run.keep { + eprintln!( + "==> {label}: keeping the VM. Connect with `ssh -p {} tmachine@127.0.0.1` (password `tmachine`). Press Enter to tear it down.", + tmachine::ssh_port() + ); + let mut line = String::new(); + let _ = std::io::stdin().read_line(&mut line); + } + eprintln!("==> {label}: tearing down"); + session.down().await; + status +} + +/// Boot a tmachine environment by name, with OpenShell installed by the named +/// installer. +async fn provision(config: &Config, environment: &str, installer: &str) -> Result { + let names = |names: &mut dyn Iterator| { + names.map(String::as_str).collect::>().join(", ") + }; + let environment = config + .environments + .iter() + .find(|candidate| candidate.name == environment) + .with_context(|| { + format!( + "tmachine environment {environment:?} is not defined; choose one of: {}", + names(&mut config.environments.iter().map(|e| &e.name)) + ) + })?; + let machine = config + .machines + .iter() + .find(|candidate| candidate.name == environment.machine) + .with_context(|| format!("tmachine machine {:?} is not defined", environment.machine))?; + let installer = config + .installers + .iter() + .find(|candidate| candidate.name == installer) + .with_context(|| { + format!( + "tmachine installer {installer:?} is not defined; choose one of: {}", + names(&mut config.installers.iter().map(|i| &i.name)) + ) + })?; + Session::up(machine, environment, installer).await +} + +/// The environment variables that point the tests at the system under test: +/// the registered gateway, if known, and the installed CLI. +pub fn guest_env(gateway: Option<&str>, openshell_bin: &str) -> Vec { + let mut vars = Vec::new(); + if let Some(gateway) = gateway { + vars.push(format!("OPENSHELL_GATEWAY={gateway}")); + } + vars.push(format!("OPENSHELL_BIN={openshell_bin}")); + vars +} + +/// Create the results directory, and remove an earlier run's results so they +/// can't pass for this one's. +fn prepare_results(results: &Path) -> Result<()> { + std::fs::create_dir_all(results).with_context(|| format!("create {}", results.display()))?; + for stale in ["junit.xml", "nextest.log"] { + let path = results.join(stale); + if path.exists() { + std::fs::remove_file(&path).with_context(|| format!("remove {}", path.display()))?; + } + } + let diagnostics = results.join("diagnostics"); + if diagnostics.exists() { + std::fs::remove_dir_all(&diagnostics) + .with_context(|| format!("remove {}", diagnostics.display()))?; + } + Ok(()) +} + +async fn run_tests(session: &Session, run: &Run<'_>) -> Result { + let results = run.results; + + let openshell_bin = checked( + "find the installed openshell CLI", + session.exec(&["sh", "-c", "command -v openshell"]).await?, + )?; + let openshell_bin = openshell_bin.trim(); + ensure!( + !openshell_bin.is_empty(), + "the installer did not put openshell on the guest's PATH" + ); + let env = guest_env(run.gateway, openshell_bin); + + let guest = GUEST_DIR; + checked( + "prepare guest directory", + session + .exec(&[ + "sh", + "-c", + &format!("rm -rf {guest} && mkdir -p {guest}/extract"), + ]) + .await?, + )?; + session + .copy_in(&run.bundle.path, &format!("{guest}/bundle.tar")) + .await?; + checked( + "unpack bundle", + session + .exec(&["tar", "-C", guest, "-xf", &format!("{guest}/bundle.tar")]) + .await?, + )?; + + // A booted guest restarts its gateway, so wait for it once here rather + // than in every test. Failing here is an environment error, not a test + // failure. + let ready_binary = format!("{guest}/{READY_BINARY}"); + let ready_timeout = READY_TIMEOUT_SECS.to_string(); + let mut ready = vec!["env"]; + ready.extend(env.iter().map(String::as_str)); + ready.extend([ready_binary.as_str(), ready_timeout.as_str()]); + checked( + "wait for the gateway", + session + .exec_timeout( + &ready, + Duration::from_secs(READY_TIMEOUT_SECS) + READY_SLACK, + ) + .await?, + )?; + + let archive = format!("{guest}/tests.tar.zst"); + let extract = format!("{guest}/extract"); + let config = format!("{guest}/nextest.toml"); + let mut nextest = vec!["env"]; + nextest.extend(env.iter().map(String::as_str)); + nextest.extend([ + "cargo-nextest", + "nextest", + "run", + "--archive-file", + &archive, + "--workspace-remap", + guest, + "--extract-to", + &extract, + "--extract-overwrite", + "--config-file", + &config, + "--profile", + PROFILE, + "--no-capture", + // Tests share one gateway and the guest has 4 GiB; run them serially + // whatever --no-capture implies. + "--test-threads", + "1", + ]); + if let Some(filter) = run.filter { + nextest.extend(["-E", filter]); + } + let output = session.exec_timeout(&nextest, RUN_TIMEOUT).await?; + let mut log = output.stdout.clone(); + log.extend_from_slice(&output.stderr); + let log_path = results.join("nextest.log"); + std::fs::write(&log_path, &log)?; + + let junit = results.join("junit.xml"); + let found = checked( + "find JUnit report", + session.exec(&["find", guest, "-name", "junit.xml"]).await?, + )?; + let xml = match found.lines().next() { + Some(path) => { + session.copy_out(path, &junit).await?; + Some(std::fs::read_to_string(&junit)?) + } + None => None, + }; + + Ok(decide_status( + output.status.success(), + xml.as_deref(), + &log_path, + )) +} + +fn checked(step: &str, output: Output) -> Result { + ensure!( + output.status.success(), + "{step}: exited with {}: {}", + output.status, + String::from_utf8_lossy(&output.stderr).trim() + ); + String::from_utf8(output.stdout).with_context(|| format!("{step}: output is not UTF-8")) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn points_tests_at_the_gateway_and_the_cli() { + assert_eq!( + guest_env(Some("openshell"), "/usr/bin/openshell"), + [ + "OPENSHELL_GATEWAY=openshell", + "OPENSHELL_BIN=/usr/bin/openshell", + ] + ); + assert_eq!( + guest_env(None, "/usr/bin/openshell"), + ["OPENSHELL_BIN=/usr/bin/openshell"] + ); + } +} diff --git a/tests/runner/src/lib.rs b/tests/runner/src/lib.rs new file mode 100644 index 0000000000..1d8821362d --- /dev/null +++ b/tests/runner/src/lib.rs @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Runs a test family in one tmachine environment: builds the family's tests +//! for the guest, boots the environment with OpenShell installed, runs the +//! tests inside it, and checks the results. + +pub mod bundle; +pub mod execute; +pub mod report; diff --git a/tests/runner/src/main.rs b/tests/runner/src/main.rs new file mode 100644 index 0000000000..a63c3efc13 --- /dev/null +++ b/tests/runner/src/main.rs @@ -0,0 +1,148 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! `openshell-test-runner`: run a test family in one tmachine environment. +//! +//! Run it through `nix run .#openshell-test-runner -- `, which +//! provides the guest cross-compilation toolchain, QEMU, Ansible, and the +//! tmachine configuration. +//! +//! Adapted from the runner in Evan Lezar's NVIDIA/OpenShell#4629. + +use std::path::{Path, PathBuf}; +use std::process::ExitCode; + +use anyhow::{Context, Result, ensure}; +use clap::Parser; +use openshell_test_runner::bundle; +use openshell_test_runner::execute::{self, Run}; + +/// Build a test family for the guest, boot a tmachine environment with +/// OpenShell installed, run the tests in it, and check the results. +#[derive(Parser)] +#[command(name = "openshell-test-runner")] +struct Cli { + /// The tmachine environment to boot, e.g. `ubuntu-docker-rootful`. + #[arg(long)] + environment: String, + /// The tmachine installer that installs OpenShell, e.g. `deb`. + #[arg(long)] + installer: String, + /// The test family: a workspace under `tests/suites/`, e.g. `conformance`. + #[arg(long)] + family: String, + /// Only build these packages of the family [default: every package]. + #[arg(long = "package", short = 'p', value_name = "PACKAGE")] + packages: Vec, + /// A nextest filterset narrowing the tests that run, e.g. + /// `binary(smoke)`. + #[arg(long, value_name = "FILTERSET")] + filter: Option, + /// The gateway name the installer registers with the CLI. The packaged + /// installers register `openshell`. + #[arg(long, default_value = "openshell")] + gateway: String, + /// Keep the VM running after the tests until Enter is pressed. + #[arg(long)] + keep: bool, + /// Where to write `junit.xml`, `nextest.log` and, after a failure, the + /// guest's diagnostics. + #[arg(long, default_value = "artifacts/test-results")] + results: PathBuf, +} + +#[tokio::main(flavor = "current_thread")] +async fn main() -> ExitCode { + // A SIGTERM from CI cancellation must not leave QEMU holding the SSH port. + tmachine::cleanup_on_signals(); + match run(Cli::parse()).await { + Ok(code) => code, + Err(error) => { + eprintln!("error: {error:#}"); + ExitCode::FAILURE + } + } +} + +async fn run(cli: Cli) -> Result { + let root = repo_root()?; + ensure!( + !cli.family.is_empty() + && cli + .family + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_'), + "--family must name a workspace directly under tests/suites/, got {:?}", + cli.family + ); + let workspace = root.join("tests/suites").join(&cli.family); + // Absolute, because `load_tmachine` changes the working directory. + let results = std::path::absolute(&cli.results) + .with_context(|| format!("resolve {}", cli.results.display()))?; + + let target = bundle::guest_target()?; + eprintln!("==> building {} for {target}", bundle::READY_BINARY); + let ready = bundle::build_ready(&root, target)?; + eprintln!("==> building the {} tests for {target}", cli.family); + let bundle = bundle::build(&workspace, &cli.packages, target, &ready)?; + + let tmachine = load_tmachine(&root)?; + let status = execute::run( + &tmachine, + &Run { + environment: &cli.environment, + installer: &cli.installer, + gateway: Some(&cli.gateway), + bundle: &bundle, + filter: cli.filter.as_deref(), + results: &results, + keep: cli.keep, + }, + ) + .await; + + eprintln!( + "\n{} / {} / {}: {status}", + cli.environment, cli.installer, cli.family + ); + eprintln!("Results: {}", results.display()); + Ok(if status.is_failure() { + ExitCode::FAILURE + } else { + ExitCode::SUCCESS + }) +} + +/// Load the tmachine configuration and prepare the process to drive it. +/// +/// tmachine configs name playbooks and inputs relative to `tests/tmachine/`, +/// and its Ansible runs read `ANSIBLE_CONFIG`. +fn load_tmachine(root: &Path) -> Result { + let path = std::env::var_os("TMACHINE_CONFIG").context( + "TMACHINE_CONFIG is not set; run through `nix run .#openshell-test-runner`, which provides it", + )?; + let config = tmachine::config::Config::load(Path::new(&path))?; + let tmachine_dir = root.join("tests/tmachine"); + std::env::set_current_dir(&tmachine_dir) + .with_context(|| format!("enter {}", tmachine_dir.display()))?; + if std::env::var_os("ANSIBLE_CONFIG").is_none() { + // SAFETY: single-threaded at this point; no other thread reads the + // environment concurrently. + unsafe { + std::env::set_var("ANSIBLE_CONFIG", tmachine_dir.join("ansible/ansible.cfg")); + }; + } + Ok(config) +} + +fn repo_root() -> Result { + let output = std::process::Command::new("git") + .args(["rev-parse", "--show-toplevel"]) + .output() + .context("run git rev-parse")?; + ensure!( + output.status.success(), + "not inside the OpenShell repository" + ); + Ok(PathBuf::from(String::from_utf8(output.stdout)?.trim())) +} diff --git a/tests/runner/src/report.rs b/tests/runner/src/report.rs new file mode 100644 index 0000000000..f5f1e74a7f --- /dev/null +++ b/tests/runner/src/report.rs @@ -0,0 +1,278 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Deciding a run's result from nextest's exit status and its `JUnit` report. +//! +//! A run passes only if nextest succeeded and its report shows at least one +//! test and no failures. The runner doesn't know which tests to expect, so it +//! can't tell that a test is missing from the report. +//! +//! Adapted from the report in Evan Lezar's NVIDIA/OpenShell#4629. + +use std::fmt; +use std::path::Path; + +#[derive(Debug, PartialEq, Eq)] +pub enum Status { + Passed { + tests: usize, + }, + Failed { + tests: usize, + failed: Vec, + }, + /// The runner could not complete the run, or the results don't add up. + Error(String), +} + +impl Status { + pub const fn is_failure(&self) -> bool { + !matches!(self, Self::Passed { .. }) + } +} + +impl fmt::Display for Status { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Passed { tests } => write!(f, "PASS: {tests} passed"), + Self::Failed { tests, failed } => write!( + f, + "FAIL: {} of {tests} failed: {}", + failed.len(), + failed.join(", ") + ), + Self::Error(error) => write!(f, "ERROR: {error}"), + } + } +} + +/// How one test case ended, from a `JUnit` report. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum CaseResult { + Passed, + Failed, + Skipped, +} + +/// One test case in a `JUnit` report. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Case { + pub classname: String, + pub name: String, + pub result: CaseResult, +} + +/// Decide a run's status from nextest's exit status and its `JUnit` report. +pub fn decide_status(nextest_succeeded: bool, junit: Option<&str>, log: &Path) -> Status { + let Some(xml) = junit else { + return Status::Error(format!( + "nextest produced no JUnit report (exit {}); see {}", + if nextest_succeeded { + "success" + } else { + "failure" + }, + log.display() + )); + }; + let cases = parse_cases(xml); + let failed: Vec<_> = cases + .iter() + .filter(|case| case.result == CaseResult::Failed) + .map(|case| format!("{} {}", case.classname, case.name)) + .collect(); + let tests = cases + .iter() + .filter(|case| case.result != CaseResult::Skipped) + .count(); + if !failed.is_empty() { + Status::Failed { tests, failed } + } else if !nextest_succeeded { + Status::Error(format!( + "nextest failed, but the JUnit report shows no failed test; see {}", + log.display() + )) + } else if tests == 0 { + Status::Error(format!("no test ran; see {}", log.display())) + } else { + Status::Passed { tests } + } +} + +/// The test cases in a `JUnit` report written by nextest. +pub fn parse_cases(xml: &str) -> Vec { + let mut cases = Vec::new(); + let mut rest = xml; + while let Some(start) = rest.find("') else { + break; + }; + let tag_end = start + len; + let tag = &rest[start..tag_end]; + let (body, next) = if tag.ends_with('/') { + ("", tag_end + 1) + } else { + let close = rest[tag_end..] + .find("") + .map_or(rest.len(), |offset| tag_end + offset); + (&rest[tag_end + 1..close], close) + }; + if let (Some(classname), Some(name)) = (attr(tag, "classname"), attr(tag, "name")) { + let result = if body.contains("(tag: &'a str, name: &str) -> Option<&'a str> { + let key = format!(" {name}=\""); + let start = tag.find(&key)? + key.len(); + let end = start + tag[start..].find('"')?; + Some(&tag[start..end]) +} + +fn unescape(value: &str) -> String { + value + .replace(""", "\"") + .replace("'", "'") + .replace("<", "<") + .replace(">", ">") + .replace("&", "&") +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use super::*; + + const REPORT: &str = r#" + + + + + +"#; + + fn report(cases: &[(&str, &str)]) -> String { + let body: String = cases + .iter() + .map(|(name, child)| { + format!( + "{child}" + ) + }) + .collect(); + format!("{body}") + } + + fn log() -> PathBuf { + PathBuf::from("nextest.log") + } + + #[test] + fn parses_cases_and_their_results() { + let results: Vec<_> = parse_cases(REPORT) + .into_iter() + .map(|case| (case.name, case.result)) + .collect(); + assert_eq!( + results, + [ + ("passed".to_string(), CaseResult::Passed), + ("failed".to_string(), CaseResult::Failed), + ("skipped".to_string(), CaseResult::Skipped), + ] + ); + } + + #[test] + fn names_are_unescaped() { + let cases = parse_cases(r#""#); + assert_eq!(cases[0].name, "a&b"); + } + + #[test] + fn passes_when_nextest_succeeded_and_no_test_failed() { + let xml = report(&[("a", ""), ("b", ""), ("c", "")]); + assert_eq!( + decide_status(true, Some(&xml), &log()), + Status::Passed { tests: 2 } + ); + } + + #[test] + fn failures_are_reported_by_name() { + assert_eq!( + decide_status(false, Some(REPORT), &log()), + Status::Failed { + tests: 2, + failed: vec!["p::b failed".to_string()], + } + ); + } + + #[test] + fn a_missing_report_is_an_error() { + assert!(matches!( + decide_status(true, None, &log()), + Status::Error(_) + )); + assert!(matches!( + decide_status(false, None, &log()), + Status::Error(_) + )); + } + + #[test] + fn nextest_failing_without_a_failed_case_is_an_error() { + let xml = report(&[("a", "")]); + assert!(matches!( + decide_status(false, Some(&xml), &log()), + Status::Error(_) + )); + } + + #[test] + fn a_run_with_no_tests_is_an_error() { + let xml = report(&[("a", "")]); + match decide_status(true, Some(&xml), &log()) { + Status::Error(message) => assert!(message.contains("no test ran"), "{message}"), + other => panic!("{other:?}"), + } + assert!(matches!( + decide_status(true, Some(""), &log()), + Status::Error(_) + )); + } + + /// A report written by the nextest version the guest installs + /// (`ansible/playbooks/nextest.yaml`) with the runner's profile, for a + /// suite with a passing, a failing and an `#[ignore]` test. nextest leaves + /// the ignored test out of the report entirely. + const NEXTEST_REPORT: &str = include_str!("../testdata/nextest-0.9.143-junit.xml"); + + #[test] + fn reads_a_real_nextest_report() { + assert_eq!( + decide_status(false, Some(NEXTEST_REPORT), &log()), + Status::Failed { + tests: 2, + failed: vec!["sandbox::smoke fails".to_string()], + } + ); + } +} diff --git a/tests/runner/testdata/nextest-0.9.143-junit.xml b/tests/runner/testdata/nextest-0.9.143-junit.xml new file mode 100644 index 0000000000..f0542e08a2 --- /dev/null +++ b/tests/runner/testdata/nextest-0.9.143-junit.xml @@ -0,0 +1,33 @@ + + + + + + thread 'fails' (63231268) panicked at sandbox/tests/smoke.rs:7:5: +assertion `left == right` failed: sandbox phase + left: 1 + right: 2 +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +running 1 test +some stdout +test fails ... FAILED + +failures: + +failures: + fails + +test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s + + + +thread 'fails' (63231268) panicked at sandbox/tests/smoke.rs:7:5: +assertion `left == right` failed: sandbox phase + left: 1 + right: 2 +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + + + + diff --git a/tests/suites/conformance/Cargo.lock b/tests/suites/conformance/Cargo.lock index 89d8c0d392..2e4a33d6cc 100644 --- a/tests/suites/conformance/Cargo.lock +++ b/tests/suites/conformance/Cargo.lock @@ -2,11 +2,330 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "asn1-rs" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 2.0.20", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "aws-lc-rs" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e" +dependencies = [ + "aws-lc-sys", + "untrusted 0.7.1", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "sync_wrapper", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", +] + +[[package]] +name = "backon" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" +dependencies = [ + "fastrand", + "gloo-timers", + "tokio", +] + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + +[[package]] +name = "backtrace-ext" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" +dependencies = [ + "backtrace", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" @@ -14,424 +333,3962 @@ version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +[[package]] +name = "cc" +version = "1.2.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ctutils" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12218662ccac37d9714f2191252553d67585b97df8c839cbe49e0bde278dc865" +dependencies = [ + "cmov", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" +dependencies = [ + "block-buffer 0.12.0", + "crypto-common 0.2.2", + "ctutils", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + +[[package]] +name = "enum-ordinalize" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89dd01549b09589510cf0647475075d12071456586d70f5c75c98ae2a5537677" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65863d15a4ce2888bd2f0f543cc963d3879c3a022c8ee43f6141d479a3ac815" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "etcetera" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96" +dependencies = [ + "cfg-if", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flume" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasip2", + "wasip3", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "h2" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "hashlink" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "headers" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" +dependencies = [ + "base64 0.22.1", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1 0.10.6", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.2", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "hostname" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd" +dependencies = [ + "cfg-if", + "libc", + "windows-link", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hybrid-array" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-http-proxy" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd1d471ea2f65ba45eddb1d6ab7d58ac2671d2d4ac14da5c6516ae1d97e1327a" +dependencies = [ + "bytes", + "futures-util", + "headers", + "http", + "hyper", + "hyper-rustls", + "hyper-util", + "pin-project-lite", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "log", + "rustls", + "rustls-native-certs", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is_ci" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7421438de105a0827e44fadd05377727847d717c80ce29a229f85fd04c427b72" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 2.0.20", +] + +[[package]] +name = "jsonpath-rust" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c00ae348f9f8fd2d09f82a98ca381c60df9e0820d8d79fce43e649b4dc3128b" +dependencies = [ + "pest", + "pest_derive", + "regex", + "serde_json", + "thiserror 2.0.20", +] + +[[package]] +name = "jsonptr" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5a3cc660ba5d72bce0b3bb295bf20847ccbb40fd423f3f05b61273672e561fe" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "jsonwebtoken" +version = "10.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" +dependencies = [ + "aws-lc-rs", + "base64 0.22.1", + "getrandom 0.2.17", + "js-sys", + "pem 3.0.6", + "serde", + "serde_json", + "signature", + "simple_asn1", +] + +[[package]] +name = "k8s-openapi" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c75b990324f09bef15e791606b7b7a296d02fc88a344f6eba9390970a870ad5" +dependencies = [ + "base64 0.22.1", + "chrono", + "serde", + "serde-value", + "serde_json", +] + +[[package]] +name = "kube" +version = "0.99.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a4eb20010536b48abe97fec37d23d43069bcbe9686adcf9932202327bc5ca6e" +dependencies = [ + "k8s-openapi", + "kube-client", + "kube-core", + "kube-derive", + "kube-runtime", +] + +[[package]] +name = "kube-client" +version = "0.99.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fc2ed952042df20d15ac2fe9614d0ec14b6118eab89633985d4b36e688dccf1" +dependencies = [ + "base64 0.22.1", + "bytes", + "chrono", + "either", + "futures", + "home", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-http-proxy", + "hyper-rustls", + "hyper-timeout", + "hyper-util", + "jsonpath-rust", + "k8s-openapi", + "kube-core", + "pem 3.0.6", + "rustls", + "secrecy", + "serde", + "serde_json", + "serde_yaml", + "thiserror 2.0.20", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tracing", +] + +[[package]] +name = "kube-core" +version = "0.99.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff0d0793db58e70ca6d689489183816cb3aa481673e7433dc618cf7e8007c675" +dependencies = [ + "chrono", + "form_urlencoded", + "http", + "json-patch", + "k8s-openapi", + "schemars", + "serde", + "serde-value", + "serde_json", + "thiserror 2.0.20", +] + +[[package]] +name = "kube-derive" +version = "0.99.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c562f58dc9f7ca5feac8a6ee5850ca221edd6f04ce0dd2ee873202a88cd494c9" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn 2.0.117", +] + +[[package]] +name = "kube-runtime" +version = "0.99.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88f34cfab9b4bd8633062e0e85edb81df23cb09f159f2e31c60b069ae826ffdc" +dependencies = [ + "ahash", + "async-broadcast", + "async-stream", + "async-trait", + "backon", + "educe", + "futures", + "hashbrown 0.15.5", + "hostname", + "json-patch", + "k8s-openapi", + "kube-client", + "parking_lot", + "pin-project", + "serde", + "serde_json", + "thiserror 2.0.20", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "md-5" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" +dependencies = [ + "cfg-if", + "digest 0.11.2", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "miette" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" +dependencies = [ + "backtrace", + "backtrace-ext", + "cfg-if", + "miette-derive", + "owo-colors", + "supports-color", + "supports-hyperlinks", + "supports-unicode", + "terminal_size", + "textwrap", + "unicode-width 0.1.14", +] + +[[package]] +name = "miette-derive" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "noyalib" +version = "0.0.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "962838abbd8dd2cd3844d2208dc502458bba6985f8bf1dc21761b30260fcbd7f" +dependencies = [ + "hashbrown 0.17.0", + "indexmap", + "libm", + "memchr", + "rustc-hash", + "serde_core", + "smallvec", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "oauth2" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" +dependencies = [ + "base64 0.22.1", + "chrono", + "getrandom 0.2.17", + "http", + "rand 0.8.6", + "reqwest", + "serde", + "serde_json", + "serde_path_to_error", + "sha2 0.10.9", + "thiserror 1.0.69", + "url", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openshell-bootstrap" +version = "0.0.0" +dependencies = [ + "bytes", + "miette", + "openshell-core", + "openshell-crypto", + "rcgen", + "serde", + "serde_json", + "tempfile", + "tokio", + "tracing", +] + +[[package]] +name = "openshell-conformance" +version = "0.0.0" +dependencies = [ + "rand 0.9.4", + "serde", + "serde_json", + "tempfile", + "tokio", +] + +[[package]] +name = "openshell-core" +version = "0.0.0" +dependencies = [ + "async-trait", + "base64 0.22.1", + "chrono", + "glob", + "ipnet", + "miette", + "nix", + "openshell-crypto", + "openshell-extension-core", + "openshell-policy-schema", + "prost", + "prost-types", + "protoc-bin-vendored", + "reqwest", + "rustix", + "rustls", + "rustls-pemfile", + "serde", + "serde_json", + "sha2 0.10.9", + "thiserror 2.0.20", + "tokio", + "tokio-stream", + "tonic", + "tonic-prost", + "tonic-prost-build", + "tonic-types", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "openshell-crypto" +version = "0.0.0" +dependencies = [ + "aws-lc-rs", + "jsonwebtoken", + "kube", + "pem 3.0.6", + "rcgen", + "rustls", + "serde", + "sqlx", + "thiserror 2.0.20", + "tonic", + "x509-parser", +] + +[[package]] +name = "openshell-extension-core" +version = "0.0.0" +dependencies = [ + "hyper-util", + "serde", + "thiserror 2.0.20", + "tokio", + "tonic", + "tower", +] + +[[package]] +name = "openshell-policy-schema" +version = "0.0.0" +dependencies = [ + "miette", + "noyalib", + "serde", + "serde_json", + "serde_path_to_error", + "thiserror 2.0.20", +] + +[[package]] +name = "openshell-sdk" +version = "0.0.0" +dependencies = [ + "async-stream", + "async-trait", + "futures", + "hyper", + "hyper-util", + "miette", + "oauth2", + "openshell-core", + "openshell-crypto", + "reqwest", + "rustls", + "serde", + "thiserror 2.0.20", + "tokio", + "tokio-rustls", + "tokio-tungstenite", + "tonic", + "tower", + "tracing", +] + +[[package]] +name = "openshell-test-conformance-cli" +version = "0.0.0" +dependencies = [ + "openshell-conformance", + "tokio", +] + +[[package]] +name = "openshell-testkit" +version = "0.0.0" +dependencies = [ + "futures", + "openshell-bootstrap", + "openshell-core", + "openshell-sdk", + "openshell-testkit-macros", + "rand 0.9.4", + "serde", + "serde_json", + "tokio", +] + +[[package]] +name = "openshell-testkit-macros" +version = "0.0.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "owo-colors" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64 0.22.1", + "serde_core", +] + +[[package]] +name = "pem" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d354a98a3d1251555de99e8fdd8afda05573c31b82f59063a7b0a29b5527f120" +dependencies = [ + "base64 0.23.1", + "serde_core", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2 0.10.9", +] + +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap", +] + +[[package]] +name = "pin-project" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7" +dependencies = [ + "heck", + "itertools", + "log", + "multimap", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "pulldown-cmark", + "pulldown-cmark-to-cmark", + "regex", + "syn 2.0.117", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "prost-types" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7" +dependencies = [ + "prost", +] + +[[package]] +name = "protoc-bin-vendored" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c381df33c98266b5f08186583660090a4ffa0889e76c7e9a5e175f645a67fa" +dependencies = [ + "protoc-bin-vendored-linux-aarch_64", + "protoc-bin-vendored-linux-ppcle_64", + "protoc-bin-vendored-linux-s390_64", + "protoc-bin-vendored-linux-x86_32", + "protoc-bin-vendored-linux-x86_64", + "protoc-bin-vendored-macos-aarch_64", + "protoc-bin-vendored-macos-x86_64", + "protoc-bin-vendored-win32", +] + +[[package]] +name = "protoc-bin-vendored-linux-aarch_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c350df4d49b5b9e3ca79f7e646fde2377b199e13cfa87320308397e1f37e1a4c" + +[[package]] +name = "protoc-bin-vendored-linux-ppcle_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a55a63e6c7244f19b5c6393f025017eb5d793fd5467823a099740a7a4222440c" + +[[package]] +name = "protoc-bin-vendored-linux-s390_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dba5565db4288e935d5330a07c264a4ee8e4a5b4a4e6f4e83fad824cc32f3b0" + +[[package]] +name = "protoc-bin-vendored-linux-x86_32" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8854774b24ee28b7868cd71dccaae8e02a2365e67a4a87a6cd11ee6cdbdf9cf5" + +[[package]] +name = "protoc-bin-vendored-linux-x86_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b38b07546580df720fa464ce124c4b03630a6fb83e05c336fea2a241df7e5d78" + +[[package]] +name = "protoc-bin-vendored-macos-aarch_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89278a9926ce312e51f1d999fee8825d324d603213344a9a706daa009f1d8092" + +[[package]] +name = "protoc-bin-vendored-macos-x86_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81745feda7ccfb9471d7a4de888f0652e806d5795b61480605d4943176299756" + +[[package]] +name = "protoc-bin-vendored-win32" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95067976aca6421a523e491fce939a3e65249bac4b977adee0ee9771568e8aa3" + +[[package]] +name = "pulldown-cmark" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c3a14896dfa883796f1cb410461aef38810ea05f2b2c33c5aded3649095fdad" +dependencies = [ + "bitflags", + "memchr", + "unicase", +] + +[[package]] +name = "pulldown-cmark-to-cmark" +version = "22.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50793def1b900256624a709439404384204a5dc3a6ec580281bfaac35e882e90" +dependencies = [ + "pulldown-cmark", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rcgen" +version = "0.14.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8774e05a7d0de114588e6a28fe7e71694b82614ed569d86d8b389dfbc98b8ad8" +dependencies = [ + "aws-lc-rs", + "pem 4.0.0", + "rustls-pki-types", + "time", + "x509-parser", + "yasna", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted 0.9.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted 0.9.0", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "sandbox" +version = "0.0.0" +dependencies = [ + "openshell-testkit", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.117", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "secrecy" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" +dependencies = [ + "zeroize", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.2", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.2", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "simple_asn1" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror 2.0.20", + "time", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" +dependencies = [ + "lock_api", +] + +[[package]] +name = "sqlx" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "378620ccc25c62c89d8be1c819e76a88d59bdcc3304733330788948e619bfd71" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b44e85bf579a8eeb4ceaa77a3a523baf2bf0e9bac7e40f405d537b5d2d5ccb" +dependencies = [ + "base64 0.22.1", + "bytes", + "cfg-if", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.16.1", + "hashlink", + "indexmap", + "log", + "memchr", + "percent-encoding", + "rustls", + "serde", + "serde_json", + "sha2 0.10.9", + "smallvec", + "thiserror 2.0.20", + "tokio", + "tokio-stream", + "tracing", + "url", + "webpki-roots", +] + +[[package]] +name = "sqlx-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd2b84f2bc39a5705ef27ec785a11c934a41bbd4a24941e257927cddc26b60bf" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.117", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb8d96de5fdc85a5c4ec813432b523ec637e80ba98f046555f75f7908ddac7c3" +dependencies = [ + "cfg-if", + "dotenvy", + "either", + "heck", + "hex", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2 0.10.9", + "sqlx-core", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.117", + "thiserror 2.0.20", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90b8020fe17c5f2c245bfa2505d7ef59c5604839527c740266ad2214acebea27" +dependencies = [ + "bitflags", + "byteorder", + "bytes", + "crc", + "digest 0.11.2", + "dotenvy", + "either", + "futures-core", + "futures-util", + "generic-array", + "log", + "percent-encoding", + "serde", + "sha1 0.11.0", + "sha2 0.11.0", + "sqlx-core", + "thiserror 2.0.20", + "tracing", +] + +[[package]] +name = "sqlx-postgres" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87a2bdd6e83f6b3ea525ca9fee568030508b58355a43d0b2c1674d5f79dcd65e" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "rand 0.10.2", + "serde", + "serde_json", + "sha2 0.11.0", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.20", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488e99c397a62007e4229aec669a179816339afc6d2620ca6fa420dbee2e982c" +dependencies = [ + "atoi", + "flume", + "form_urlencoded", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "thiserror 2.0.20", + "tracing", + "url", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "supports-color" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6" +dependencies = [ + "is_ci", +] + +[[package]] +name = "supports-hyperlinks" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e396b6523b11ccb83120b115a0b7366de372751aa6edf19844dfb13a6af97e91" + +[[package]] +name = "supports-unicode" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "terminal_size" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" +dependencies = [ + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +dependencies = [ + "unicode-linebreak", + "unicode-width 0.2.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl 2.0.20", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" +dependencies = [ + "futures-util", + "log", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "libc", + "pin-project-lite", + "slab", + "tokio", +] + +[[package]] +name = "tonic" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef" +dependencies = [ + "async-trait", + "axum", + "base64 0.22.1", + "bytes", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "rustls-native-certs", + "socket2", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-build" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c68f61875ac5293cf72e6c8cf0158086428c82c37229e98c840878f1706b0322" +dependencies = [ + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tonic-prost" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0" +dependencies = [ + "bytes", + "prost", + "tonic", +] + +[[package]] +name = "tonic-prost-build" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "654e5643eff75d7f8c99197ce1440ed19a3474eada74c12bbac488b2cafdae27" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build", + "prost-types", + "quote", + "syn 2.0.117", + "tempfile", + "tonic-build", +] + +[[package]] +name = "tonic-types" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab1b02061f83d519bba3caa167f88f261ef05720ab8ebc954ade70de3348e8" +dependencies = [ + "prost", + "prost-types", + "tonic", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "indexmap", + "pin-project-lite", + "slab", + "sync_wrapper", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "base64 0.22.1", + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "mime", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.9.4", + "rustls", + "rustls-pki-types", + "sha1 0.10.6", + "thiserror 2.0.20", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] -name = "errno" -version = "0.3.14" +name = "want" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ - "libc", - "windows-sys", + "try-lock", ] [[package]] -name = "fastrand" -version = "2.5.0" +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "getrandom" -version = "0.3.4" +name = "wasip2" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", + "wit-bindgen 0.57.1", ] [[package]] -name = "itoa" -version = "1.0.18" +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] [[package]] -name = "libc" -version = "0.2.189" +name = "wasm-bindgen" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] [[package]] -name = "linux-raw-sys" -version = "0.12.1" +name = "wasm-bindgen-futures" +version = "0.4.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928" +dependencies = [ + "js-sys", + "wasm-bindgen", +] [[package]] -name = "lock_api" -version = "0.4.14" +name = "wasm-bindgen-macro" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" dependencies = [ - "scopeguard", + "quote", + "wasm-bindgen-macro-support", ] [[package]] -name = "memchr" -version = "2.8.3" +name = "wasm-bindgen-macro-support" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.5", + "wasm-bindgen-shared", +] [[package]] -name = "mio" -version = "1.2.3" +name = "wasm-bindgen-shared" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" dependencies = [ - "libc", - "wasi", - "windows-sys", + "unicode-ident", ] [[package]] -name = "once_cell" -version = "1.21.4" +name = "wasm-encoder" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] [[package]] -name = "openshell-conformance" -version = "0.0.0" +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ - "rand", - "serde", - "serde_json", - "tempfile", - "tokio", + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", ] [[package]] -name = "openshell-test-conformance-cli" -version = "0.0.0" +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "openshell-conformance", - "tokio", + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", ] [[package]] -name = "parking_lot" -version = "0.12.5" +name = "web-sys" +version = "0.3.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8" dependencies = [ - "lock_api", - "parking_lot_core", + "js-sys", + "wasm-bindgen", ] [[package]] -name = "parking_lot_core" -version = "0.9.12" +name = "webpki-roots" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +checksum = "1c1c8e20c8a38fc7d65abfe74eb7280e4aa667e8eec8bc84996d2874810748d9" dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link", + "rustls-pki-types", ] [[package]] -name = "pin-project-lite" -version = "0.2.17" +name = "whoami" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +checksum = "626c4bac6755d76ffc12cb01b2eac751db1996b9e0041de9aa02c8c211ddc82c" [[package]] -name = "ppv-lite86" -version = "0.2.21" +name = "windows-core" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "zerocopy", + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", ] [[package]] -name = "proc-macro2" -version = "1.0.107" +name = "windows-implement" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ - "unicode-ident", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] -name = "quote" -version = "1.0.47" +name = "windows-interface" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] -name = "r-efi" -version = "5.3.0" +name = "windows-link" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] -name = "rand" -version = "0.9.5" +name = "windows-result" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "rand_chacha", - "rand_core", + "windows-link", ] [[package]] -name = "rand_chacha" -version = "0.9.0" +name = "windows-strings" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "ppv-lite86", - "rand_core", + "windows-link", ] [[package]] -name = "rand_core" -version = "0.9.5" +name = "windows-sys" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "getrandom", + "windows-targets", ] [[package]] -name = "redox_syscall" -version = "0.5.18" +name = "windows-sys" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "bitflags", + "windows-link", ] [[package]] -name = "rustix" -version = "1.1.5" +name = "windows-targets" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] -name = "scopeguard" -version = "1.2.0" +name = "windows_aarch64_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] -name = "serde" -version = "1.0.229" +name = "windows_aarch64_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" dependencies = [ - "serde_core", - "serde_derive", + "wit-bindgen-rust-macro", ] [[package]] -name = "serde_core" -version = "1.0.229" +name = "wit-bindgen" +version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" dependencies = [ - "serde_derive", + "anyhow", + "heck", + "wit-parser", ] [[package]] -name = "serde_derive" -version = "1.0.229" +name = "wit-bindgen-rust" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", "proc-macro2", "quote", - "syn 3.0.5", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", ] [[package]] -name = "serde_json" -version = "1.0.151" +name = "wit-component" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ - "itoa", - "memchr", + "anyhow", + "bitflags", + "indexmap", + "log", "serde", - "serde_core", - "zmij", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", ] [[package]] -name = "signal-hook-registry" -version = "1.4.8" +name = "wit-parser" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ - "errno", - "libc", + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", ] [[package]] -name = "smallvec" -version = "1.16.1" +name = "writeable" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] -name = "socket2" -version = "0.6.5" +name = "x509-parser" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" dependencies = [ - "libc", - "windows-sys", + "asn1-rs", + "aws-lc-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror 2.0.20", + "time", ] [[package]] -name = "syn" -version = "2.0.119" +name = "yasna" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +checksum = "b5f6765e852b9b4dc8e2a76843e4d64d1cea8e79bcde0b6901aea8e7c7f08282" dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", + "bit-vec", + "time", ] [[package]] -name = "syn" -version = "3.0.5" +name = "yoke" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", + "stable_deref_trait", + "yoke-derive", + "zerofrom", ] [[package]] -name = "tempfile" -version = "3.27.0" +name = "yoke-derive" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ - "fastrand", - "getrandom", - "once_cell", - "rustix", - "windows-sys", + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", ] [[package]] -name = "tokio" -version = "1.53.1" +name = "zerocopy" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ - "bytes", - "libc", - "mio", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "tokio-macros", - "windows-sys", + "zerocopy-derive", ] [[package]] -name = "tokio-macros" -version = "2.7.2" +name = "zerocopy-derive" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 2.0.117", ] [[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" +name = "zerofrom" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +dependencies = [ + "zerofrom-derive", +] [[package]] -name = "wasip2" -version = "1.0.4+wasi-0.2.12" +name = "zerofrom-derive" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ - "wit-bindgen", + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", ] [[package]] -name = "windows-link" -version = "0.2.1" +name = "zeroize" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] -name = "windows-sys" -version = "0.61.2" +name = "zerotrie" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ - "windows-link", + "displaydoc", + "yoke", + "zerofrom", ] [[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - -[[package]] -name = "zerocopy" -version = "0.8.57" +name = "zerovec" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d35102a9f36d089ccae9e4c6802bc118be4487b80aaffc0ab4e0cf5ce92d2873" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ - "zerocopy-derive", + "yoke", + "zerofrom", + "zerovec-derive", ] [[package]] -name = "zerocopy-derive" -version = "0.8.57" +name = "zerovec-derive" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c01f5ab44258da43cf276c74a2763db2ff3969c9c652c3f2de07041d0b2bc" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 2.0.117", ] [[package]] name = "zmij" -version = "1.0.23" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/tests/suites/conformance/Cargo.toml b/tests/suites/conformance/Cargo.toml index 1c6b001096..8dad4ea1ff 100644 --- a/tests/suites/conformance/Cargo.toml +++ b/tests/suites/conformance/Cargo.toml @@ -3,4 +3,4 @@ [workspace] resolver = "2" -members = ["cli"] +members = ["cli", "sandbox"] diff --git a/tests/suites/conformance/sandbox/Cargo.toml b/tests/suites/conformance/sandbox/Cargo.toml new file mode 100644 index 0000000000..52ec79cbba --- /dev/null +++ b/tests/suites/conformance/sandbox/Cargo.toml @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "sandbox" +description = "Conformance suite: sandbox behavior on every compute driver" +version = "0.0.0" +edition = "2024" +rust-version = "1.94" +license = "Apache-2.0" +repository = "https://github.com/NVIDIA/OpenShell" +publish = false + +[dependencies] +openshell-testkit = { path = "../../../testkit/testkit" } diff --git a/tests/suites/conformance/sandbox/README.md b/tests/suites/conformance/sandbox/README.md new file mode 100644 index 0000000000..cd74905af4 --- /dev/null +++ b/tests/suites/conformance/sandbox/README.md @@ -0,0 +1,60 @@ +# Sandbox conformance suite + +The `sandbox` crate holds testkit suites for sandbox behavior that every compute +driver must provide. It is a member of the `conformance` workspace. Each file in +`tests/` is one suite. + +| Suite | Covers | +|---|---| +| `smoke` | Create a sandbox, run `echo` in it, delete it | + +The suite is small on purpose. It proves that the runner can run a real test in +a tmachine environment on CI. The older suites move to testkit by adding suites +here, or a new family under `tests/suites/`. + +## Running it + +Against a local Docker gateway: + +```shell +mise run e2e:testkit +``` + +Against any gateway the CLI can reach, with the testkit environment variables +described in [tests/testkit/README.md](../../../testkit/README.md): + +```shell +OPENSHELL_GATEWAY= cargo nextest run --locked \ + --manifest-path tests/suites/conformance/Cargo.toml -p sandbox --test-threads 1 +``` + +In a tmachine VM, as CI runs it (see [TESTING.md](../../../../TESTING.md)): + +```shell +nix run .#openshell-test-runner -- \ + --environment ubuntu-docker-rootful --installer deb \ + --family conformance --package sandbox +``` + +## Adding a test + +Add a `#[testkit::test]` function to a suite, or a new file in `tests/` for a new +area. No playbook, test archive or workflow entry is needed: the runner builds +and runs every test in the package. + +## What a test may assert + +These tests run on every compute driver, so they check the public contract and +leave driver behavior alone. + +- Assert only what the public contract promises: the proto comments and the + published docs. +- Assert `phase` and `exit_code`. Don't assert condition reasons. +- Check that something did not happen by reading state, not by sleeping. +- Every environment must pass every test here. If a behavior is left to the + driver, the test belongs in a `features` or `drivers` suite instead. +- Use the CLI for upload, download, connect and forward. Rely on its exit code + and `--output json`. +- Add a testkit helper instead of calling raw gRPC from a test. + +Guidance adapted from NVIDIA/OpenShell#4629 by Evan Lezar. diff --git a/tests/suites/conformance/sandbox/tests/smoke.rs b/tests/suites/conformance/sandbox/tests/smoke.rs new file mode 100644 index 0000000000..4bcfc62d48 --- /dev/null +++ b/tests/suites/conformance/sandbox/tests/smoke.rs @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Sandbox smoke suite: the smallest end-to-end check that the runner can run a +//! testkit test against a real gateway. +//! +//! Adapted from the `sandbox-lifecycle` suite in NVIDIA/OpenShell#4629 by +//! Evan Lezar. + +use testkit::Context; + +/// A sandbox becomes ready, runs a command with exec, and is gone from get and +/// list after delete. +#[testkit::test] +async fn create_exec_delete(cx: &mut Context) -> testkit::Result { + let sandbox = cx + .sandbox("smoke") + .command(["sleep", "infinity"]) + .create() + .await?; + sandbox.wait_ready().await?; + + let marker = format!("openshell-e2e-{}", cx.run_id()); + let output = sandbox.exec(["echo", marker.as_str()]).await?; + testkit::ensure!( + output.exit_code == 0, + "exec exited with {}; stderr: {}", + output.exit_code, + output.stderr + ); + testkit::ensure!( + output.stdout == format!("{marker}\n"), + "exec stdout was {:?}; expected {:?}", + output.stdout, + format!("{marker}\n") + ); + + sandbox.delete().await?; + cx.expect_absent(&sandbox).await +} diff --git a/tests/testkit/Cargo.lock b/tests/testkit/Cargo.lock index 9375647f18..60fab5a0d4 100644 --- a/tests/testkit/Cargo.lock +++ b/tests/testkit/Cargo.lock @@ -2630,9 +2630,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.15" +version = "0.103.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281" dependencies = [ "aws-lc-rs", "ring", @@ -3876,9 +3876,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.7" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +checksum = "1c1c8e20c8a38fc7d65abfe74eb7280e4aa667e8eec8bc84996d2874810748d9" dependencies = [ "rustls-pki-types", ] diff --git a/tests/testkit/README.md b/tests/testkit/README.md index fd6897013d..7a529b2929 100644 --- a/tests/testkit/README.md +++ b/tests/testkit/README.md @@ -89,6 +89,12 @@ time or the CLI does not report it connected. ephemeral gateway and export `OPENSHELL_GATEWAY` and `OPENSHELL_BIN`, so a suite runs under them unchanged. +`mise run e2e:testkit` does this for the suites in `tests/suites/conformance`. +In a tmachine VM, the test runner (`tests/runner/`) does the same: it sets +`OPENSHELL_GATEWAY` to the gateway the installer registered and +`OPENSHELL_BIN` to the installed CLI, then runs the suites with nextest. See +[TESTING.md](../../TESTING.md). + ## Checks ```shell diff --git a/tests/tmachine/Cargo.lock b/tests/tmachine/Cargo.lock index 9116e42ee8..527613035f 100644 --- a/tests/tmachine/Cargo.lock +++ b/tests/tmachine/Cargo.lock @@ -579,6 +579,7 @@ dependencies = [ "blake3", "clap", "directories", + "libc", "serde", "serde-saphyr", "tempfile", diff --git a/tests/tmachine/Cargo.toml b/tests/tmachine/Cargo.toml index 46358ea4e0..3d6508cf42 100644 --- a/tests/tmachine/Cargo.toml +++ b/tests/tmachine/Cargo.toml @@ -15,9 +15,10 @@ anyhow = { version = "1.0.100", default-features = false, features = ["std"] } blake3 = { version = "1.8.7", default-features = false, features = ["std"] } clap = { version = "4.6.6", default-features = false, features = ["std", "derive", "help", "usage", "color"] } directories = { version = "6.0.0", default-features = false } +libc = { version = "0.2", default-features = false } serde = { version = "1.0.229", default-features = false, features = ["std", "derive"] } serde-saphyr = { version = "1.2.0", default-features = false, features = ["deserialize"] } tempfile = { version = "3.27.0", default-features = false } -tokio = { version = "1.53.1", default-features = false, features = ["macros", "rt", "process", "net", "io-util"] } +tokio = { version = "1.53.1", default-features = false, features = ["macros", "rt", "process", "net", "io-util", "signal", "time"] } [workspace] diff --git a/tests/tmachine/config.nix b/tests/tmachine/config.nix index 8f048e16af..843b1d52e1 100644 --- a/tests/tmachine/config.nix +++ b/tests/tmachine/config.nix @@ -4,6 +4,7 @@ { pkgs, toolchains, + rustToolchain, qemuPkgs ? pkgs, firmwarePkgs ? pkgs, }: @@ -232,9 +233,42 @@ let exec ${tmachine}/bin/tmachine --config ${config} "$@" ''; }; + + # Runs a test family in a tmachine environment with tests/runner + # (`nix run .#openshell-test-runner -- --environment ubuntu-docker-rootful + # --installer deb --family conformance`). It cross-builds guest test bundles and drives + # tmachine sessions, so it needs the toolchains and the VM tools. The runner + # itself is built by cargo on first use. By Evan Lezar (NVIDIA/OpenShell#4629). + testRunner = pkgs.writeShellApplication { + name = "openshell-test-runner"; + runtimeInputs = [ + qemu + pkgs.ansible + pkgs.cargo-nextest + pkgs.coreutils + pkgs.git + pkgs.gnutar + pkgs.sshpass + rustToolchain + ]; + runtimeEnv = + pkgs.lib.foldl' (env: toolchain: env // toolchain.env) { } (builtins.attrValues toolchains) + // { + TMACHINE_CONFIG = "${config}"; + } + // pkgs.lib.optionalAttrs isAarch64 { + TMACHINE_FIRMWARE_CODE = firmwarePkgs.OVMF.firmware; + TMACHINE_FIRMWARE_VARS = firmwarePkgs.OVMF.variables; + }; + text = '' + root=$(git rev-parse --show-toplevel) + export ANSIBLE_CONFIG="$root/tests/tmachine/ansible/ansible.cfg" + exec cargo run --locked --quiet --manifest-path "$root/tests/runner/Cargo.toml" --bin openshell-test-runner -- "$@" + ''; + }; in { package = runner; unwrapped = tmachine; - inherit config; + inherit config testRunner; } diff --git a/tests/tmachine/src/ansible.rs b/tests/tmachine/src/ansible.rs index 868b763d5b..3301934954 100644 --- a/tests/tmachine/src/ansible.rs +++ b/tests/tmachine/src/ansible.rs @@ -4,14 +4,14 @@ use std::collections::BTreeMap; use std::path::{Path, PathBuf}; -use anyhow::{Context, Result}; +use anyhow::{Context, Result, ensure}; use tokio::process::Command; pub fn requirements_path() -> PathBuf { PathBuf::from(std::env::var("ANSIBLE_CONFIG").unwrap()).with_file_name("requirements.yaml") } -pub async fn install_roles() { +pub async fn install_roles() -> Result<()> { let status = Command::new("ansible-galaxy") .arg("role") .arg("install") @@ -20,13 +20,18 @@ pub async fn install_roles() { .arg(requirements_path()) .status() .await - .unwrap(); + .context("run ansible-galaxy")?; - assert!(status.success()); + ensure!(status.success(), "ansible-galaxy exited with {status}"); + Ok(()) } pub async fn run(playbook: &Path, inputs: &BTreeMap) -> Result<()> { let mut command = Command::new("ansible-playbook"); + // The VM's SSH forward may not be the inventory's default port. + command + .arg("--extra-vars") + .arg(format!("ansible_port={}", crate::ssh_port())); for (name, path) in inputs { let value = match std::fs::canonicalize(path) { Ok(path) => path, @@ -42,8 +47,19 @@ pub async fn run(playbook: &Path, inputs: &BTreeMap) -> Result< .arg(format!("{name}={}", value.display())); } - let status = command.arg(playbook).status().await.unwrap(); + // Stop ansible-playbook if the caller is dropped, for example when the + // runner is cancelled. + command.kill_on_drop(true); + let status = command + .arg(playbook) + .status() + .await + .context("run ansible-playbook")?; - assert!(status.success()); + ensure!( + status.success(), + "ansible-playbook {} exited with {status}", + playbook.display() + ); Ok(()) } diff --git a/tests/tmachine/src/lib.rs b/tests/tmachine/src/lib.rs new file mode 100644 index 0000000000..716b903922 --- /dev/null +++ b/tests/tmachine/src/lib.rs @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Isolated QEMU test machines provisioned with Ansible. +//! +//! The `tmachine` binary drives one-shot `setup`, `install`, and `test` +//! commands. Callers that need to act on a running machine use [`Session`]. +//! A binary that starts machines should call [`cleanup_on_signals`] once, so a +//! `SIGTERM` does not leave QEMU running. +//! +//! The session API is by Evan Lezar (NVIDIA/OpenShell#4629). + +pub mod ansible; +pub mod config; +mod qemu; + +pub use qemu::{DEFAULT_SSH_PORT, Session, cleanup_on_signals, install, setup, ssh_port, test}; diff --git a/tests/tmachine/src/main.rs b/tests/tmachine/src/main.rs index 3c0d1a4eee..c69e3a29e6 100644 --- a/tests/tmachine/src/main.rs +++ b/tests/tmachine/src/main.rs @@ -5,11 +5,8 @@ use std::path::PathBuf; use anyhow::{Context, Result}; use clap::{Parser, Subcommand}; -use config::{Config, Environment, Installer, Machine}; - -mod ansible; -mod config; -mod qemu; +use tmachine::config::{Config, Environment, Installer, Machine}; +use tmachine::{install, setup, test}; #[derive(Parser)] #[command(name = "tmachine")] @@ -40,12 +37,14 @@ enum Command { #[tokio::main(flavor = "current_thread")] async fn main() -> Result<()> { let cli = Cli::parse(); + // A SIGTERM from CI cancellation must not leave QEMU holding the SSH port. + tmachine::cleanup_on_signals(); let config = Config::load(&cli.config)?; match cli.command { Command::Setup { environment } => { let (machine, environment) = find_environment(&config, &environment)?; - qemu::setup(&machine, &environment).await?; + setup(&machine, &environment).await?; } Command::Install { environment, @@ -53,7 +52,7 @@ async fn main() -> Result<()> { } => { let (machine, environment) = find_environment(&config, &environment)?; let installer = find_installer(&config, &installer)?; - qemu::install(&machine, &environment, &installer).await?; + install(&machine, &environment, &installer).await?; } Command::Test { environment, @@ -67,7 +66,7 @@ async fn main() -> Result<()> { .iter() .find(|candidate| candidate.name == testsuite) .with_context(|| format!("testsuite {testsuite:?} is not defined"))?; - qemu::test(&machine, &environment, &installer, testsuite).await?; + test(&machine, &environment, &installer, testsuite).await?; } } diff --git a/tests/tmachine/src/qemu/cleanup.rs b/tests/tmachine/src/qemu/cleanup.rs new file mode 100644 index 0000000000..08fd822570 --- /dev/null +++ b/tests/tmachine/src/qemu/cleanup.rs @@ -0,0 +1,127 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Keeping QEMU from outliving tmachine. +//! +//! Dropping a VM handle stops QEMU, but a destructor never runs when the +//! process is killed by a signal. Two mechanisms cover that: +//! +//! - On Linux, QEMU asks the kernel to kill it when its parent dies +//! (`PR_SET_PDEATHSIG`), whatever the reason, including `SIGKILL`. +//! - [`cleanup_on_signals`] kills every tracked QEMU on `SIGTERM`, `SIGINT` +//! and `SIGHUP` and exits, which also covers hosts without that kernel +//! feature. +//! +//! A leaked QEMU holds the SSH forward port, so the next run on that host +//! fails until someone kills it by hand. + +use std::sync::Mutex; + +use tokio::process::Command; +use tokio::signal::unix::{SignalKind, signal}; + +static LIVE: Mutex> = Mutex::new(Vec::new()); + +pub(super) fn track(pid: u32) { + if let Ok(mut live) = LIVE.lock() { + live.push(pid); + } +} + +pub(super) fn forget(pid: u32) { + if let Ok(mut live) = LIVE.lock() { + live.retain(|candidate| *candidate != pid); + } +} + +/// Kill every QEMU this process started and has not yet reaped. +fn kill_all() { + let pids = LIVE.lock().map(|live| live.clone()).unwrap_or_default(); + for pid in pids { + if let Ok(pid) = libc::pid_t::try_from(pid) { + // SAFETY: `kill` has no memory-safety preconditions. A recycled + // PID is not a concern: entries are removed when the VM handle + // that owns the child is dropped, and the child is reaped by it. + unsafe { libc::kill(pid, libc::SIGKILL) }; + } + } +} + +/// Make a child die when this process dies, on platforms that support it. +#[cfg(target_os = "linux")] +pub(super) fn die_with_parent(command: &mut Command) { + // SAFETY: the closure runs between fork and exec and calls only + // `prctl`, which is async-signal-safe. + unsafe { + command.pre_exec(|| { + // PR_SET_PDEATHSIG fires when the thread that spawned the child + // exits, so the runtime must keep its spawning thread alive: + // tmachine's binaries use a current-thread runtime. + if libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL) == -1 { + return Err(std::io::Error::last_os_error()); + } + Ok(()) + }); + } +} + +#[cfg(not(target_os = "linux"))] +pub(super) fn die_with_parent(_command: &mut Command) {} + +/// Kill every VM started through tmachine when the process receives +/// `SIGTERM`, `SIGINT` or `SIGHUP`, then exit with the conventional status. +/// +/// Call once from a binary's `main`, inside the Tokio runtime. +pub fn cleanup_on_signals() { + for (kind, number) in [ + (SignalKind::terminate(), libc::SIGTERM), + (SignalKind::interrupt(), libc::SIGINT), + (SignalKind::hangup(), libc::SIGHUP), + ] { + let Ok(mut stream) = signal(kind) else { + eprintln!("tmachine: could not install the signal {number} handler; VMs may leak"); + continue; + }; + tokio::spawn(async move { + stream.recv().await; + kill_all(); + std::process::exit(128 + number); + }); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn tracks_and_forgets_pids() { + // Out of range for a `pid_t`, so a concurrent `kill_all` skips them. + let (kept, dropped) = (u32::MAX - 1, u32::MAX - 2); + track(kept); + track(dropped); + forget(dropped); + let live = LIVE.lock().unwrap().clone(); + assert!(live.contains(&kept)); + assert!(!live.contains(&dropped)); + forget(kept); + } + + #[tokio::test(flavor = "current_thread")] + async fn kill_all_stops_tracked_children_and_handlers_install() { + // Installing handlers must not change behavior until a signal arrives. + cleanup_on_signals(); + let child = Command::new("sleep") + .arg("60") + .kill_on_drop(true) + .spawn() + .unwrap(); + let pid = child.id().unwrap(); + track(pid); + kill_all(); + let mut child = child; + let status = child.wait().await.unwrap(); + assert!(!status.success()); + forget(pid); + } +} diff --git a/tests/tmachine/src/qemu/layer.rs b/tests/tmachine/src/qemu/layer.rs index 9b1273a836..e6ec03650d 100644 --- a/tests/tmachine/src/qemu/layer.rs +++ b/tests/tmachine/src/qemu/layer.rs @@ -10,7 +10,7 @@ use blake3::Hasher; use directories::ProjectDirs; use super::img::QemuImage; -use super::vm::QemuVm; +use super::vm::{LAYER_POWER_OFF_TIMEOUT, QemuVm}; pub(super) async fn cached_layer( base_image: &Path, @@ -29,7 +29,7 @@ pub(super) async fn cached_layer( } if use_galaxy { - crate::ansible::install_roles().await; + crate::ansible::install_roles().await?; } let temporary_disk = disks_dir.join(format!("{hash}.tmp")); @@ -38,10 +38,12 @@ pub(super) async fn cached_layer( } let image = QemuImage::create(base_image, temporary_disk.clone()).await; - let vm = QemuVm::start(&image).await; + let vm = QemuVm::start(&image).await?; run_playbooks(playbooks, inputs).await?; - vm.shutdown().await; - vm.wait().await; + // A layer is only cached after a clean power-off, so failing here is an + // error rather than something to log and move past. + vm.shutdown().await.context("power off the VM")?; + vm.wait(LAYER_POWER_OFF_TIMEOUT).await?; std::fs::rename(temporary_disk, &disk).unwrap(); Ok(disk) diff --git a/tests/tmachine/src/qemu/mod.rs b/tests/tmachine/src/qemu/mod.rs index c13ebbf56b..4dac339bc4 100644 --- a/tests/tmachine/src/qemu/mod.rs +++ b/tests/tmachine/src/qemu/mod.rs @@ -2,13 +2,18 @@ // SPDX-License-Identifier: Apache-2.0 mod ansible_hash; +mod cleanup; mod img; mod install; mod layer; +mod session; mod setup; mod test; mod vm; +pub use cleanup::cleanup_on_signals; pub use install::install; +pub use session::Session; pub use setup::setup; pub use test::test; +pub use vm::{DEFAULT_SSH_PORT, ssh_port}; diff --git a/tests/tmachine/src/qemu/session.rs b/tests/tmachine/src/qemu/session.rs new file mode 100644 index 0000000000..37fb72cd9c --- /dev/null +++ b/tests/tmachine/src/qemu/session.rs @@ -0,0 +1,328 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! A running machine that callers act on over SSH. +//! +//! The session API is by Evan Lezar (NVIDIA/OpenShell#4629). Timeouts on +//! every SSH operation, the configurable SSH port, and a `down` that cannot +//! panic were added when it landed. + +use std::collections::BTreeMap; +use std::future::Future; +use std::path::{Path, PathBuf}; +use std::process::{ExitStatus, Output, Stdio}; +use std::time::{Duration, Instant}; + +use anyhow::{Context, Result, bail, ensure}; +use tempfile::{TempDir, tempdir}; +use tokio::process::Command; + +use crate::config::{Environment, Installer, Machine}; + +use super::img::QemuImage; +use super::install::install; +use super::layer::run_playbooks; +use super::vm::{QemuVm, ssh_port}; + +// The port is the QEMU forward in `vm.rs`; `ansible.rs` passes the same port +// to Ansible. +const SSH_HOST: &str = "127.0.0.1"; +const SSH_USER: &str = "tmachine"; +const SSH_PASSWORD: &str = "tmachine"; +// Password authentication only: keys from the caller's agent or ~/.ssh would +// otherwise use up the guest's authentication attempts first. The keep-alive +// options make SSH give up on a guest that stopped answering, after about a +// minute, instead of waiting forever. +const SSH_OPTIONS: &[&str] = &[ + "-o", + "StrictHostKeyChecking=no", + "-o", + "UserKnownHostsFile=/dev/null", + "-o", + "LogLevel=ERROR", + "-o", + "ConnectTimeout=5", + "-o", + "ServerAliveInterval=15", + "-o", + "ServerAliveCountMax=4", + "-o", + "PreferredAuthentications=password", + "-o", + "PubkeyAuthentication=no", + "-o", + "IdentityAgent=none", +]; +const SSH_READY_TIMEOUT: Duration = Duration::from_secs(300); +const SSH_READY_INTERVAL: Duration = Duration::from_secs(2); +/// How long one probe for SSH readiness may take. +const SSH_PROBE_TIMEOUT: Duration = Duration::from_secs(30); + +/// The longest [`Session::exec`] and the file copies wait for a command. +/// Use [`Session::exec_timeout`] for commands that run longer, such as a +/// whole test suite. +pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(600); + +/// A machine booted from a throwaway overlay of the installed disk. +/// +/// The VM stops when the session is dropped, so a caller that fails or +/// panics cannot leak it. Call [`Session::down`] for a clean shutdown. +pub struct Session { + // Declared before `_dir` so the VM stops before its disk is removed. + vm: Option, + _image: QemuImage, + _dir: TempDir, +} + +impl Session { + /// Set up and install the environment (reusing cached disks), boot a VM + /// from a throwaway overlay, and wait until it accepts SSH. + pub async fn up( + machine: &Machine, + environment: &Environment, + installer: &Installer, + ) -> Result { + let install_disk = install(machine, environment, installer).await?; + let dir = tempdir().context("create session directory")?; + let image = QemuImage::create(&install_disk, dir.path().join("session.qcow2")).await; + let vm = QemuVm::start(&image).await?; + let mut session = Self { + vm: Some(vm), + _image: image, + _dir: dir, + }; + session.wait_for_ssh().await?; + Ok(session) + } + + async fn wait_for_ssh(&mut self) -> Result<()> { + let started = Instant::now(); + loop { + if let Some(status) = self.vm.as_mut().and_then(QemuVm::try_wait) { + bail!( + "QEMU exited with {status} before SSH was ready; is another tmachine VM using port {}?", + ssh_port() + ); + } + match self.exec_timeout(&["true"], SSH_PROBE_TIMEOUT).await { + Ok(output) if output.status.success() => return Ok(()), + Ok(_) | Err(_) if started.elapsed() < SSH_READY_TIMEOUT => { + tokio::time::sleep(SSH_READY_INTERVAL).await; + } + Ok(output) => bail!( + "SSH was not ready after {SSH_READY_TIMEOUT:?}: {}", + String::from_utf8_lossy(&output.stderr).trim() + ), + Err(error) => { + return Err( + error.context(format!("SSH was not ready after {SSH_READY_TIMEOUT:?}")) + ); + } + } + } + } + + /// Run Ansible playbooks against the machine. + pub async fn run_playbooks( + &self, + playbooks: &[PathBuf], + inputs: &BTreeMap, + ) -> Result<()> { + run_playbooks(playbooks, inputs).await + } + + /// Run a command in the machine as the `tmachine` user and capture its + /// output, giving up after [`DEFAULT_TIMEOUT`]. A non-zero exit is + /// returned in the output, not as an error. + pub async fn exec(&self, argv: &[&str]) -> Result { + self.exec_timeout(argv, DEFAULT_TIMEOUT).await + } + + /// Like [`Session::exec`], with an explicit timeout. On timeout the local + /// `sshpass` process is killed and the result is an error. The remote + /// command is not guaranteed to stop with it (there is no TTY to hang up), + /// so treat the machine as unusable after a timeout and shut it down. + pub async fn exec_timeout(&self, argv: &[&str], timeout: Duration) -> Result { + ensure!(!argv.is_empty(), "exec needs a command"); + let remote = argv + .iter() + .map(|arg| shell_quote(arg)) + .collect::>() + .join(" "); + let mut command = ssh_command("ssh", &["-p", &ssh_port().to_string()]); + command + .arg(format!("{SSH_USER}@{SSH_HOST}")) + .arg("--") + .arg(remote) + .stdin(Stdio::null()); + output_within(command, timeout) + .await + .with_context(|| format!("run {argv:?} in tmachine VM")) + } + + /// Copy a local file or directory into the machine. + pub async fn copy_in(&self, src: &Path, dst: &str) -> Result<()> { + let remote = format!("{SSH_USER}@{SSH_HOST}:{dst}"); + scp(src.as_os_str().to_string_lossy().as_ref(), &remote) + .await + .with_context(|| format!("copy {} to {dst} in tmachine VM", src.display())) + } + + /// Copy a file or directory out of the machine. + pub async fn copy_out(&self, src: &str, dst: &Path) -> Result<()> { + let remote = format!("{SSH_USER}@{SSH_HOST}:{src}"); + scp(&remote, dst.as_os_str().to_string_lossy().as_ref()) + .await + .with_context(|| format!("copy {src} from tmachine VM to {}", dst.display())) + } + + /// Write the OpenShell gateway's journal to `dir/gateway.log`. + pub async fn collect_diagnostics(&self, dir: &Path) -> Result<()> { + std::fs::create_dir_all(dir) + .with_context(|| format!("create diagnostics directory {}", dir.display()))?; + let output = self + .exec(&[ + "sudo", + "-n", + "journalctl", + "--no-pager", + "--lines", + "2000", + "_SYSTEMD_UNIT=openshell-gateway.service", + "+", + "_SYSTEMD_USER_UNIT=openshell-gateway.service", + ]) + .await?; + let log = dir.join("gateway.log"); + let mut contents = output.stdout; + contents.extend_from_slice(&output.stderr); + std::fs::write(&log, contents).with_context(|| format!("write {}", log.display())) + } + + /// Open an interactive SSH shell in the machine. + pub async fn shell(&self) -> Result { + ssh_command("ssh", &["-tt", "-p", &ssh_port().to_string()]) + .arg(format!("{SSH_USER}@{SSH_HOST}")) + .stdin(Stdio::inherit()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .status() + .await + .context("open SSH shell in tmachine VM") + } + + /// Power the machine off and wait for QEMU to exit. + /// + /// This never panics and never fails: teardown has no useful recovery, + /// so a guest that does not power off cleanly is killed and the problem + /// is logged to stderr. + pub async fn down(mut self) { + if let Some(vm) = self.vm.take() { + vm.power_off_or_kill().await; + } + } +} + +fn ssh_command(program: &str, args: &[&str]) -> Command { + let mut command = Command::new("sshpass"); + command + .env("SSHPASS", SSH_PASSWORD) + .arg("-e") + .arg(program) + .args(args) + .args(SSH_OPTIONS); + // Dropping the future that waits for the process, as a timeout does, stops it. + command.kill_on_drop(true); + command +} + +/// Run a command to completion and capture its output, or give up after +/// `timeout` and kill it. +async fn output_within(mut command: Command, timeout: Duration) -> Result { + bounded(timeout, command.output()).await?.context("run") +} + +/// Wait for `work`, or fail after `timeout`. +async fn bounded(timeout: Duration, work: impl Future) -> Result { + match tokio::time::timeout(timeout, work).await { + Ok(value) => Ok(value), + Err(_) => bail!("timed out after {timeout:?}"), + } +} + +async fn scp(src: &str, dst: &str) -> Result<()> { + let mut command = ssh_command("scp", &["-r", "-P", &ssh_port().to_string()]); + command.arg(src).arg(dst).stdin(Stdio::null()); + let output = output_within(command, DEFAULT_TIMEOUT) + .await + .context("run scp")?; + ensure!( + output.status.success(), + "scp exited with {}: {}", + output.status, + String::from_utf8_lossy(&output.stderr).trim() + ); + Ok(()) +} + +/// Quote an argument for the remote POSIX shell that SSH runs commands in. +fn shell_quote(arg: &str) -> String { + let safe = !arg.is_empty() + && arg + .bytes() + .all(|b| b.is_ascii_alphanumeric() || b"-_./=:@,+%".contains(&b)); + if safe { + arg.to_string() + } else { + format!("'{}'", arg.replace('\'', r"'\''")) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn leaves_plain_arguments_unquoted() { + assert_eq!(shell_quote("cargo-nextest"), "cargo-nextest"); + assert_eq!(shell_quote("/var/lib/x.tar.zst"), "/var/lib/x.tar.zst"); + assert_eq!(shell_quote("KEY=value"), "KEY=value"); + } + + #[test] + fn quotes_shell_metacharacters() { + assert_eq!( + shell_quote("binary(a) | binary(b)"), + "'binary(a) | binary(b)'" + ); + assert_eq!(shell_quote("$HOME"), "'$HOME'"); + assert_eq!(shell_quote(""), "''"); + } + + #[test] + fn escapes_single_quotes() { + assert_eq!(shell_quote("it's"), r"'it'\''s'"); + } + + #[tokio::test(flavor = "current_thread")] + async fn a_command_that_outlives_its_timeout_is_an_error_and_is_killed() { + let mut command = Command::new("sleep"); + command.arg("60").kill_on_drop(true); + let started = Instant::now(); + let error = output_within(command, Duration::from_millis(100)) + .await + .unwrap_err(); + assert!(format!("{error:#}").contains("timed out"), "{error:#}"); + assert!(started.elapsed() < Duration::from_secs(5)); + } + + #[tokio::test(flavor = "current_thread")] + async fn a_command_within_its_timeout_returns_its_output() { + let mut command = Command::new("echo"); + command.arg("hi"); + let output = output_within(command, Duration::from_secs(10)) + .await + .unwrap(); + assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "hi"); + } +} diff --git a/tests/tmachine/src/qemu/test.rs b/tests/tmachine/src/qemu/test.rs index ac8a8bdfc0..cd4cb95d48 100644 --- a/tests/tmachine/src/qemu/test.rs +++ b/tests/tmachine/src/qemu/test.rs @@ -1,18 +1,11 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -use std::process::Stdio; - -use anyhow::{Context, Result}; -use tempfile::tempdir; -use tokio::process::Command; +use anyhow::Result; use crate::config::{Environment, Installer, Machine, Testsuite}; -use super::img::QemuImage; -use super::install::install; -use super::layer::run_playbooks; -use super::vm::QemuVm; +use super::session::Session; pub async fn test( machine: &Machine, @@ -20,46 +13,20 @@ pub async fn test( installer: &Installer, testsuite: &Testsuite, ) -> Result<()> { - let install_disk = install(machine, environment, installer).await?; - let test_dir = tempdir().unwrap(); - let test_disk = test_dir.path().join("test.qcow2"); - let image = QemuImage::create(&install_disk, test_disk).await; - let vm = QemuVm::start(&image).await; - - run_playbooks(&testsuite.playbooks, &testsuite.inputs).await?; + let session = Session::up(machine, environment, installer).await?; + session + .run_playbooks(&testsuite.playbooks, &testsuite.inputs) + .await?; if testsuite.interactive { println!("Opening an SSH shell in the tmachine VM."); - let ssh_status = Command::new("sshpass") - .env("SSHPASS", "tmachine") - .args([ - "-e", - "ssh", - "-tt", - "-p", - "2222", - "-o", - "StrictHostKeyChecking=no", - "-o", - "UserKnownHostsFile=/dev/null", - "-o", - "LogLevel=ERROR", - "tmachine@127.0.0.1", - ]) - .stdin(Stdio::inherit()) - .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()) - .status() - .await - .context("open SSH shell in tmachine VM")?; - - vm.shutdown().await; - vm.wait().await; + let ssh_status = session.shell().await; + session.down().await; + let ssh_status = ssh_status?; anyhow::ensure!(ssh_status.success(), "SSH shell exited with {ssh_status}"); return Ok(()); } - vm.shutdown().await; - vm.wait().await; + session.down().await; Ok(()) } diff --git a/tests/tmachine/src/qemu/vm.rs b/tests/tmachine/src/qemu/vm.rs index 1e035632e2..1ba16b3e49 100644 --- a/tests/tmachine/src/qemu/vm.rs +++ b/tests/tmachine/src/qemu/vm.rs @@ -3,82 +3,219 @@ #[cfg(target_arch = "aarch64")] use std::fs; +use std::net::{Ipv4Addr, TcpListener}; #[cfg(target_arch = "aarch64")] use std::os::unix::fs::PermissionsExt; use std::path::Path; use std::path::PathBuf; use std::process::Stdio; +use std::time::Duration; +use anyhow::{Context, Result, bail, ensure}; use tempfile::{TempDir, tempdir}; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::net::UnixStream; use tokio::process::{Child, Command}; +use super::cleanup; use super::img::QemuImage; +/// The host port forwarded to the guest's SSH server unless +/// `TMACHINE_SSH_PORT` says otherwise. +pub const DEFAULT_SSH_PORT: u16 = 2222; + +/// How long the QMP power-off request has to be acknowledged. A guest or QEMU +/// that stops answering must not hang teardown. +const QMP_TIMEOUT: Duration = Duration::from_secs(10); + +/// How long a powered-off guest has to exit at teardown before QEMU is +/// killed. +const POWER_OFF_TIMEOUT: Duration = Duration::from_secs(120); + +/// How long a guest being saved as a cached layer has to power off. A heavily +/// provisioned guest (Docker, k3s) can take minutes to stop its services, and +/// a layer that is killed instead is not cached, so this is much longer than +/// [`POWER_OFF_TIMEOUT`]. It still bounds a guest that never stops. +pub(super) const LAYER_POWER_OFF_TIMEOUT: Duration = Duration::from_secs(600); + +/// The host port forwarded to the guest's SSH server. +/// +/// Concurrent tmachine runs on one host need different ports; set +/// `TMACHINE_SSH_PORT` to run them side by side. +pub fn ssh_port() -> u16 { + let value = std::env::var("TMACHINE_SSH_PORT").ok(); + let port = parse_ssh_port(value.as_deref()); + if let Some(value) = value.as_deref().filter(|_| port == DEFAULT_SSH_PORT) + && value.trim() != DEFAULT_SSH_PORT.to_string() + { + eprintln!( + "tmachine: ignoring TMACHINE_SSH_PORT={value:?}, which is not a port from 1 to 65535; using {DEFAULT_SSH_PORT}" + ); + } + port +} + +fn parse_ssh_port(value: Option<&str>) -> u16 { + value + .and_then(|value| value.trim().parse().ok()) + .filter(|port| *port != 0) + .unwrap_or(DEFAULT_SSH_PORT) +} + pub(super) struct QemuVm { child: Child, runtime_dir: TempDir, + pid: Option, +} + +impl Drop for QemuVm { + fn drop(&mut self) { + // `kill_on_drop` stops QEMU; stop tracking it for signal cleanup. + if let Some(pid) = self.pid.take() { + cleanup::forget(pid); + } + } } impl QemuVm { - pub(super) async fn start(image: &QemuImage) -> Self { - let runtime_dir = tempdir().unwrap(); + pub(super) async fn start(image: &QemuImage) -> Result { + let port = ssh_port(); + // QEMU reports a taken port only by exiting, which is slow to notice + // and hard to diagnose. + TcpListener::bind((Ipv4Addr::LOCALHOST, port)).with_context(|| { + format!( + "host port {port} is not available for the VM's SSH forward; is another tmachine VM running? Set TMACHINE_SSH_PORT to use another port" + ) + })?; + + let runtime_dir = tempdir().context("create VM runtime directory")?; let qmp_socket = runtime_dir.path().join("qmp.sock"); - assert!( + ensure!( qmp_socket.as_os_str().as_encoded_bytes().len() < 108, "QMP socket path is too long: {} (set TMPDIR to a shorter path)", qmp_socket.display() ); let firmware_vars = prepare_firmware(runtime_dir.path()); - let child = command(image, &qmp_socket, firmware_vars.as_deref()) + let child = command(image, &qmp_socket, firmware_vars.as_deref(), port) .spawn() - .unwrap(); + .context("start QEMU")?; + let pid = child.id(); + if let Some(pid) = pid { + cleanup::track(pid); + } + + Ok(Self { + child, + runtime_dir, + pid, + }) + } + + /// The exit status, if QEMU has already exited. + pub(super) fn try_wait(&mut self) -> Option { + self.child.try_wait().ok().flatten() + } - Self { child, runtime_dir } + /// Wait for QEMU to exit after a power-off request, and check it exited + /// cleanly. QEMU is killed if it does not exit within `timeout`. + pub(super) async fn wait(mut self, timeout: Duration) -> Result<()> { + match tokio::time::timeout(timeout, self.child.wait()).await { + Ok(status) => { + let status = status.context("wait for QEMU")?; + ensure!(status.success(), "QEMU exited with {status}"); + Ok(()) + } + Err(_) => { + self.kill().await; + bail!("QEMU did not exit within {timeout:?} of the power-off request; killed it") + } + } } - pub(super) async fn wait(mut self) { - let status = self.child.wait().await.unwrap(); - assert!(status.success()); + /// Stop QEMU immediately and reap it. + pub(super) async fn kill(&mut self) { + let _ = self.child.kill().await; } - pub(super) async fn shutdown(&self) { + /// Ask the guest to power off over QMP. + pub(super) async fn shutdown(&self) -> Result<()> { let stream = UnixStream::connect(self.runtime_dir.path().join("qmp.sock")) .await - .unwrap(); + .context("connect to QMP")?; let (reader, mut writer) = stream.into_split(); let mut reader = BufReader::new(reader); let mut response = String::new(); - reader.read_line(&mut response).await.unwrap(); + reader + .read_line(&mut response) + .await + .context("read QMP greeting")?; writer .write_all(b"{\"execute\":\"qmp_capabilities\"}\r\n") .await - .unwrap(); + .context("negotiate QMP capabilities")?; response.clear(); - reader.read_line(&mut response).await.unwrap(); - assert!(response.contains("\"return\"")); + reader + .read_line(&mut response) + .await + .context("read QMP capabilities response")?; + ensure!( + response.contains("\"return\""), + "unexpected QMP response: {response}" + ); writer .write_all(b"{\"execute\":\"system_powerdown\"}\r\n") .await - .unwrap(); + .context("send system_powerdown")?; loop { response.clear(); - let bytes_read = reader.read_line(&mut response).await.unwrap(); - assert_ne!(bytes_read, 0); + let bytes_read = reader + .read_line(&mut response) + .await + .context("read system_powerdown response")?; + ensure!( + bytes_read != 0, + "QMP closed before system_powerdown returned" + ); if response.contains("\"return\"") { - break; + return Ok(()); + } + } + } + + /// Power the guest off and wait for QEMU to exit. Never panics: a guest + /// that can't be powered off cleanly is killed, and the problem is + /// logged. Use this when tearing down, where there is nothing left to do + /// about a failure. + pub(super) async fn power_off_or_kill(mut self) { + let shutdown = match tokio::time::timeout(QMP_TIMEOUT, self.shutdown()).await { + Ok(result) => result, + Err(_) => Err(anyhow::anyhow!("QMP did not answer within {QMP_TIMEOUT:?}")), + }; + match shutdown { + Ok(()) => { + if let Err(error) = self.wait(POWER_OFF_TIMEOUT).await { + eprintln!("tmachine: {error:#}"); + } + } + Err(error) => { + eprintln!("tmachine: could not power off the VM cleanly, killing it: {error:#}"); + self.kill().await; } } } } -fn command(image: &QemuImage, qmp_socket: &Path, firmware_vars: Option<&Path>) -> Command { +fn command( + image: &QemuImage, + qmp_socket: &Path, + firmware_vars: Option<&Path>, + ssh_port: u16, +) -> Command { let qemu = if cfg!(target_arch = "aarch64") { "qemu-system-aarch64" } else { @@ -117,8 +254,10 @@ fn command(image: &QemuImage, qmp_socket: &Path, firmware_vars: Option<&Path>) - .arg(format!("unix:{},server=on,wait=off", qmp_socket.display())); if let Some(firmware_vars) = firmware_vars { - let firmware_code = option_env!("TMACHINE_FIRMWARE_CODE") - .expect("TMACHINE_FIRMWARE_CODE must be set for ARM builds"); + let firmware_code = firmware_path( + "TMACHINE_FIRMWARE_CODE", + option_env!("TMACHINE_FIRMWARE_CODE"), + ); command .arg("-drive") .arg(format!( @@ -142,7 +281,7 @@ fn command(image: &QemuImage, qmp_socket: &Path, firmware_vars: Option<&Path>) - .arg("-device") .arg("virtio-rng-pci") .arg("-netdev") - .arg("user,id=net0,hostfwd=tcp:127.0.0.1:2222-:22") + .arg(format!("user,id=net0,hostfwd=tcp:127.0.0.1:{ssh_port}-:22")) .arg("-device") .arg("virtio-net-pci,netdev=net0"); @@ -150,15 +289,30 @@ fn command(image: &QemuImage, qmp_socket: &Path, firmware_vars: Option<&Path>) - .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()); + // Dropping the handle stops QEMU; the parent-death signal covers the + // cases where no destructor runs. command.kill_on_drop(true); + cleanup::die_with_parent(&mut command); command } +/// A firmware path from the runtime environment, falling back to the value +/// compiled in. The fallback lets library users built outside Nix (such as +/// the test runner) supply firmware at runtime. +fn firmware_path(name: &str, compiled: Option<&'static str>) -> String { + std::env::var(name) + .ok() + .or_else(|| compiled.map(str::to_string)) + .unwrap_or_else(|| panic!("{name} must be set for ARM builds")) +} + fn prepare_firmware(runtime_dir: &Path) -> Option { #[cfg(target_arch = "aarch64")] { - let template = option_env!("TMACHINE_FIRMWARE_VARS") - .expect("TMACHINE_FIRMWARE_VARS must be set for ARM builds"); + let template = firmware_path( + "TMACHINE_FIRMWARE_VARS", + option_env!("TMACHINE_FIRMWARE_VARS"), + ); let firmware_vars = runtime_dir.join("firmware-vars.fd"); fs::copy(template, &firmware_vars).unwrap(); fs::set_permissions(&firmware_vars, fs::Permissions::from_mode(0o600)).unwrap(); @@ -171,3 +325,18 @@ fn prepare_firmware(runtime_dir: &Path) -> Option { None } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn ssh_port_defaults_and_overrides() { + assert_eq!(parse_ssh_port(None), DEFAULT_SSH_PORT); + assert_eq!(parse_ssh_port(Some("2300")), 2300); + assert_eq!(parse_ssh_port(Some(" 2301 ")), 2301); + assert_eq!(parse_ssh_port(Some("0")), DEFAULT_SSH_PORT); + assert_eq!(parse_ssh_port(Some("not-a-port")), DEFAULT_SSH_PORT); + assert_eq!(parse_ssh_port(Some("70000")), DEFAULT_SSH_PORT); + } +}