diff --git a/.github/workflows/cultmesh-portability.yml b/.github/workflows/cultmesh-portability.yml index bafe70fc..0b8aca1d 100644 --- a/.github/workflows/cultmesh-portability.yml +++ b/.github/workflows/cultmesh-portability.yml @@ -158,7 +158,7 @@ jobs: # (only the interop example built, inside the C# store tests). The target directory is under RUNNER_TEMP, away from the checkout. - name: Verify the CultCache document owner with the Rust cache shell: bash - run: CARGO_TARGET_DIR="$RUNNER_TEMP/cultcache-rs-target" cargo test --locked --manifest-path packages/cultcache-rs/Cargo.toml + run: CARGO_TARGET_DIR="$RUNNER_TEMP/cultcache-rs-target" cargo test --manifest-path packages/cultcache-rs/Cargo.toml # tests/GameCult.Caching.Tests, the C# reference cache's own suite (about 1,260 tests, under a minute), ran in no # forge workflow; only the Mesh tests above exercised the reference. diff --git a/.github/workflows/cultnet-interop.yml b/.github/workflows/cultnet-interop.yml index 0d193aef..e053e123 100644 --- a/.github/workflows/cultnet-interop.yml +++ b/.github/workflows/cultnet-interop.yml @@ -135,10 +135,10 @@ jobs: python -m unittest discover -s packages/cultmesh-py/tests - name: Verify Rust RUDP reliability - run: cargo test --locked --manifest-path packages/cultnet-rs/Cargo.toml rudp + run: cargo test --manifest-path packages/cultnet-rs/Cargo.toml rudp - name: Verify Rust CultMesh runtime-local surface - run: cargo test --locked --manifest-path packages/cultmesh-rs/Cargo.toml + run: cargo test --manifest-path packages/cultmesh-rs/Cargo.toml - name: Verify C# RUDP reliability run: dotnet test tests/GameCult.Networking.Tests/GameCult.Networking.Tests.csproj --filter "FullyQualifiedName~Rudp" diff --git a/README.md b/README.md index a555aa92..b916cabe 100644 --- a/README.md +++ b/README.md @@ -212,8 +212,8 @@ identity-owned C# session after provider route replacement. Rust package tests: ```powershell -cargo test --locked --manifest-path packages/cultcache-rs/Cargo.toml -cargo test --locked --manifest-path packages/cultnet-rs/Cargo.toml +cargo test --manifest-path packages/cultcache-rs/Cargo.toml +cargo test --manifest-path packages/cultnet-rs/Cargo.toml ``` Kotlin package build: diff --git a/docs/mutation-testing.md b/docs/mutation-testing.md index 9136b667..8db67be5 100644 --- a/docs/mutation-testing.md +++ b/docs/mutation-testing.md @@ -3,7 +3,7 @@ CultLib measures its C# test suites with Stryker.NET, pinned as a repo-local tool in `dotnet-tools.json`. Coverage is added project by project; a project with no `stryker-config.json` next to its test project has not been scoped -yet (see "Scope and follow-ups" below for what's outstanding). +yet (see "Follow-ups" below for what's outstanding). ## Running it @@ -126,17 +126,14 @@ project as precedent for the `vstest` caveat itself - that caveat is `mtp`-runner-specific and documented above for `static readonly` field initializers, and GameCult.Geometry has never actually hit it. -## Scope and follow-ups +## Follow-ups -CultMath.Tests, GameCult.Geometry.Tests and GameCult.Caching.Tests are the -projects currently scoped. GameCult.Geometry's `stryker-config.json` has no -`test-runner` entry: the project is NUnit 4 with `NUnit3TestAdapter` and +CultMath.Tests and GameCult.Geometry.Tests are the only projects currently +scoped. GameCult.Geometry's `stryker-config.json` has no `test-runner` +entry: the project is NUnit 4 with `NUnit3TestAdapter` and `Microsoft.NET.Test.Sdk`, so Stryker's default `vstest` runner applies directly (no `mtp` workaround needed, unlike CultMath.Tests's xunit.v3 -suite). GameCult.Caching.Tests is the same shape (NUnit, default `vstest`) -and its `stryker-config.json` is driven the same way, by the repo-local -`dotnet-stryker` tool: `dotnet tool restore` once, then `dotnet tool run -dotnet-stryker` from `tests/GameCult.Caching.Tests`. +suite). GameCult.Geometry has only ever been run `--since:`-scoped to the cut that added surface nets; a full-project baseline (running mutation against all of @@ -154,83 +151,7 @@ file, anything under `Fixtures/`) loses `--since` line-level scoping for that run; a future cut whose diff is C#-only, against a base commit that already has `stryker-config.json`, should scope correctly. -### GameCult.Caching.Tests - -`tests/GameCult.Caching.Tests/stryker-config.json` mutates all of -`src/GameCult.Caching` (the project is `GameCult.Caching.csproj`; Standard -mutation level, `break` 0, the same thresholds as Geometry). Caching was -scoped before Networking because its suite is deterministic and quick (1415 -tests, about 40 s) over about 7.8k lines of source, where GameCult.Networking -is about 20k lines and its tests lean on socket timing, so a mutant timing out -there says little about the test. A full Caching run is about 2,900 candidate -mutants (3,602 created, minus 730 that fail to compile) at 1.25 to 1.6 s -each, so about 1 to 1.3 hours; run it detached and read the JSON report, -never as one attached session. - -The first measurement was a narrow pass over the three files that carry the -store-replace and element-id machinery, from `tests/GameCult.Caching.Tests`: - -```bash -dotnet tool run dotnet-stryker \ - -m '**/src/GameCult.Caching/StoreReplace.cs' \ - -m '**/src/GameCult.Caching/CultStoreLinks.cs' \ - -m '**/src/GameCult.Caching/CultElementIds.cs' -``` - -A clean single run of that command tests 243 to 265 mutants (the count moves with the revision) and scored 85.28% on -568a60fa, before any test of this scope was added; 86.79% on d38b5728, with -the first batch of tests; and 87.17% on 76d9e7d1, with the throwing-enumerator -case, in about 10 minutes. The first measurement of it overlapped two Stryker -runs, so its numbers (and the 114 survivors it showed for `CultElementIds.cs`, -where tests that kill a mutant by hand reported `Survived` or `Timeout`) are -not measurements and are not quoted here. Never run two Stryker runs against -one checkout at once, and check `ps` before starting one. A clean single run -on `CultElementIds.cs` alone, after the tests below, tested 228 mutants: 208 -`Killed`, 8 `Timeout`, 12 `Survived`, a score of 94.74%. The 12th survivor -was 321, which the throwing-enumerator case in `ListsOfValuesHoldNoIds` now -kills; the other 11 are equivalent mutants. With that case a clean single run -on `CultElementIds.cs` is 209 `Killed`, 8 `Timeout`, 11 `Survived`, 95.18%. - -Real gaps that run exposed, each now pinned by `ElementIdWalkTests` (and each -failing when its mutant is applied by hand): the order of the id path (the -codec refuses at the first type MessagePack cannot allow, so order is what an -error names first; `Append` to `Prepend` at 163, 167 and 180); the -union-cycle guard in `ConcreteTypes` (248; the mutant is a stack overflow, so -the red is a crashed test host: a depth guard would need production code in -`ConcreteTypes`, since the recursion is private and a stack overflow cannot be -caught by the test, so the regression test is left to crash loudly); a list of values reporting that it holds an -id (`Holds`, 322); `[Key]` read through an override (`KeyOf`, 583); and the -`IsWrittenWhole` and `IsKeyed` predicates (629 to 631 and 636: formatter, -`[MessagePackObject]` and `KeyAsPropertyName` inherited from a base, and the -`[DataContract]` without `[MessagePackObject]` and `||` arms). - -Equivalent survivors in `CultElementIds.cs` (11), each hand-checked against the full -suite; none can be told apart by any test: - -- 142, 251, 636 (second arm), 506 (`IsDefined(UnionAttribute, false)` to - `true`): `UnionAttribute` is `Inherited = false`, so its `inherit` argument - changes nothing. Likewise 630's `DataContractAttribute` `inherit` argument - (`DataContractAttribute` is `Inherited = false`). -- 264, 542: `group.First()` to `FirstOrDefault()` over `GroupBy` groups, which - are never empty. -- 491: `new StringBuilder(bytes.Length * 2)` to `/ 2` changes only the initial - capacity. -- 578: removing the `Nullable.GetUnderlyingType` unwrap in `WalksAsOwner`; - `Classify` unwraps nullables itself, so the unwrap is dead. A cut - candidate, not a test gap. -- 645, 657: `Append(type)` to `Prepend(type)` in `ElementTypeOf` and - `MapTypes`; `type` matches only when it is itself `IEnumerable<>`, - `IDictionary<,>` or `IReadOnlyDictionary<,>`, and none of those has a second - matching interface, so the position of `type` never decides the answer. - -The Windows-only P/Invoke group is exercised only on Windows: its mutants are -`NoCoverage` on Linux and are a target for a Windows-leg mutation run, not a -Linux gap or a finding. `StoreReplace.cs` line 45 is a different case: its -negated `IsOSPlatform(Windows)` mutant is `Survived` on Linux, because the -negated branch calls `ReplaceWindows`, whose `DllNotFound` fallback gives the -same result. - -`GameCult.Mesh` (+ `.Quic`, `.Quic.Native`), +`GameCult.Caching`, `GameCult.Mesh` (+ `.Quic`, `.Quic.Native`), `GameCult.Networking` (+ `.WebSockets`), and the non-.NET runtimes (`cultcache-rs/py/ts`, `cultmesh-*`, `cultnet-*`) each still need their own `stryker-config.json` (or ecosystem-equivalent tool: `cargo-mutants` for diff --git a/docs/start/README.md b/docs/start/README.md index e9dc0b3f..6a7d6d74 100644 --- a/docs/start/README.md +++ b/docs/start/README.md @@ -16,7 +16,7 @@ this page. | | CultCache | CultMath | Geometry | CultNet | CultMesh | | --- | --- | --- | --- | --- | --- | -| **C#** | [first call, tested](#c-first-call); [update, tested](#c-update-with-retry) | [package](../../packages/cultmath/README.md); snippet not yet | [package](../../src/GameCult.Geometry/README.md); snippet not yet | [first call, tested](#c-cultnet-first-call); [package](../../src/GameCult.Networking) | [quickstart](../../src/GameCult.Mesh/docs/durable-node-quickstart.md); run by `scripts/verify-eve-getting-started.ps1` | +| **C#** | [first call, tested](#c-first-call) | [package](../../packages/cultmath/README.md); snippet not yet | [package](../../src/GameCult.Geometry/README.md); snippet not yet | [first call, tested](#c-cultnet-first-call); [package](../../src/GameCult.Networking) | [quickstart](../../src/GameCult.Mesh/docs/durable-node-quickstart.md); run by `scripts/verify-eve-getting-started.ps1` | | **TypeScript** | [first call, tested](#typescript-first-call) | not yet | not yet | [first call, tested](#typescript-cultnet-first-call); [package](../../packages/cultnet-ts/README.md) | [package](../../packages/cultmesh-ts/README.md); snippet not yet; [recipe: two-client .io game](io-game.md), tested with two live clients, browser page not yet run | | **Rust** | [first call, tested](#rust-first-call) | [native core](../../packages/cultmath/native/cultmath-core); snippet not yet | not yet | [first call, tested](#rust-cultnet-first-call); [package](../../packages/cultnet-rs/README.md), [examples](../../packages/cultnet-rs/examples) | [package](../../packages/cultmesh-rs); snippet not yet | | **Python** | [first call, tested](#python-first-call) | not yet | not yet | [first call, tested](#python-cultnet-first-call); [package](../../packages/cultnet-py/README.md) | [package](../../packages/cultmesh-py/README.md); snippet not yet | @@ -70,37 +70,6 @@ public static class FirstCall `FirstCall.Run` is the call; a second test in that file fails if the block above stops matching it. More: `src/GameCult.Caching/README.md`. -## C# update with retry - -Change a record you may not be the only one changing. `Update` reads the record, hands your function a private -copy, and commits the result only if the record is still the one it read. If another writer got there first, it -reads again and runs your function again, up to five times. The source is -[`tests/GameCult.Caching.Tests/UpdateCallTests.cs`](../../tests/GameCult.Caching.Tests/UpdateCallTests.cs); the -snippet reuses `Item` from the first call. - -```csharp -using GameCult.Caching; -using GameCult.Caching.MessagePack; - -public static class UpdateCall -{ - public static CultUpdateResult Restock(string path, CultRecordKey key) - { - var cache = CultCacheMessagePack.Create(path); - return cache.Update(key, item => - { - item ??= new Item { Name = "Potion", Category = "Consumable" }; - item.Value += 10; - return item; - }); - } -} -``` - -The write is durable when `Update` returns, with no `PushAll`. A lost race re-runs the function, so keep it free of -side effects; when the attempts run out the result says `LostRace` instead of throwing. `UpsertAsync` plus -`PushAll` is the blind opt-out: last writer wins. - ## TypeScript first call Same shape in TypeScript: the document is a name, a schema id and a `parse`-able schema (zod here), @@ -155,7 +124,7 @@ The test imports from `../src/index` where the block reads `@gamecult/cultcache- Same shape with `derive(DatabaseEntry)`: slots are `#[cultcache(key = N)]`, so the `.cc` payload is the slot-indexed array the C# runtime writes. The source is [`packages/cultcache-rs/tests/first_call.rs`](../../packages/cultcache-rs/tests/first_call.rs), run by -`cargo test --locked --test first_call` in `packages/cultcache-rs`; a second test in that file fails if the block +`cargo test --test first_call` in `packages/cultcache-rs`; a second test in that file fails if the block below stops matching the file. The Rust runtime has no name lookup, so the read-back is by key. More: `packages/cultcache-rs/README.md`. @@ -372,7 +341,7 @@ async function firstCall() { ### Rust CultNet first call Source: [`packages/cultnet-rs/tests/first_call.rs`](../../packages/cultnet-rs/tests/first_call.rs), run by -`cargo test --locked --test first_call` in `packages/cultnet-rs`. More: `packages/cultnet-rs/README.md`. +`cargo test --test first_call` in `packages/cultnet-rs`. More: `packages/cultnet-rs/README.md`. ```rust use std::net::{TcpListener, TcpStream}; diff --git a/packages/cultcache-py/CHANGELOG.md b/packages/cultcache-py/CHANGELOG.md index 77324c91..84d454bf 100644 --- a/packages/cultcache-py/CHANGELOG.md +++ b/packages/cultcache-py/CHANGELOG.md @@ -17,7 +17,7 @@ ### Breaking -- `push_all(envelopes, soft=False)` on the single-file and JSONL stores and on the `BackingStore` protocol takes the soft flag, as TypeScript, Rust and C# do: a soft push writes only where nothing is at the path; over a store the reader reads, empty included, it returns without writing, and over a file the reader cannot read it refuses (`StoreUnreadableError` for the single-file store). The `BackingStore` protocol has `pull_or_absent()`: the envelopes the reader reads, `None` when nothing is at the path, a raise when the path cannot be read; its default is `pull_all()`, which cannot say absent, so a store that inherits it is always read. The default `push_all` decides soft by it (writes where it says absent, skips over a store it read, refuses where it raises), and the single-file and JSONL stores expose their pull through it; an inheriting store that can tell nothing-at-the-path overrides `pull_or_absent`, not `push_all`. Before, a soft push through the default always skipped. A store kind that overrides `push_all(envelopes)` without the keyword fails when a caller passes `soft`. +- `push_all(envelopes, soft=False)` on the single-file and JSONL stores and on the `BackingStore` protocol takes the soft flag, as TypeScript, Rust and C# do: a soft push writes only where nothing is at the path; over a store the reader reads, empty included, it returns without writing, and over a file the reader cannot read it refuses (`StoreUnreadableError` for the single-file store). The `BackingStore` protocol's default `push_all` cannot tell an absent store from an empty one (a pull returns a list), so a soft push through it always skips a store whose pull returns; a store kind that can tell nothing-at-the-path overrides `push_all`. A store kind that overrides `push_all(envelopes)` without the keyword fails when a caller passes `soft`. - A write at a store path that is a link, live or dangling, is refused with the new `StoreLinkedError` (an `OSError`, exported from `cultcache_py`; `.path` names the path) before the lock and the replace, for the single-file store and for `JsonLinesBackingStore` (`push`, `push_all`, `delete`), and nothing is written. Before, a write at a live link replaced the link with a regular file, and at a dangling link both stores raised `FileNotFoundError`. A caller catching `FileNotFoundError` there now catches `StoreLinkedError` (`OSError`); configure the link's target as the store path. On Windows a junction or any reparse point counts as a link. Reads through a live link work. - A record whose payload carries a slot past its reader's declared slots, at any depth (the document, a nested record, a list element, a union arm, a unit arm), is read, held read-only, and every write or removal of its key (`put`, `put_global`, `update`, `put_envelope`, `put_envelopes`, `delete`, `delete_global`) is refused with `SchemaConflictError` before any store is touched (`cultlib-gaps:ruling:extra-trailing-slots`). Reading dropped those slots silently and the next write shed them. `SchemaConflictError.unread_slots` is an `UnreadSlots(type_name, slot_count, declared_count, occurrences, depth0_only)` and the message names the type and both counts, never a slot value. A depth-0 mark is consented, and the record writable, only when the record is stored under an id the document lists in `compatible_schema_ids` and that id's catalog entry has slots past the document's, covering the mark's slot count (`cultlib-gaps:ruling:slot-mark-consent`); a nested mark, or a mark under the document's own id, keeps the record read-only whatever is declared. Missing trailing slots keep their declared defaults. An untouched marked record keeps its stored bytes through every other write. Added `CultCache.is_writable(document, key)` (advisory; the write still decides: it also answers False for a foreign-carried key, for a key held as a type the document does not own, and for a global document at a non-global key; `delete` asks the same judges, so removing a key held as another type is refused with `SchemaConflictError` unless the document owns that record's schema id (its own or one it lists compatible), in which case the removal removes that record, its file row and its lookups, as a write of the key would replace it; it returned silently and removed nothing), `DocumentDefinition.decode_payload_marked`, and `UnreadSlots`. A document with a custom `decode` (no slot codec) never marks. A store holding a variant record is still unreadable here. Implies a minor bump while the version is 0.x (0.3.0 to 0.4.0): code that wrote back a record from a newer writer now gets `SchemaConflictError`. - A payload is one MessagePack value: `DocumentDefinition.decode_payload` refuses bytes after it, by count, with `ValueError` ` payload has N bytes after its first MessagePack value (refused on read).` (never the bytes, never the depth refusal; a payload that ends early stays the decoder's), as the C# reference does (`tests/vectors/union-parity/bytes-after-outcomes.msgpack`). It raised `msgpack.ExtraData` (a `ValueError`, so only a caller naming `msgpack.ExtraData` is affected); this is a minor-version bump while pre-1.0. The refused set is unchanged. Cache loads, `get` and every definition built on the MessagePack decoder go through it; a user `payload_decoder` that raises `ExtraData` is refused the same way. Depth is judged first, so a value nested past 256 with a tail is the depth refusal, as in C#. diff --git a/packages/cultcache-py/src/cultcache_py/backing_store.py b/packages/cultcache-py/src/cultcache_py/backing_store.py index bded4b11..7b07e7be 100644 --- a/packages/cultcache-py/src/cultcache_py/backing_store.py +++ b/packages/cultcache-py/src/cultcache_py/backing_store.py @@ -67,19 +67,12 @@ class BackingStore(Protocol): def delete(self, type: str, key: str) -> None: ... - def pull_or_absent(self) -> list[CultCacheEnvelope] | None: - """The envelopes the reader reads (empty included), None when nothing is at the path; raises when the path cannot be read. - - The default is pull_all, which cannot say absent: a store that inherits it is always a store the reader read. A store kind - that can tell nothing-at-the-path overrides this, and the default push_all then writes where nothing is on a soft push.""" - return self.pull_all() - def push_all(self, envelopes: list[CultCacheEnvelope], soft: bool = False) -> None: - # Soft: write only where pull_or_absent says nothing is; skip over a store it read, empty included; its raise refuses. - pulled = self.pull_or_absent() - if soft and pulled is not None: + # A pull that returns is a store the reader read, empty included: the protocol cannot say "absent", so a soft push + # through this default always skips. A store that can tell nothing-at-the-path overrides push_all. + pulled = self.pull_all() + if soft: return - pulled = pulled or [] existing = {(envelope.type, envelope.key): envelope for envelope in pulled} for envelope in envelopes: existing[(envelope.type, envelope.key)] = envelope diff --git a/packages/cultcache-py/src/cultcache_py/stores.py b/packages/cultcache-py/src/cultcache_py/stores.py index 894252c8..bc3e5429 100644 --- a/packages/cultcache-py/src/cultcache_py/stores.py +++ b/packages/cultcache-py/src/cultcache_py/stores.py @@ -78,9 +78,6 @@ class JsonLinesBackingStore: def pull_all(self) -> list[CultCacheEnvelope]: return self._pull() or [] - def pull_or_absent(self) -> list[CultCacheEnvelope] | None: - return self._pull() - def _pull(self) -> list[CultCacheEnvelope] | None: """The envelopes the reader reads, or None when nothing is at the path.""" with self._lock: @@ -155,9 +152,6 @@ class SingleFileMessagePackBackingStore: def pull_all(self) -> list[CultCacheEnvelope]: return self._pull() or [] - def pull_or_absent(self) -> list[CultCacheEnvelope] | None: - return self._pull() - def _pull(self) -> list[CultCacheEnvelope] | None: """The envelopes the reader reads, or None when nothing is at the path.""" with self._lock: diff --git a/packages/cultcache-py/tests/test_soft_push_all.py b/packages/cultcache-py/tests/test_soft_push_all.py index d30483a3..9a2c6e03 100644 --- a/packages/cultcache-py/tests/test_soft_push_all.py +++ b/packages/cultcache-py/tests/test_soft_push_all.py @@ -69,33 +69,8 @@ class SoftPushAllTests(unittest.TestCase): JsonLinesBackingStore(self.path).push_all([envelope("a")], soft=True) self.assertEqual(junk, self.path.read_bytes()) - def test_the_protocol_default_push_all_decides_soft_by_what_pull_or_absent_says(self) -> None: + def test_the_protocol_default_push_all_skips_what_its_pull_read_empty_included(self) -> None: class Memory(BackingStore): - def __init__(self) -> None: - self.items: list[CultCacheEnvelope] | None = None # None: nothing at the path - - def pull_all(self) -> list[CultCacheEnvelope]: - return list(self.items or []) - - def pull_or_absent(self) -> list[CultCacheEnvelope] | None: - return None if self.items is None else list(self.items) - - def _replace_all(self, envelopes: list[CultCacheEnvelope]) -> None: - self.items = list(envelopes) - - store = Memory() - store.push_all([envelope("a")], soft=True) # nothing at the path: writes - self.assertEqual(["a"], [item.key for item in store.items or []]) - store.push_all([envelope("b")], soft=True) # a store with records: skips - self.assertEqual(["a"], [item.key for item in store.items or []]) - store.items = [] - store.push_all([envelope("b")], soft=True) # a store the reader read, empty: skips - self.assertEqual([], store.items) - store.push_all([envelope("b")]) # plain: lands - self.assertEqual(["b"], [item.key for item in store.items]) - - def test_a_store_that_only_pulls_all_is_read_so_a_soft_push_skips_it(self) -> None: - class PullOnly(BackingStore): def __init__(self) -> None: self.items: list[CultCacheEnvelope] = [] @@ -105,18 +80,14 @@ class SoftPushAllTests(unittest.TestCase): def _replace_all(self, envelopes: list[CultCacheEnvelope]) -> None: self.items = list(envelopes) - store = PullOnly() - store.push_all([envelope("a")], soft=True) + store = Memory() + store.push_all([envelope("a")], soft=True) # the pull read an empty store: skips self.assertEqual([], store.items) - - def test_the_single_file_and_jsonl_stores_say_absent_and_read_through_pull_or_absent(self) -> None: - for make in (SingleFileMessagePackBackingStore, JsonLinesBackingStore): - with self.subTest(make.__name__): - path = self.dir / make.__name__ - store = make(path) - self.assertIsNone(store.pull_or_absent()) - store.push_all([envelope("a")]) - self.assertEqual(["a"], [item.key for item in store.pull_or_absent() or []]) + store.push_all([envelope("a")]) # plain: lands + store.push_all([envelope("b")], soft=True) # a store with records: skips + self.assertEqual(["a"], [item.key for item in store.items]) + store.push_all([envelope("b")]) + self.assertEqual(["a", "b"], sorted(item.key for item in store.items)) def test_the_protocol_default_push_all_refuses_where_its_pull_refuses(self) -> None: class Unreadable(BackingStore): @@ -125,9 +96,6 @@ class SoftPushAllTests(unittest.TestCase): def pull_all(self) -> list[CultCacheEnvelope]: raise StoreUnreadableError(Path("x"), ValueError("junk")) - def pull_or_absent(self) -> list[CultCacheEnvelope] | None: - raise StoreUnreadableError(Path("x"), ValueError("junk")) - def _replace_all(self, envelopes: list[CultCacheEnvelope]) -> None: Unreadable.replaced = True diff --git a/packages/cultcache-py/tests/test_store_lock.py b/packages/cultcache-py/tests/test_store_lock.py index 364106bf..50b95436 100644 --- a/packages/cultcache-py/tests/test_store_lock.py +++ b/packages/cultcache-py/tests/test_store_lock.py @@ -1,7 +1,6 @@ from __future__ import annotations import os -import select import shutil import subprocess import sys @@ -19,7 +18,6 @@ from cultcache_py.backing_store import CultCacheEnvelope REPO = Path(__file__).resolve().parents[3] TS_STORE_FILE = REPO / "packages" / "cultcache-ts" / "dist" / "store-file.js" HOLD_SECONDS = 1.5 -HELD_WAIT_SECONDS = 3 # A holder that takes the lock the way the Rust runtime does (an exclusive file lock), not through this package. PY_HOLDER = """ @@ -45,14 +43,7 @@ def envelope(key: str) -> CultCacheEnvelope: def start_holder(argv: list[str]) -> subprocess.Popen[str]: holder = subprocess.Popen(argv, stdout=subprocess.PIPE, text=True) - assert holder.stdout is not None - ready, _, _ = select.select([holder.stdout], [], [], HELD_WAIT_SECONDS) # a holder blocked on a leaked lock never prints - if not ready: - holder.kill() - holder.wait() - holder.stdout.close() - raise AssertionError(f"the holder never took {argv[-2]}.lock within {HELD_WAIT_SECONDS} s: an earlier writer leaked the lock") - assert holder.stdout.readline().strip() == "HELD" + assert holder.stdout is not None and holder.stdout.readline().strip() == "HELD" holder.held_at = time.monotonic() # type: ignore[attr-defined] return holder diff --git a/packages/cultcache-rs/.gitignore b/packages/cultcache-rs/.gitignore index ea8c4bf7..96ef6c0b 100644 --- a/packages/cultcache-rs/.gitignore +++ b/packages/cultcache-rs/.gitignore @@ -1 +1,2 @@ /target +Cargo.lock diff --git a/packages/cultcache-rs/Cargo.lock b/packages/cultcache-rs/Cargo.lock deleted file mode 100644 index 40d9e1e8..00000000 --- a/packages/cultcache-rs/Cargo.lock +++ /dev/null @@ -1,730 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "android_system_properties" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" -dependencies = [ - "libc", -] - -[[package]] -name = "anyhow" -version = "1.0.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" - -[[package]] -name = "ar_archive_writer" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523e5e187fff36fcb8d1d19ccf0ca4ca43ec3d158316057f66dc6f72716bc5ec" -dependencies = [ - "object", -] - -[[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 = "bumpalo" -version = "3.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" - -[[package]] -name = "cc" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ff2a8126fff0f8c839080d619675a585e61720bb12d2baceae45769fd66f155" -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 = "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 = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "cultcache-rs" -version = "0.3.1" -dependencies = [ - "anyhow", - "chrono", - "cultcache-rs-derive", - "fs2", - "pretty_assertions", - "redb", - "rmp-serde", - "serde", - "serde_bytes", - "serde_json", - "serde_stacker", - "stacker", - "tempfile", - "uuid", - "windows-sys 0.59.0", -] - -[[package]] -name = "cultcache-rs-derive" -version = "0.2.0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "diff" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" - -[[package]] -name = "errno" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e35d6e78f4cebb90e51d543db8fe41528851d6deeef6ef21ebd4bfdc1e784776" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[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.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" - -[[package]] -name = "fs2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "futures-core" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" - -[[package]] -name = "futures-task" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" - -[[package]] -name = "futures-util" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" -dependencies = [ - "futures-core", - "futures-task", - "pin-project-lite", - "slab", -] - -[[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 = "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 = "itoa" -version = "1.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bd1574e617a9a6c1f8f8181bd7e841e1f20e4277607ef28945765188d6284d4" - -[[package]] -name = "js-sys" -version = "0.3.107" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dc09d2154098bbf4ef54eab6b117a3c24cbbffe94046b16ee6d6d4510584d3a" -dependencies = [ - "cfg-if", - "futures-util", - "wasm-bindgen", -] - -[[package]] -name = "libc" -version = "0.2.191" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" - -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - -[[package]] -name = "log" -version = "0.4.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c" - -[[package]] -name = "memchr" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "object" -version = "0.39.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "pretty_assertions" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" -dependencies = [ - "diff", - "yansi", -] - -[[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 = "psm" -version = "0.1.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dcd034599e63b970727f70d79e02d62390a4a84f7c6b827c27c46d5ac3fa622" -dependencies = [ - "ar_archive_writer", - "cc", -] - -[[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 = "redb" -version = "4.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b317ef681e18f6091efbfab532eaf924a135c492d9d0299d0fbfae07e92a39b" -dependencies = [ - "libc", -] - -[[package]] -name = "rmp" -version = "0.8.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" -dependencies = [ - "num-traits", -] - -[[package]] -name = "rmp-serde" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" -dependencies = [ - "rmp", - "serde", -] - -[[package]] -name = "rustix" -version = "1.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5e76742e5294153d4dde1207dbec5bdd81eef6fabdc0d2118babadabfd858aa" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustversion" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" - -[[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_bytes" -version = "0.11.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" -dependencies = [ - "serde", - "serde_core", -] - -[[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.10", -] - -[[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 = "serde_stacker" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4936375d50c4be7eff22293a9344f8e46f323ed2b3c243e52f89138d9bb0f4a" -dependencies = [ - "serde", - "serde_core", - "stacker", -] - -[[package]] -name = "shlex" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "stacker" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707f49d46706bacf8a2b00d51dace3f9de527c13eec3778f570c411f89e69967" -dependencies = [ - "cc", - "cfg-if", - "libc", - "psm", - "windows-sys 0.61.2", -] - -[[package]] -name = "syn" -version = "2.0.119" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "3.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae9ccbea3a7b0b09443036490c8f34a8b1b760fd345e5c0dae0aac718faea79" -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 0.61.2", -] - -[[package]] -name = "unicode-ident" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" - -[[package]] -name = "uuid" -version = "1.29.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bed99139f7dac9b5cfe5f17be4d66dc7079a34dee1d74bcc77217119240e267c" -dependencies = [ - "getrandom", - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.130" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0181c14ddd6c7b8b0b0132a5f1ea2f32be0d893cd3ea97b6e3b28886b8a3d1c" -dependencies = [ - "cfg-if", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.130" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e931a02a39abdbf66966fc35902b5ed58344750a70885bed4ebb3d8eaf28ed9" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.130" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ca4b54280e455bd831e33a6b8e734baf432fe63fbc35c42ac14ce67a043a53a" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 3.0.10", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.130" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b1f9c3798725016c6ee5f2f57869ec0831e8bbc0ab0310a86e787e97f41389e" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets", -] - -[[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 = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "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 = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -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 = "yansi" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" - -[[package]] -name = "zmij" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/packages/cultcache-rs/src/lib.rs b/packages/cultcache-rs/src/lib.rs index e7d56c10..9ba800e9 100644 --- a/packages/cultcache-rs/src/lib.rs +++ b/packages/cultcache-rs/src/lib.rs @@ -1861,11 +1861,7 @@ impl SingleFileMessagePackBackingStore { fn try_with_exclusive_lock(&self, action: impl FnOnce() -> Result) -> Result> { refuse_linked(&self.path)?; - #[cfg(test)] - run_after_pre_lock_look(); - let Some(lock) = self.try_open_lock_file()? else { - return Ok(None); - }; + let lock = self.open_lock_file()?; match fs2::FileExt::try_lock_exclusive(&lock) { Ok(()) => {} Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => return Ok(None), @@ -1883,31 +1879,19 @@ impl SingleFileMessagePackBackingStore { release_lock(lock, refuse_linked(&self.path).and_then(|()| action())) } - /// Opens the lock file, waiting through a Windows sharing violation the way the lock itself waits. fn open_lock_file(&self) -> Result { - let lock_path = self.prepare_lock_dir()?; - retry_while_busy( - || open_lock_once(&lock_path), - is_sharing_violation, - || std::thread::sleep(std::time::Duration::from_millis(5)), - ) - .with_context(|| format!("failed to open {}", lock_path.display())) - } - - /// The single-attempt open for the try entry: a sharing violation is `None`, the same answer as a held lock. - fn try_open_lock_file(&self) -> Result> { - let lock_path = self.prepare_lock_dir()?; - open_unless_busy(|| open_lock_once(&lock_path), is_sharing_violation) - .with_context(|| format!("failed to open {}", lock_path.display())) - } - - fn prepare_lock_dir(&self) -> Result { let lock_path = self.lock_path(); if let Some(parent) = lock_path.parent() { fs::create_dir_all(parent) .with_context(|| format!("failed to create {}", parent.display()))?; } - Ok(lock_path) + OpenOptions::new() + .create(true) + .read(true) + .truncate(false) + .write(true) + .open(&lock_path) + .with_context(|| format!("failed to open {}", lock_path.display())) } fn lock_path(&self) -> PathBuf { @@ -4562,53 +4546,6 @@ impl ReplaceKernel for WindowsKernel { } } -fn open_lock_once(lock_path: &Path) -> std::io::Result { - OpenOptions::new() - .create(true) - .read(true) - .truncate(false) - .write(true) - .open(lock_path) -} - -/// Windows ERROR_SHARING_VIOLATION (raw os error 32): another handle holds the lock file open without sharing. On Unix -/// errno 32 is EPIPE and is never a busy file. -#[cfg(windows)] -fn is_sharing_violation(error: &std::io::Error) -> bool { - error.raw_os_error() == Some(32) -} - -#[cfg(not(windows))] -fn is_sharing_violation(_error: &std::io::Error) -> bool { - false -} - -/// Repeats `open` while it fails busy, sleeping between tries and without a bound, as the lock waits; any other error surfaces. -fn retry_while_busy( - mut open: impl FnMut() -> std::io::Result, - busy: impl Fn(&std::io::Error) -> bool, - mut sleep: impl FnMut(), -) -> std::io::Result { - loop { - match open() { - Err(error) if busy(&error) => sleep(), - other => return other, - } - } -} - -/// One attempt: `None` when the open fails busy, any other error surfaces. -fn open_unless_busy( - open: impl FnOnce() -> std::io::Result, - busy: impl Fn(&std::io::Error) -> bool, -) -> std::io::Result> { - match open() { - Ok(file) => Ok(Some(file)), - Err(error) if busy(&error) => Ok(None), - Err(error) => Err(error), - } -} - #[cfg(test)] mod replace_with_tests { use super::{replace_with, ReplaceKernel}; @@ -4836,20 +4773,6 @@ fn injected_write_fault(step: WriteStep) -> std::io::Result<()> { Ok(()) } -// The one gap a test cannot hold open from outside: a try-lock never waits, so what happens between its pre-lock look and -// the lock is only reachable by running the test's closure there. -#[cfg(test)] -thread_local! { - static AFTER_PRE_LOCK_LOOK: std::cell::RefCell>> = const { std::cell::RefCell::new(None) }; -} - -#[cfg(test)] -fn run_after_pre_lock_look() { - if let Some(hook) = AFTER_PRE_LOCK_LOOK.with(|hook| hook.borrow_mut().take()) { - hook(); - } -} - #[cfg(not(test))] #[inline(always)] fn injected_write_fault(_step: WriteStep) -> std::io::Result<()> { @@ -8322,138 +8245,6 @@ mod tests { Ok(()) } - fn busy_error() -> std::io::Error { - std::io::Error::from_raw_os_error(32) - } - - fn is_busy(error: &std::io::Error) -> bool { - error.raw_os_error() == Some(32) - } - - #[test] - fn the_lock_open_waits_through_busy_failures_then_opens() { - let (mut tries, mut sleeps) = (0, 0); - let opened = retry_while_busy( - || { - tries += 1; - if tries <= 4 { Err(busy_error()) } else { Ok(tries) } - }, - is_busy, - || sleeps += 1, - ); - assert_eq!(opened.unwrap(), 5); - assert_eq!(sleeps, 4); - } - - #[test] - fn the_lock_open_surfaces_a_non_busy_error_at_once() { - let (mut tries, mut sleeps) = (0, 0); - let error = retry_while_busy::<()>( - || { - tries += 1; - Err(std::io::Error::from(std::io::ErrorKind::PermissionDenied)) - }, - is_busy, - || sleeps += 1, - ) - .expect_err("a non-busy error must surface"); - assert_eq!(error.kind(), std::io::ErrorKind::PermissionDenied); - assert_eq!((tries, sleeps), (1, 0)); - } - - #[test] - fn the_try_open_maps_a_sharing_violation_to_none_without_retrying() { - let mut tries = 0; - let opened = open_unless_busy::<()>( - || { - tries += 1; - Err(busy_error()) - }, - is_busy, - ); - assert!(opened.unwrap().is_none()); - assert_eq!(tries, 1); - let other = open_unless_busy::<()>(|| Err(std::io::Error::from(std::io::ErrorKind::PermissionDenied)), is_busy); - assert!(other.is_err()); - } - - // The try entry must answer at once while another handle holds the lock. The attempt runs on a thread and its - // answer is awaited with a timeout, so an entry that waits fails this test by timeout instead of hanging the suite. - #[cfg(unix)] - #[test] - fn the_try_entry_returns_at_once_while_the_lock_is_held() -> Result<()> { - let temp = tempfile::tempdir()?; - let path = temp.path().join("held.cc"); - let store = SingleFileMessagePackBackingStore::new(&path); - let holder = store.open_lock_file()?; - fs2::FileExt::lock_exclusive(&holder)?; - let (sender, receiver) = std::sync::mpsc::channel(); - let attempt = SingleFileMessagePackBackingStore::new(&path); - std::thread::spawn(move || { - let _ = sender.send(attempt.try_compare_exchange_snapshot(&[], &[snapshot_envelope("b", b"2")])); - }); - let outcome = receiver - .recv_timeout(std::time::Duration::from_secs(3)) - .map_err(|_| anyhow!("the try entry waited on a held lock instead of returning"))?; - assert!(matches!(outcome?, TryCompareExchangeSnapshotOutcome::LockContended)); - assert!(!path.exists(), "the contended attempt wrote"); - let again = store.open_lock_file()?; - assert!(fs2::FileExt::try_lock_exclusive(&again).is_err(), "the held lock was disturbed"); - fs2::FileExt::unlock(&holder)?; - Ok(()) - } - - // Off Windows errno 32 is EPIPE, never a busy file: the predicate is false and the wait surfaces it on the first try. - #[cfg(not(windows))] - #[test] - fn error_32_is_not_busy_off_windows_and_surfaces_at_once() { - assert!(!is_sharing_violation(&busy_error())); - let (mut tries, mut sleeps) = (0, 0); - let error = retry_while_busy::<()>( - || { - tries += 1; - Err(busy_error()) - }, - is_sharing_violation, - || sleeps += 1, - ) - .expect_err("error 32 must surface off Windows"); - assert_eq!(error.raw_os_error(), Some(32)); - assert_eq!((tries, sleeps), (1, 0)); - } - - // The try entry has no wait, so the late window is the gap between its look and its lock: a link put there is refused - // once the lock is held, nothing is written through or over it, and the lock is released. - #[cfg(unix)] - #[test] - fn a_link_put_between_the_try_entrys_look_and_its_lock_is_refused_and_the_lock_is_released() -> Result<()> { - let temp = tempfile::tempdir()?; - let target = temp.path().join("target.cc"); - SingleFileMessagePackBackingStore::new(&target).push(&snapshot_envelope("a", b"1"))?; - let before = fs::read(&target)?; - let dangling = temp.path().join("missing.cc"); - let path = temp.path().join("late.cc"); - for link_to in [target.clone(), dangling.clone()] { - let store = SingleFileMessagePackBackingStore::new(&path); - let (link, at) = (link_to.clone(), path.clone()); - AFTER_PRE_LOCK_LOOK.with(|hook| { - *hook.borrow_mut() = Some(Box::new(move || std::os::unix::fs::symlink(&link, &at).expect("put the link"))); - }); - let error = store - .try_compare_exchange_snapshot(&[], &[snapshot_envelope("b", b"2")]) - .expect_err("the late link must be refused"); - assert!(error.downcast_ref::().is_some(), "{error:#}"); - assert!(fs::symlink_metadata(&path)?.file_type().is_symlink(), "the link was replaced"); - assert_eq!(fs::read(&target)?, before, "the target changed"); - assert!(!dangling.exists(), "the dangling target was created"); - let probe = store.open_lock_file()?; - fs2::FileExt::try_lock_exclusive(&probe).map_err(|e| anyhow!("lock still held: {e}"))?; - drop(probe); - fs::remove_file(&path)?; - } - Ok(()) - } - // A write at a store path that is a link is refused: a replace there would swap the link for a regular file and // detach the store from its target. Reads through a live link keep working. #[cfg(unix)] diff --git a/packages/cultcache-ts/test/cult-cache.test.ts b/packages/cultcache-ts/test/cult-cache.test.ts index dc1ddb33..d3011ee2 100644 --- a/packages/cultcache-ts/test/cult-cache.test.ts +++ b/packages/cultcache-ts/test/cult-cache.test.ts @@ -2106,7 +2106,7 @@ async function readTsInteropStore(file: string): Promise { async function buildInteropPeers(): Promise { if (!(await exists(rustInteropBinary))) { - await execAsync(`"${cargoCommand}" build --locked --quiet --example cultcache_interop`, { + await execAsync(`"${cargoCommand}" build --quiet --example cultcache_interop`, { cwd: cultcacheRsRoot, }); } diff --git a/packages/cultmath/README.md b/packages/cultmath/README.md index 6760a1f5..b144c56d 100644 --- a/packages/cultmath/README.md +++ b/packages/cultmath/README.md @@ -147,8 +147,8 @@ CultMath names with tests instead of copied project-local helper drift. ```powershell dotnet build packages/cultmath/src/CultMath/CultMath.csproj dotnet test packages/cultmath/tests/CultMath.Tests/CultMath.Tests.csproj -cargo test --locked --manifest-path packages/cultmath/native/cultmath-core/Cargo.toml -cargo build --locked --release --manifest-path packages/cultmath/native/cultmath-core/Cargo.toml +cargo test --manifest-path packages/cultmath/native/cultmath-core/Cargo.toml +cargo build --release --manifest-path packages/cultmath/native/cultmath-core/Cargo.toml .\packages\cultmath\scripts\build-unity-package.ps1 ``` diff --git a/packages/cultmesh-ts/test/interop/write-receipt-interop.test.ts b/packages/cultmesh-ts/test/interop/write-receipt-interop.test.ts index 14d9e443..092d5ffb 100644 --- a/packages/cultmesh-ts/test/interop/write-receipt-interop.test.ts +++ b/packages/cultmesh-ts/test/interop/write-receipt-interop.test.ts @@ -122,7 +122,7 @@ const rust: Target = { name: "rust", payload: (text) => encode({ text }), async start() { - if (!prebuilt) await execFileAsync(cargo, ["build", "--locked", "--example", "receipt_interop_server", "--message-format", "short"], { cwd: rustManifestDir }); + if (!prebuilt) await execFileAsync(cargo, ["build", "--example", "receipt_interop_server", "--message-format", "short"], { cwd: rustManifestDir }); const metadata = spawnSync(cargo, ["metadata", "--format-version", "1", "--no-deps"], { cwd: rustManifestDir, encoding: "utf8" }); if (metadata.status !== 0) throw new Error(`cargo metadata failed: ${metadata.stderr}`); const exe = join(JSON.parse(metadata.stdout).target_directory, "debug", "examples", `receipt_interop_server${process.platform === "win32" ? ".exe" : ""}`); diff --git a/packages/cultnet-rs/.gitignore b/packages/cultnet-rs/.gitignore index ea8c4bf7..96ef6c0b 100644 --- a/packages/cultnet-rs/.gitignore +++ b/packages/cultnet-rs/.gitignore @@ -1 +1,2 @@ /target +Cargo.lock diff --git a/packages/cultnet-rs/Cargo.lock b/packages/cultnet-rs/Cargo.lock deleted file mode 100644 index ed0aab43..00000000 --- a/packages/cultnet-rs/Cargo.lock +++ /dev/null @@ -1,1227 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "aead" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" -dependencies = [ - "crypto-common", - "generic-array", -] - -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - -[[package]] -name = "aes-gcm" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" -dependencies = [ - "aead", - "aes", - "cipher", - "ctr", - "ghash", - "subtle", -] - -[[package]] -name = "android_system_properties" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" -dependencies = [ - "libc", -] - -[[package]] -name = "anyhow" -version = "1.0.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" - -[[package]] -name = "ar_archive_writer" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523e5e187fff36fcb8d1d19ccf0ca4ca43ec3d158316057f66dc6f72716bc5ec" -dependencies = [ - "object", -] - -[[package]] -name = "autocfg" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "base64ct" -version = "1.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" - -[[package]] -name = "bitflags" -version = "2.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" - -[[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 = "bumpalo" -version = "3.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" - -[[package]] -name = "cc" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ff2a8126fff0f8c839080d619675a585e61720bb12d2baceae45769fd66f155" -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 = "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 = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", -] - -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - -[[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 = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "rand_core 0.6.4", - "typenum", -] - -[[package]] -name = "ctr" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" -dependencies = [ - "cipher", -] - -[[package]] -name = "cultcache-rs" -version = "0.3.1" -dependencies = [ - "anyhow", - "chrono", - "cultcache-rs-derive", - "fs2", - "redb", - "rmp-serde", - "serde", - "serde_bytes", - "serde_json", - "serde_stacker", - "stacker", - "uuid", - "windows-sys 0.59.0", -] - -[[package]] -name = "cultcache-rs-derive" -version = "0.2.0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "cultnet-rs" -version = "0.3.0" -dependencies = [ - "aes-gcm", - "anyhow", - "base64", - "chrono", - "cultcache-rs", - "ed25519-dalek", - "fs2", - "hmac", - "libc", - "pretty_assertions", - "rand", - "rmp-serde", - "rmpv", - "serde", - "serde_bytes", - "serde_json", - "sha2", - "socket2", - "tempfile", - "uuid", - "windows-sys 0.61.2", - "zeroize", -] - -[[package]] -name = "curve25519-dalek" -version = "4.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" -dependencies = [ - "cfg-if", - "cpufeatures", - "curve25519-dalek-derive", - "digest", - "fiat-crypto", - "rustc_version", - "subtle", - "zeroize", -] - -[[package]] -name = "curve25519-dalek-derive" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "der" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" -dependencies = [ - "const-oid", - "zeroize", -] - -[[package]] -name = "diff" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", - "subtle", -] - -[[package]] -name = "ed25519" -version = "2.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" -dependencies = [ - "pkcs8", - "signature", -] - -[[package]] -name = "ed25519-dalek" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" -dependencies = [ - "curve25519-dalek", - "ed25519", - "serde", - "sha2", - "subtle", - "zeroize", -] - -[[package]] -name = "errno" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e35d6e78f4cebb90e51d543db8fe41528851d6deeef6ef21ebd4bfdc1e784776" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "fastrand" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" - -[[package]] -name = "fiat-crypto" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" - -[[package]] -name = "find-msvc-tools" -version = "0.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" - -[[package]] -name = "fs2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "futures-core" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" - -[[package]] -name = "futures-task" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" - -[[package]] -name = "futures-util" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" -dependencies = [ - "futures-core", - "futures-task", - "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", - "libc", - "wasi", -] - -[[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.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" -dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", -] - -[[package]] -name = "ghash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" -dependencies = [ - "opaque-debug", - "polyval", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[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 = "inout" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" -dependencies = [ - "generic-array", -] - -[[package]] -name = "itoa" -version = "1.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bd1574e617a9a6c1f8f8181bd7e841e1f20e4277607ef28945765188d6284d4" - -[[package]] -name = "js-sys" -version = "0.3.107" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dc09d2154098bbf4ef54eab6b117a3c24cbbffe94046b16ee6d6d4510584d3a" -dependencies = [ - "cfg-if", - "futures-util", - "wasm-bindgen", -] - -[[package]] -name = "libc" -version = "0.2.191" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" - -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - -[[package]] -name = "log" -version = "0.4.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c" - -[[package]] -name = "memchr" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "object" -version = "0.39.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "spki", -] - -[[package]] -name = "polyval" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" -dependencies = [ - "cfg-if", - "cpufeatures", - "opaque-debug", - "universal-hash", -] - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "pretty_assertions" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" -dependencies = [ - "diff", - "yansi", -] - -[[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 = "psm" -version = "0.1.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dcd034599e63b970727f70d79e02d62390a4a84f7c6b827c27c46d5ac3fa622" -dependencies = [ - "ar_archive_writer", - "cc", -] - -[[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 = "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.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" -dependencies = [ - "rand_chacha", - "rand_core 0.9.5", -] - -[[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 = "redb" -version = "4.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b317ef681e18f6091efbfab532eaf924a135c492d9d0299d0fbfae07e92a39b" -dependencies = [ - "libc", -] - -[[package]] -name = "rmp" -version = "0.8.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" -dependencies = [ - "num-traits", -] - -[[package]] -name = "rmp-serde" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" -dependencies = [ - "rmp", - "serde", -] - -[[package]] -name = "rmpv" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a4e1d4b9b938a26d2996af33229f0ca0956c652c1375067f0b45291c1df8417" -dependencies = [ - "rmp", - "serde", - "serde_bytes", -] - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "1.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5e76742e5294153d4dde1207dbec5bdd81eef6fabdc0d2118babadabfd858aa" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustversion" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" - -[[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_bytes" -version = "0.11.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" -dependencies = [ - "serde", - "serde_core", -] - -[[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.10", -] - -[[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 = "serde_stacker" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4936375d50c4be7eff22293a9344f8e46f323ed2b3c243e52f89138d9bb0f4a" -dependencies = [ - "serde", - "serde_core", - "stacker", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "shlex" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" - -[[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 = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der", -] - -[[package]] -name = "stacker" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707f49d46706bacf8a2b00d51dace3f9de527c13eec3778f570c411f89e69967" -dependencies = [ - "cc", - "cfg-if", - "libc", - "psm", - "windows-sys 0.61.2", -] - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "2.0.119" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "3.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae9ccbea3a7b0b09443036490c8f34a8b1b760fd345e5c0dae0aac718faea79" -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 0.4.3", - "once_cell", - "rustix", - "windows-sys 0.61.2", -] - -[[package]] -name = "typenum" -version = "1.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" - -[[package]] -name = "unicode-ident" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" - -[[package]] -name = "universal-hash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" -dependencies = [ - "crypto-common", - "subtle", -] - -[[package]] -name = "uuid" -version = "1.29.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bed99139f7dac9b5cfe5f17be4d66dc7079a34dee1d74bcc77217119240e267c" -dependencies = [ - "getrandom 0.4.3", - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.4+wasi-0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.130" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0181c14ddd6c7b8b0b0132a5f1ea2f32be0d893cd3ea97b6e3b28886b8a3d1c" -dependencies = [ - "cfg-if", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.130" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e931a02a39abdbf66966fc35902b5ed58344750a70885bed4ebb3d8eaf28ed9" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.130" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ca4b54280e455bd831e33a6b8e734baf432fe63fbc35c42ac14ce67a043a53a" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 3.0.10", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.130" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b1f9c3798725016c6ee5f2f57869ec0831e8bbc0ab0310a86e787e97f41389e" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets", -] - -[[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 = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "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 = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -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.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - -[[package]] -name = "yansi" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" - -[[package]] -name = "zerocopy" -version = "0.8.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "zeroize" -version = "1.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" - -[[package]] -name = "zmij" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/packages/cultnet-rs/README.md b/packages/cultnet-rs/README.md index 69a8e70d..c356b97b 100644 --- a/packages/cultnet-rs/README.md +++ b/packages/cultnet-rs/README.md @@ -30,7 +30,7 @@ knock on the contract before touching the furniture. ## Receipts ```powershell -cargo test --locked +cargo test ``` The initial tests prove: diff --git a/packages/cultnet-rs/examples/cultnet_impair/main.rs b/packages/cultnet-rs/examples/cultnet_impair/main.rs index 35a2168d..c2ebd874 100644 --- a/packages/cultnet-rs/examples/cultnet_impair/main.rs +++ b/packages/cultnet-rs/examples/cultnet_impair/main.rs @@ -7,7 +7,7 @@ //! traffic crosses the same schedule. The schedule is `policy.rs`. //! //! ```text -//! cargo run --locked --release --example cultnet_impair -- \ +//! cargo run --release --example cultnet_impair -- \ //! --listen 0.0.0.0:17890 --upstream 127.0.0.1:17990 \ //! --profile tests/impairment/loss-1pct.toml --seed 424242 --metrics loss-1pct.csv //! ``` diff --git a/packages/cultnet-rs/tests/media_fec_bench.rs b/packages/cultnet-rs/tests/media_fec_bench.rs index f0279495..40c9de78 100644 --- a/packages/cultnet-rs/tests/media_fec_bench.rs +++ b/packages/cultnet-rs/tests/media_fec_bench.rs @@ -3,7 +3,7 @@ //! Ignored by default; run in release, where the numbers mean something: //! //! ```text -//! cargo test --locked --release --test media_fec_bench -- --ignored --nocapture +//! cargo test --release --test media_fec_bench -- --ignored --nocapture //! ``` //! //! Each line is nanoseconds per call through the public surface, so record diff --git a/packages/cultnet-ts/test/fixtures/idunn-runtime/README.md b/packages/cultnet-ts/test/fixtures/idunn-runtime/README.md index 86dd821e..7856c78d 100644 --- a/packages/cultnet-ts/test/fixtures/idunn-runtime/README.md +++ b/packages/cultnet-ts/test/fixtures/idunn-runtime/README.md @@ -6,7 +6,7 @@ cultnet-rs only, and Idunn's `IdunnRuntimeActivationLaunch` is that same crate's It needs Linux and a machine-id equal to the `machine-id` file here, because the provider identity is bound to it. On Yggdrasil: - DOCKER_ARGS='--cap-add SYS_ADMIN --security-opt apparmor=unconfined' bash ygg-verify.sh HEAD rust 'cd packages/cultnet-rs && cp /tmp/mid && mount --bind /tmp/mid /etc/machine-id && cargo run --locked -q --example idunn_runtime_fixture /tmp/out' + DOCKER_ARGS='--cap-add SYS_ADMIN --security-opt apparmor=unconfined' bash ygg-verify.sh HEAD rust 'cd packages/cultnet-rs && cp /tmp/mid && mount --bind /tmp/mid /etc/machine-id && cargo run -q --example idunn_runtime_fixture /tmp/out' then copy `/tmp/out` over this directory. `--cap-add SYS_ADMIN` exists only so the bind mount can present the committed `machine-id`, which keeps Yggdrasil's real machine-id out of the repo. Every `presence-*.bin` was accepted by `authenticate_runtime_presence_claim` and correlated by @@ -26,7 +26,7 @@ The generator takes its specification as input, so a consumer needs no patch to specification is command-line flags; `@` splices in a response file (one `--flag value` per line, `#` comments). CultLib's own sets are `fixture.args` here: - cargo run --locked -q --example idunn_runtime_fixture /tmp/out @/fixture.args + cargo run -q --example idunn_runtime_fixture /tmp/out @/fixture.args The consumer keeps its own `.args` file in its repository and runs the same command with the same machine-id bind mount as above. Global flags: `--source-repository`, `--detail`. Each diff --git a/packages/cultnet-ts/test/interop/cultnet-interop.test.ts b/packages/cultnet-ts/test/interop/cultnet-interop.test.ts index 3b8b7bc9..d0e732ce 100644 --- a/packages/cultnet-ts/test/interop/cultnet-interop.test.ts +++ b/packages/cultnet-ts/test/interop/cultnet-interop.test.ts @@ -2795,7 +2795,7 @@ async function buildInteropPeers(): Promise { } async function buildRustInteropPeer(): Promise { - rustInteropPeerBuild ??= execFileAsync(cargoCommand, ["build", "--locked", "--quiet", "--example", "cultnet_interop_peer"], { + rustInteropPeerBuild ??= execFileAsync(cargoCommand, ["build", "--quiet", "--example", "cultnet_interop_peer"], { cwd: cultnetRsRoot, }).then(() => undefined); await rustInteropPeerBuild; diff --git a/packages/cultnet-ts/test/runtime-presence-health.test.ts b/packages/cultnet-ts/test/runtime-presence-health.test.ts index 1d7c4fa6..2dc4a845 100644 --- a/packages/cultnet-ts/test/runtime-presence-health.test.ts +++ b/packages/cultnet-ts/test/runtime-presence-health.test.ts @@ -2,7 +2,7 @@ // // The vectors below were produced by `cultnet-rs`, not written by hand: // -// cargo run --locked --example runtime_presence_vector -p cultnet-rs +// cargo run --example runtime_presence_vector -p cultnet-rs // // That matters. A fixture authored for both sides at once agrees with itself // and proves nothing; the contract is only real if one fork emits the bytes and diff --git a/packages/heimdall-consent/README.md b/packages/heimdall-consent/README.md index 926b7d1a..3782e720 100644 --- a/packages/heimdall-consent/README.md +++ b/packages/heimdall-consent/README.md @@ -7,7 +7,7 @@ plus the rules that need no store to state (`Answer::is_training_data`, `Answer: (comments 682 and 759). Specification only: no collector, tally, export or tombstone hashing lives here. Entry points: `src/lib.rs`; golden vectors `tests/vectors/consent_golden.csv`, run by `tests/golden.rs` -(`cargo test --locked`). Planned, not live: the collector, the tally, the training export. +(`cargo test`). Planned, not live: the collector, the tally, the training export. ## Where each ruling lands (doc lines at aaeb175) diff --git a/packages/heimdall-stake/README.md b/packages/heimdall-stake/README.md index 9b6acb19..5c8ae095 100644 --- a/packages/heimdall-stake/README.md +++ b/packages/heimdall-stake/README.md @@ -11,8 +11,8 @@ adversary, storage, and any projection. Every parameter is a placeholder until t ## First call ``` -cargo run --locked --example first_call # prints the Meta example and a splitting table -cargo test --locked +cargo run --example first_call # prints the Meta example and a splitting table +cargo test ``` ```rust diff --git a/src/GameCult.Caching/CultCache.cs b/src/GameCult.Caching/CultCache.cs index dc0c8b99..469fc7af 100644 --- a/src/GameCult.Caching/CultCache.cs +++ b/src/GameCult.Caching/CultCache.cs @@ -2052,29 +2052,6 @@ namespace GameCult.Caching Contended } - public enum CultUpdateOutcome - { - Updated, - Declined, - LostRace - } - - /// - /// What did. A lost race is a value, not an exception. is the instance the - /// cache holds at the key when Update returns: the written one for Updated, null when the key is absent. - /// - public readonly struct CultUpdateResult where T : class - { - public CultUpdateResult(CultUpdateOutcome outcome, T? document) - { - Outcome = outcome; - Document = document; - } - - public CultUpdateOutcome Outcome { get; } - public T? Document { get; } - } - public sealed class CultCommitRequest { internal CultCommitRequest( @@ -2586,7 +2563,6 @@ namespace GameCult.Caching return UpsertAsync(document, handle); } - // A blind write, last-writer-wins per key; Update is the conditional golden path. // Refused with CultSchemaConflictException when the key holds a record stored under an id no registered type declares. public Task> UpsertAsync(T document, CultRecordHandle? handle = null) { @@ -2663,7 +2639,6 @@ namespace GameCult.Caching /// /// Commits the staged batch. Returns false when a condition failed: nothing was written, changed in memory, or published. - /// Without Expect a commit is blind; retries a conditional one. /// /// /// A commit that succeeds and then fails in a journal or observer still stands and is published, and the writer @@ -2681,59 +2656,6 @@ namespace GameCult.Caching return Land(stage, wait: false); } - /// - /// Reads the record at , applies to a private copy of it, and commits the result only - /// if the record is still the one that was read; when another writer got there first it re-reads and runs - /// again, up to times. The write is durable when Update returns. - /// - /// - /// may run up to times. It gets a private copy (null when the key holds - /// nothing), which it may change and return. It must not write to the cache or have other side effects. Return null to write - /// nothing. A store with staged blind writes refuses Update until it is flushed, as it refuses any conditional commit. The cache - /// needs a codec to copy with. Running out of attempts returns ; every other failure throws. - /// - public CultUpdateResult Update(CultRecordKey key, Func apply, int maxAttempts = 5) where T : class - { - if (apply == null) throw new ArgumentNullException(nameof(apply)); - if (maxAttempts < 1) throw new ArgumentOutOfRangeException(nameof(maxAttempts), maxAttempts, "At least one attempt is required."); - var codec = _codec ?? throw new InvalidOperationException("This cache has no codec; create it with CultCacheMessagePack.Create."); - for (var attempt = 1; ; attempt++) - { - var (version, copy) = Held(() => - { - if (!_entries.TryGetValue(key.Value, out var held)) - return ((string?)null, (T?)null); - if (held.Variant != null || held.Document is not T) - throw new InvalidOperationException($"Record {key.Value} is a variant or not a {typeof(T).Name}; Update cannot copy it."); - return (held.StoredVersion, (T?)codec.Clone(held.Document, held.Descriptor.DocumentType)); - }); - var next = apply(copy); - if (next == null) - return new CultUpdateResult(CultUpdateOutcome.Declined, Get(key)); - CultCommitOutcome outcome; - try - { - outcome = Land(batch => - { - batch.Expected.Add((key, version)); - batch.Upsert(next, new CultRecordHandle(key)); - }, wait: true); - } - catch (CultWriteConflictException conflict) when (conflict.InnerException == null) - { - outcome = CultCommitOutcome.Mismatch; - } - - if (outcome == CultCommitOutcome.Committed) - return new CultUpdateResult(CultUpdateOutcome.Updated, next); - if (outcome != CultCommitOutcome.Mismatch) - throw new InvalidOperationException($"Update of {key.Value} was refused: {outcome}."); - Held(() => { Home(next.GetType())?.PullAll(); return true; }); - if (attempt >= maxAttempts) - return new CultUpdateResult(CultUpdateOutcome.LostRace, Get(key)); - } - } - public CultRecordHandle? TryGetHandle(T document) { if (document == null) throw new ArgumentNullException(nameof(document)); diff --git a/src/GameCult.Caching/CultInspectorModel.cs b/src/GameCult.Caching/CultInspectorModel.cs index 60dec974..7aa6ab74 100644 --- a/src/GameCult.Caching/CultInspectorModel.cs +++ b/src/GameCult.Caching/CultInspectorModel.cs @@ -352,7 +352,7 @@ namespace GameCult.Caching } } - public object Clone(object document, Type type) => _codec.Clone(document, type); + public object Clone(object document, Type type) => _deserialize(type, _serialize(document, type, type)); public CultInspectorEdit BeginEdit(CultStoredDocument record) { diff --git a/src/GameCult.Caching/CultVariants.cs b/src/GameCult.Caching/CultVariants.cs index e5c28bbf..48909c19 100644 --- a/src/GameCult.Caching/CultVariants.cs +++ b/src/GameCult.Caching/CultVariants.cs @@ -29,11 +29,6 @@ namespace GameCult.Caching // (document payload, slot and encoded value pairs) public Func>, byte[]> Overlay { get; } - // The one copy of a document: its encoding, decoded again. Update's private argument and CultInspectorModel.Clone share it. - internal object Clone(object document, Type type) => - Deserialize(type, type, Serialize(document, type, type)) - ?? throw new InvalidOperationException($"The codec returned null for {type.FullName}."); - /// /// Decodes a document payload read from a store file, as a cache load decodes it: the ids a load mints are minted from the key /// and their path, so every reader of one store mints the same ids, and a payload a cache write would refuse is refused here too. diff --git a/src/GameCult.Caching/README.md b/src/GameCult.Caching/README.md index dffce80a..6c76f5cd 100644 --- a/src/GameCult.Caching/README.md +++ b/src/GameCult.Caching/README.md @@ -13,7 +13,6 @@ Cacheable models are plain classes annotated with cache intent. using GameCult.Caching; using MessagePack; -[MessagePackObject] [CultDocument("gamecult.item_data", "gamecult.item_data.v1")] public sealed class ItemData { @@ -132,33 +131,18 @@ using GameCult.Caching; using GameCult.Caching.MessagePack; var cache = await CultCacheMessagePack.OpenAsync("Data.msgpack"); -var key = new CultRecordKey("potion"); -// The golden path: read, change a private copy, commit only if the record is unchanged, re-run on a lost race. -// Durable when it returns; no flush. -var result = cache.Update(key, item => -{ - item ??= new ItemData { Name = "Potion", Sku = "POT-1", Category = "Consumable" }; - item.Value += 10; - return item; -}); - -// The blind opt-out: last writer wins per key. Name the key, or a new key claims the same unique Sku and is refused. var handle = await cache.UpsertAsync(new ItemData { Name = "Potion", Sku = "POT-1", Category = "Consumable", Value = 50 -}, new CultRecordHandle(key)); +}); await cache.FlushAsync(); ``` -`Update` may run your function more than once, so it must have no side effects. Returning null writes nothing, and a -lost race that outlasts `maxAttempts` comes back as `CultUpdateOutcome.LostRace`. `UpsertAsync` plus `FlushAsync` never -checks what it overwrites. - If you want the explicit assembly rite, the lower-level `CultCache` + `SingleFileMessagePackBackingStore` path is still there. The helper is just the blessed front door. @@ -310,10 +294,13 @@ Useful surfaces: - `cache.IsDirty` - `store.IsDirty` - `cache.Document(key)` -- `cache.Update(key, apply)` -- `cache.UpsertAsync(document)` (blind write) +- `cache.Soa()` +- `cache.UpsertAsync(document)` - `cache.FlushAllBackingStores()` - `cache.FlushAsync()` +- `cache.FlushBackingStore(store)` +- `cache.PrepareForReloadOrShutdown()` +- `cache.PrepareForReloadOrShutdownAsync()` - `cache.FlushAttachedStoresOnDispose` - `store.FlushOnDispose` diff --git a/tests/GameCult.Caching.Tests/ElementIdWalkTests.cs b/tests/GameCult.Caching.Tests/ElementIdWalkTests.cs deleted file mode 100644 index bd6cb903..00000000 --- a/tests/GameCult.Caching.Tests/ElementIdWalkTests.cs +++ /dev/null @@ -1,151 +0,0 @@ -#nullable enable -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.Serialization; -using GameCult.Caching; -using MessagePack; -using NUnit.Framework; - -namespace GameCult.Caching.Tests -{ - // The id walk's own contracts, one mutant-sized fact at a time: the order of the id path, a union that names itself, a list of - // values never counting as an id, and the attribute predicates that decide whether a type is walked or written whole. - public class ElementIdWalkTests - { - // The codec asks MessagePack about the id path in order and refuses at the first type it cannot allow, so the order is what - // an error names first: the document, then the collection types from the outermost in, then the element and its arms. - [Test] - public void TheIdPathListsCollectionsFromTheOutermostIn() - { - Assert.That(CultElementIds.Inspect(typeof(MsMapMap)).IdPath, Is.EqualTo(new[] - { - typeof(MsMapMap), typeof(Dictionary>>), - typeof(Dictionary>), typeof(List), typeof(ElementIdEngineTests.EIded) - }), "maps nested in maps"); - Assert.That(CultElementIds.Inspect(typeof(MsListDeep)).IdPath, Is.EqualTo(new[] - { - typeof(MsListDeep), typeof(List>>), typeof(List>), - typeof(List), typeof(ElementIdEngineTests.EIded) - }), "lists nested in lists"); - } - - // ConcreteTypes stops at a type it has already expanded, so a union that lists its own base is finished, not a stack overflow. - [Test] - public void AUnionThatNamesItselfIsWalkedOnce() - { - var survey = CultElementIds.Inspect(typeof(MsSelfHolder)); - Assert.That(survey.Problems, Is.Empty); - Assert.That(survey.IdPath, Is.EquivalentTo(new[] { typeof(MsSelfHolder), typeof(List), typeof(MsSelfBase), typeof(MsSelfArm) })); - } - - // Value lists hold no ids however they are nested, and a root that is itself a value list holds none either. - [Test] - public void ListsOfValuesHoldNoIds() - { - var values = new MsValues { Numbers = { 1 }, Tags = { "a" }, Grid = { new List { 1 } }, Map = { ["k"] = new List { 2 } } }; - Assert.That(CultElementIds.Holds(typeof(MsValues), values), Is.False); - Assert.That(CultElementIds.Holds(typeof(List), new List { 1, 2 }), Is.False); - Assert.That(CultElementIds.Holds(typeof(List>), new List> { new() { 1 } }), Is.False); - Assert.That(CultElementIds.Holds(typeof(List), new List { "a" }), Is.False); - Assert.That(CultElementIds.Holds(typeof(MsBoom), new MsBoom()), Is.False, "a sequence of values is not enumerated"); - } - - // A nullable struct is walked as the struct it wraps; a nullable value is not walked at all. - [Test] - public void ANullableKeyedStructWalksAsItsOwner() - { - Assert.That(CultElementIds.WalksAsOwner(typeof(MsKeyedStruct)), Is.True); - Assert.That(CultElementIds.WalksAsOwner(typeof(MsKeyedStruct?)), Is.True); - Assert.That(CultElementIds.WalksAsOwner(typeof(int?)), Is.False); - Assert.That(CultElementIds.WalksAsOwner(typeof(int)), Is.False); - } - - // [Key] is read through an override, so an element whose id property overrides a keyed base member is still an id with a key. - [Test] - public void AKeyOnTheBaseOfAnOverrideCountsForTheOverride() - { - Assert.That(CultElementIds.Inspect(typeof(MsOverrideHolder)).Problems, Is.Empty); - Assert.That(CultElementIds.Inspect(typeof(MsOverrideHolder)).IdPath, Does.Contain(typeof(MsOverrideEl))); - } - - // A type is walked by key when it is [MessagePackObject] or [Union], read through its bases for the former; it is one whole - // value when it declares its own formatter, is [DataContract] without being [MessagePackObject], or is keyed by name. - [Test] - public void KeyedTypesWalkAndWholeTypesDoNot() - { - Assert.That(CultElementIds.WalksAsOwner(typeof(MsMpBase)), Is.True, "[MessagePackObject]"); - Assert.That(CultElementIds.WalksAsOwner(typeof(MsUBase)), Is.True, "[Union]"); - Assert.That(CultElementIds.WalksAsOwner(typeof(MsMpDerived)), Is.True, "[MessagePackObject] inherited"); - Assert.That(CultElementIds.WalksAsOwner(typeof(MsPlain)), Is.False, "[Key] members without [MessagePackObject] or [Union]"); - Assert.That(CultElementIds.WalksAsOwner(typeof(MsUArm)), Is.True, "an arm that is itself [MessagePackObject]"); - - Assert.That(CultElementIds.WalksAsOwner(typeof(MsDc)), Is.False, "[DataContract] alone"); - Assert.That(CultElementIds.WalksAsOwner(typeof(MsDcMp)), Is.True, "[DataContract] beside [MessagePackObject]"); - Assert.That(CultElementIds.WalksAsOwner(typeof(MsDcOverMpBase)), Is.True, "[DataContract] over an inherited [MessagePackObject]"); - - Assert.That(CultElementIds.WalksAsOwner(typeof(MsPropName)), Is.False, "KeyAsPropertyName"); - Assert.That(CultElementIds.WalksAsOwner(typeof(MsPropNameDerived)), Is.False, "KeyAsPropertyName inherited"); - Assert.That(CultElementIds.WalksAsOwner(typeof(MsStringKey)), Is.False, "a string [Key]"); - - Assert.That(CultElementIds.WalksAsOwner(typeof(MsFormattedBase)), Is.False, "[MessagePackFormatter]"); - Assert.That(CultElementIds.WalksAsOwner(typeof(MsFormattedDerived)), Is.False, "[MessagePackFormatter] inherited"); - } - - [MessagePackObject] public class MsMapMap { [Key(0)] public Dictionary>> M { get; set; } = new(); } - [MessagePackObject] public class MsListDeep { [Key(0)] public List>> G { get; set; } = new(); } - - [Union(0, typeof(MsSelfBase)), Union(1, typeof(MsSelfArm))] public abstract class MsSelfBase { } - [MessagePackObject] public class MsSelfArm : MsSelfBase { [Key(0)] [CultElementId] public string Id { get; set; } = ""; } - [MessagePackObject] public class MsSelfHolder { [Key(0)] public List Items { get; set; } = new(); } - - [MessagePackObject] - public class MsValues - { - [Key(0)] public List Numbers { get; set; } = new(); - [Key(1)] public List Tags { get; set; } = new(); - [Key(2)] public List> Grid { get; set; } = new(); - [Key(3)] public Dictionary> Map { get; set; } = new(); - } - - public sealed class MsBoom : IEnumerable - { - public IEnumerator GetEnumerator() => throw new InvalidOperationException("enumerated"); - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); - } - - [MessagePackObject] public struct MsKeyedStruct { [Key(0)] public int X; } - - [MessagePackObject] - public class MsOverrideBase - { - [Key(0)] [CultElementId] public virtual string Id { get; set; } = ""; - } - - [MessagePackObject] - public class MsOverrideEl : MsOverrideBase - { - public override string Id { get; set; } = ""; - } - - [MessagePackObject] public class MsOverrideHolder { [Key(0)] public List Items { get; set; } = new(); } - - [MessagePackObject] public class MsMpBase { [Key(0)] public int A { get; set; } } - public class MsMpDerived : MsMpBase { } - public class MsPlain { [Key(0)] public int A { get; set; } } - [Union(0, typeof(MsUArm))] public abstract class MsUBase { } - [MessagePackObject] public class MsUArm : MsUBase { [Key(0)] public int A { get; set; } } - - [DataContract] public class MsDc { [DataMember] public int A { get; set; } } - [DataContract] [MessagePackObject] public class MsDcMp { [Key(0)] public int A { get; set; } } - [DataContract] public class MsDcOverMpBase : MsMpBase { } - - [MessagePackObject(keyAsPropertyName: true)] public class MsPropName { public int A { get; set; } } - public class MsPropNameDerived : MsPropName { } - [MessagePackObject] public class MsStringKey { [Key("a")] public int A { get; set; } } - - public sealed class MsNoFormatter { } - [MessagePackFormatter(typeof(MsNoFormatter))] [MessagePackObject] public class MsFormattedBase { [Key(0)] public int A { get; set; } } - [MessagePackObject] public class MsFormattedDerived : MsFormattedBase { [Key(1)] public int B { get; set; } } - } -} diff --git a/tests/GameCult.Caching.Tests/FirstCallTests.cs b/tests/GameCult.Caching.Tests/FirstCallTests.cs index d8febc49..8fad1229 100644 --- a/tests/GameCult.Caching.Tests/FirstCallTests.cs +++ b/tests/GameCult.Caching.Tests/FirstCallTests.cs @@ -62,28 +62,17 @@ namespace GameCult.Caching.Tests } [Test] - public void DocsShowTheTestedSnippet() => StartDocs.AssertShowsSnippet(SourcePath()); - - static string SourcePath([System.Runtime.CompilerServices.CallerFilePath] string path = "") => path; - } - - // docs/start/README.md shows each tested snippet verbatim: the region between snippet:begin and snippet:end of its source file. - internal static class StartDocs - { - public static void AssertShowsSnippet(string sourcePath) => - AssertShowsSnippet(sourcePath, Path.Combine("..", "..", "docs", "start", "README.md")); - - // The same check against another document, its path relative to the directory of sourcePath. The snippet there is the - // statements of a block, so it need not open the fence (the block's usings come first). - public static void AssertShowsSnippet(string sourcePath, string docsRelativePath, bool wholeBlock = true) + public void DocsShowTheTestedSnippet() { - var source = File.ReadAllText(sourcePath).Replace("\r\n", "\n"); + var source = File.ReadAllText(SourcePath()); const string begin = "// snippet:begin\n"; var start = source.IndexOf(begin, StringComparison.Ordinal) + begin.Length; var snippet = source[start..source.IndexOf("// snippet:end\n", StringComparison.Ordinal)]; - var docs = File.ReadAllText(Path.Combine(Path.GetDirectoryName(sourcePath)!, docsRelativePath)); - Assert.That(docs.Replace("\r\n", "\n"), Does.Contain(wholeBlock ? "```csharp\n" + snippet + "```" : snippet), - $"{docsRelativePath} no longer shows {Path.GetFileName(sourcePath)} verbatim"); + var docs = File.ReadAllText(Path.Combine(Path.GetDirectoryName(SourcePath())!, "..", "..", "docs", "start", "README.md")); + Assert.That(docs.Replace("\r\n", "\n"), Does.Contain("```csharp\n" + snippet + "```"), + "docs/start/README.md no longer shows tests/GameCult.Caching.Tests/FirstCallTests.cs verbatim"); } + + static string SourcePath([System.Runtime.CompilerServices.CallerFilePath] string path = "") => path; } } diff --git a/tests/GameCult.Caching.Tests/ReadmeHappyPathTests.cs b/tests/GameCult.Caching.Tests/ReadmeHappyPathTests.cs deleted file mode 100644 index de2dabb4..00000000 --- a/tests/GameCult.Caching.Tests/ReadmeHappyPathTests.cs +++ /dev/null @@ -1,81 +0,0 @@ -#nullable enable -using System; -using System.IO; -using System.Threading.Tasks; -using GameCult.Caching; -using GameCult.Caching.MessagePack; -using MessagePack; -using NUnit.Framework; - -namespace GameCult.Caching.Tests -{ - // The Happy Path of src/GameCult.Caching/README.md, run as written: its statements are the region between snippet:begin and - // snippet:end below, and ReadmeShowsTheTestedSnippet fails when the README stops showing them verbatim. - public class ReadmeHappyPathTests - { - [CultDocument("gamecult.item_data", "gamecult.item_data.v1")] - [MessagePackObject] - public sealed class ItemData - { - [Key(0)] [CultName] public string Name = string.Empty; - [Key(1)] [CultIndex] public string Sku = string.Empty; - [Key(2)] public string Category = string.Empty; - [Key(3)] public int Value; - } - - private static async Task HappyPath() - { -// snippet:begin -var cache = await CultCacheMessagePack.OpenAsync("Data.msgpack"); -var key = new CultRecordKey("potion"); - -// The golden path: read, change a private copy, commit only if the record is unchanged, re-run on a lost race. -// Durable when it returns; no flush. -var result = cache.Update(key, item => -{ - item ??= new ItemData { Name = "Potion", Sku = "POT-1", Category = "Consumable" }; - item.Value += 10; - return item; -}); - -// The blind opt-out: last writer wins per key. Name the key, or a new key claims the same unique Sku and is refused. -var handle = await cache.UpsertAsync(new ItemData -{ - Name = "Potion", - Sku = "POT-1", - Category = "Consumable", - Value = 50 -}, new CultRecordHandle(key)); - -await cache.FlushAsync(); -// snippet:end - return cache; - } - - [Test] - public async Task HappyPathRunsAsWritten() - { - var directory = Path.Combine(Path.GetTempPath(), $"readme-happy-{Guid.NewGuid():N}"); - Directory.CreateDirectory(directory); - var previous = Environment.CurrentDirectory; - try - { - Environment.CurrentDirectory = directory; // the README opens a relative path - using var cache = await HappyPath(); - Assert.That(cache.Get(new CultRecordKey("potion"))!.Value, Is.EqualTo(50)); - Assert.That(cache.GetByIndex("Sku", "POT-1"), Is.Not.Null); - } - finally - { - Environment.CurrentDirectory = previous; - Directory.Delete(directory, recursive: true); - } - } - - [Test] - public void ReadmeShowsTheTestedSnippet() => - StartDocs.AssertShowsSnippet(SourcePath(), Path.Combine("..", "..", "src", "GameCult.Caching", "README.md"), wholeBlock: false); - - static string SourcePath([System.Runtime.CompilerServices.CallerFilePath] string path = "") => path; - } -} diff --git a/tests/GameCult.Caching.Tests/UpdateCallTests.cs b/tests/GameCult.Caching.Tests/UpdateCallTests.cs deleted file mode 100644 index d9a8b31e..00000000 --- a/tests/GameCult.Caching.Tests/UpdateCallTests.cs +++ /dev/null @@ -1,54 +0,0 @@ -#nullable enable -using System; -using System.IO; -using NUnit.Framework; - -// snippet:begin -using GameCult.Caching; -using GameCult.Caching.MessagePack; - -public static class UpdateCall -{ - public static CultUpdateResult Restock(string path, CultRecordKey key) - { - var cache = CultCacheMessagePack.Create(path); - return cache.Update(key, item => - { - item ??= new Item { Name = "Potion", Category = "Consumable" }; - item.Value += 10; - return item; - }); - } -} -// snippet:end - -namespace GameCult.Caching.Tests -{ - // The C# update snippet that docs/start/README.md shows verbatim; it reuses FirstCallTests' Item. - public class UpdateCallTests - { - [Test] - public void RestockTwiceReadsTwenty() - { - var path = Path.Combine(Path.GetTempPath(), $"update-call-{Guid.NewGuid():N}.cc"); - try - { - var key = new CultRecordKey("potion"); - Assert.That(UpdateCall.Restock(path, key).Outcome, Is.EqualTo(CultUpdateOutcome.Updated)); - Assert.That(UpdateCall.Restock(path, key).Outcome, Is.EqualTo(CultUpdateOutcome.Updated)); - - using var reader = CultCacheMessagePack.Create(path); - Assert.That(reader.Get(key)!.Value, Is.EqualTo(20)); - } - finally - { - File.Delete(path); - } - } - - [Test] - public void DocsShowTheTestedSnippet() => StartDocs.AssertShowsSnippet(SourcePath()); - - static string SourcePath([System.Runtime.CompilerServices.CallerFilePath] string path = "") => path; - } -} diff --git a/tests/GameCult.Caching.Tests/UpdateTests.cs b/tests/GameCult.Caching.Tests/UpdateTests.cs deleted file mode 100644 index 0dd96bad..00000000 --- a/tests/GameCult.Caching.Tests/UpdateTests.cs +++ /dev/null @@ -1,515 +0,0 @@ -#nullable enable -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using GameCult.Caching; -using GameCult.Caching.MessagePack; -using MessagePack; -using NUnit.Framework; -using R3; - -namespace GameCult.Caching.Tests -{ - // CultCache.Update: read the content version, apply a pure function to a private copy, commit conditionally, re-run on a lost - // race (variants:ruling:write-set-content-version-declared-ownership; variants:question:update-apply-copy). - public class UpdateTests - { - private static readonly CultRecordKey Key = new("counter"); - private static readonly CultDocumentRegistry Registry = - CultDocumentRegistry.ForTypes(new[] { typeof(UpdateCounter), typeof(UpdateTally), typeof(UpdateGear) }); - - private string _directory = string.Empty; - - [SetUp] - public void CreateDirectory() - { - _directory = Path.Combine(Path.GetTempPath(), $"cultlib-update-{Guid.NewGuid():N}"); - Directory.CreateDirectory(_directory); - } - - [TearDown] - public void DeleteDirectory() - { - if (Directory.Exists(_directory)) - Directory.Delete(_directory, recursive: true); - } - - private string PathOf(string name) => Path.Combine(_directory, name); - - private static CultCache Open(string path, bool directory, bool codec = true) - { - var cache = codec ? new CultCache(Registry, CultCacheMessagePack.CreateCodec(Registry)) : new CultCache(Registry); - cache.AddBackingStore(directory ? new DirectoryMessagePackBackingStore(path) : new SingleFileMessagePackBackingStore(path)); - return cache; - } - - private static void Seed(string path, bool directory, int value) - { - using var cache = Open(path, directory); - Assert.That(cache.Commit(batch => batch.Upsert(new UpdateCounter { Name = "counter", Value = value }, new CultRecordHandle(Key))), Is.True); - } - - private static void Rival(CultCache cache, int value) => - Assert.That(cache.Commit(batch => batch.Upsert(new UpdateCounter { Name = "counter", Value = value }, new CultRecordHandle(Key))), Is.True); - - private static int StoredValue(string path, bool directory) - { - using var reader = Open(path, directory); - return reader.Get(Key)!.Value; - } - - private static UpdateCounter Plus(UpdateCounter? counter, int by) - { - counter ??= new UpdateCounter { Name = "counter" }; - counter.Value += by; - return counter; - } - - [TestCase(false)] - [TestCase(true)] - public void TwoWritersUpdatingLoseNoIncrement(bool directory) - { - const int n = 100; - var path = PathOf("race.cc"); - Seed(path, directory, 0); - using var a = Open(path, directory); - using var b = Open(path, directory); - var applied = 0; - - var racers = new[] { a, b }.Select(cache => Task.Run(() => - { - var results = new List(); - for (var i = 0; i < n; i++) - results.Add(cache.Update(Key, counter => - { - Interlocked.Increment(ref applied); - return Plus(counter, 1); - }, maxAttempts: 200).Outcome); - return results; - })).ToArray(); - Assert.That(Task.WaitAll(racers, TimeSpan.FromMinutes(2)), Is.True, "the racers did not finish"); - - Assert.That(racers.SelectMany(racer => racer.Result), Is.All.EqualTo(CultUpdateOutcome.Updated)); - Assert.That(StoredValue(path, directory), Is.EqualTo(2 * n)); - Assert.That(applied, Is.GreaterThan(2 * n), "the writers never actually raced"); - } - - [Test] - public void LostAttemptRerunsApplyOnTheFreshValue() - { - var path = PathOf("rerun.cc"); - Seed(path, directory: false, 1); - using var cache = Open(path, false); - using var rival = Open(path, false); - var seen = new List(); - - var result = cache.Update(Key, counter => - { - seen.Add(counter!.Value); - if (seen.Count == 1) - Rival(rival, 100); - return Plus(counter, 1); - }); - - Assert.That(seen, Is.EqualTo(new[] { 1, 100 })); - Assert.That(result.Outcome, Is.EqualTo(CultUpdateOutcome.Updated)); - Assert.That(result.Document!.Value, Is.EqualTo(101)); - Assert.That(StoredValue(path, false), Is.EqualTo(101)); - } - - [Test] - public void ExhaustedAttemptsReturnLostRace() - { - var path = PathOf("lost.cc"); - Seed(path, false, 1); - using var cache = Open(path, false); - using var rival = Open(path, false); - var calls = 0; - - var result = cache.Update(Key, counter => - { - Rival(rival, 100 * ++calls); - return Plus(counter, 1); - }, maxAttempts: 3); - - Assert.That(calls, Is.EqualTo(3)); - Assert.That(result.Outcome, Is.EqualTo(CultUpdateOutcome.LostRace)); - Assert.That(result.Document!.Value, Is.EqualTo(300)); - Assert.That(StoredValue(path, false), Is.EqualTo(300), "the store holds the rival's last write"); - } - - [Test] - public void ApplyGetsAPrivateCopy() - { - var path = PathOf("copy.cc"); - Seed(path, false, 1); - using var cache = Open(path, false); - using var rival = Open(path, false); - var published = new List(); - using var subscription = cache.Watch().Subscribe(change => published.Add(change.Document!.Value)); - var arguments = new List(); - var calls = 0; - - var result = cache.Update(Key, counter => - { - calls++; - arguments.Add(counter!.Value); - Assert.That(counter, Is.Not.SameAs(cache.Get(Key)), "apply was handed the held instance"); - counter.Value += 1000; - Assert.That(cache.Get(Key)!.Value, Is.Not.EqualTo(counter.Value), "the change reached the cache before any commit"); - if (calls == 1) - Rival(rival, 5); - return counter; - }); - - Assert.That(arguments, Is.EqualTo(new[] { 1, 5 }), "the second call saw the first call's change"); - Assert.That(result.Outcome, Is.EqualTo(CultUpdateOutcome.Updated)); - Assert.That(published, Does.Not.Contain(1001), "an uncommitted value was published"); - Assert.That(published.Count(value => value == 1005), Is.EqualTo(1)); - Assert.That(StoredValue(path, false), Is.EqualTo(1005)); - } - - [Test] - public void NullFromApplyDeclines() - { - var path = PathOf("decline.cc"); - Seed(path, false, 7); - var before = File.ReadAllBytes(path); - using var cache = Open(path, false); - var changes = 0; - using var subscription = cache.Watch().Subscribe(_ => changes++); - - var result = cache.Update(Key, _ => null); - - Assert.That(result.Outcome, Is.EqualTo(CultUpdateOutcome.Declined)); - Assert.That(result.Document!.Value, Is.EqualTo(7)); - Assert.That(File.ReadAllBytes(path), Is.EqualTo(before)); - Assert.That(changes, Is.EqualTo(0)); - } - - [TestCase(false)] - [TestCase(true)] - public void AbsentKeyCreatesOnceUnderRace(bool directory) - { - var path = PathOf("create.cc"); - using var a = Open(path, directory); - using var b = Open(path, directory); - var started = new Barrier(2); - - var racers = new[] { a, b }.Select(cache => Task.Run(() => - { - started.SignalAndWait(); - return cache.Update(Key, counter => Plus(counter, 1)).Outcome; - })).ToArray(); - Assert.That(Task.WaitAll(racers, TimeSpan.FromMinutes(1)), Is.True); - - Assert.That(racers.Select(racer => racer.Result), Is.All.EqualTo(CultUpdateOutcome.Updated)); - Assert.That(StoredValue(path, directory), Is.EqualTo(2)); - } - - [Test] - public void RefusalsAreNotRetried() - { - var path = PathOf("refuse.cc"); - Seed(path, false, 1); - var before = File.ReadAllBytes(path); - var calls = 0; - Func apply = counter => { calls++; return Plus(counter, 1); }; - - using (var codecLess = Open(path, false, codec: false)) - Assert.That(() => codecLess.Update(Key, apply), Throws.InvalidOperationException); - using (var cache = Open(path, false)) - { - Assert.That(() => cache.Update(Key, apply, maxAttempts: 0), Throws.TypeOf()); - Assert.That(() => cache.Update(Key, null!), Throws.TypeOf()); - Assert.That(() => cache.Update(Key, _ => new UpdateTally { Name = "counter" }), Throws.InvalidOperationException); - } - - using (var cache = Open(path, false)) - { - cache.Commit(batch => - { - batch.Upsert(new UpdateGear { Name = "gear", Power = 1 }, new CultRecordHandle(new CultRecordKey("gear"))); - }); - cache.Commit(batch => batch.UpsertVariant(new CultRecordKey("gear-big"), new CultRecordKey("gear"), - new[] { cache.Override(nameof(UpdateGear.Name), "gear big"), cache.Override(nameof(UpdateGear.Power), 9) })); - } - - before = File.ReadAllBytes(path); - var gearCalls = 0; - using (var cache = Open(path, false)) - Assert.That(() => cache.Update(new CultRecordKey("gear-big"), gear => { gearCalls++; return gear; }), Throws.InvalidOperationException); - Assert.That(gearCalls, Is.EqualTo(0), "apply ran for a variant"); - Assert.That(calls, Is.EqualTo(0), "apply ran for a refused call"); - Assert.That(File.ReadAllBytes(path), Is.EqualTo(before)); - - using (var cache = Open(path, false)) - { - cache.UpsertAsync(new UpdateTally { Name = "staged" }).GetAwaiter().GetResult(); - Assert.That(() => cache.Update(Key, apply), Throws.InvalidOperationException.With.Message.Contains("flush before a conditional commit")); - Assert.That(calls, Is.LessThanOrEqualTo(1)); - } - - Assert.That(File.ReadAllBytes(path), Is.EqualTo(before), "a refusal wrote"); - } - - [Test] - public void VariantStoreConflictIsALostAttempt() - { - var path = PathOf("variant.cc"); - Seed(path, false, 1); - using (var seed = Open(path, false)) - { - seed.Commit(batch => batch.Upsert(new UpdateGear { Name = "gear", Power = 1 }, new CultRecordHandle(new CultRecordKey("gear")))); - seed.Commit(batch => batch.UpsertVariant(new CultRecordKey("gear-big"), new CultRecordKey("gear"), - new[] { seed.Override(nameof(UpdateGear.Name), "gear big"), seed.Override(nameof(UpdateGear.Power), 9) })); - } - - using var cache = Open(path, false); - using (var rival = Open(path, false)) - Assert.That(rival.Commit(batch => batch.Upsert(new UpdateTally { Name = "other" }, new CultRecordHandle(new CultRecordKey("other")))), Is.True); - var calls = 0; - - var result = cache.Update(Key, counter => { calls++; return Plus(counter, 1); }); - - Assert.That(result.Outcome, Is.EqualTo(CultUpdateOutcome.Updated)); - Assert.That(calls, Is.EqualTo(2), "the conflict was not a lost attempt that re-ran apply"); - Assert.That(StoredValue(path, false), Is.EqualTo(2)); - } - - private static CultPersistedStoreSnapshot ReadSnapshot(string path) => - CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(path)); - - // A reload that fails inside a conflict is a read failure, not a lost race: Update lets it propagate and never re-runs apply. - // Here the gear a variant is based on has gone foreign in the file, so the reload the conflict owes cannot drop it. - [Test] - public void ConflictWhoseReloadFailedPropagates() - { - var path = PathOf("reload-failed.cc"); - Seed(path, false, 1); - using var cache = Open(path, false); - cache.Commit(batch => batch.Upsert(new UpdateGear { Name = "gear", Power = 1 }, new CultRecordHandle(new CultRecordKey("gear")))); - cache.Commit(batch => batch.UpsertVariant(new CultRecordKey("gear-big"), new CultRecordKey("gear"), - new[] { cache.Override(nameof(UpdateGear.Name), "gear big"), cache.Override(nameof(UpdateGear.Power), 9) })); - const string foreignId = "sha256:00000000000000000000000000000000000000000000000000000000000050de"; - var snapshot = ReadSnapshot(path); - snapshot.Records.Single(record => record.Key == "gear").SchemaId = foreignId; - snapshot.SchemaCatalog = snapshot.SchemaCatalog.Append(new CultSchemaCatalogEntry - { - SchemaId = foreignId, SchemaName = "tests.update_foreign", SchemaVersion = "tests.update_foreign.v1", - ContentHash = "foreign", CanonicalSchemaJson = "{}" - }).ToArray(); - File.WriteAllBytes(path, CultDocumentMessagePackSerialization.SerializeSnapshot(snapshot)); - var calls = 0; - - var conflict = Assert.Throws(() => cache.Update(Key, counter => { calls++; return Plus(counter, 1); }))!; - - Assert.That(conflict.InnerException, Is.Not.Null, "the reload that failed is the cause"); - Assert.That(calls, Is.EqualTo(1), "a failed reload was retried"); - } - - [TestCase(false)] - [TestCase(true)] - public void RivalWritingAndRestoringTheValueStillRerunsApply(bool directory) - { - var path = PathOf("aba.cc"); - Seed(path, directory, 1); - using var cache = Open(path, directory); - using var rival = Open(path, directory); - var seen = new List(); - - var result = cache.Update(Key, counter => - { - seen.Add(counter!.Value); - if (seen.Count == 1) - { - Rival(rival, 5); - Rival(rival, 1); - } - return Plus(counter, 1); - }); - - Assert.That(seen, Is.EqualTo(new[] { 1, 1 }), "the content version missed A-B-A"); - Assert.That(result.Outcome, Is.EqualTo(CultUpdateOutcome.Updated)); - Assert.That(StoredValue(path, directory), Is.EqualTo(2)); - } - - [TestCase(false)] - [TestCase(true)] - public void RivalRemovalIsSeenAsNull(bool directory) - { - var path = PathOf("removal.cc"); - Seed(path, directory, 1); - using var cache = Open(path, directory); - using var rival = Open(path, directory); - var seen = new List(); - - var result = cache.Update(Key, counter => - { - seen.Add(counter?.Value); - if (seen.Count == 1) - rival.Commit(batch => batch.Remove(Key)); - return Plus(counter, 1); - }); - - Assert.That(seen, Is.EqualTo(new int?[] { 1, null })); - Assert.That(result.Outcome, Is.EqualTo(CultUpdateOutcome.Updated)); - Assert.That(StoredValue(path, directory), Is.EqualTo(1)); - } - - [Test] - public void ExhaustedAttemptsReturnLostRaceInADirectoryStore() - { - var path = PathOf("lost-dir.cc"); - Seed(path, true, 1); - using var cache = Open(path, true); - using var rival = Open(path, true); - var calls = 0; - - var result = cache.Update(Key, counter => - { - Rival(rival, 100 * ++calls); - return Plus(counter, 1); - }, maxAttempts: 2); - - Assert.That(calls, Is.EqualTo(2)); - Assert.That(result.Outcome, Is.EqualTo(CultUpdateOutcome.LostRace)); - Assert.That(result.Document!.Value, Is.EqualTo(200)); - Assert.That(StoredValue(path, true), Is.EqualTo(200)); - } - - [Test] - public void OneAttemptMeansOneApplyAndNonPositiveThrows() - { - var path = PathOf("one.cc"); - Seed(path, false, 1); - using var cache = Open(path, false); - using var rival = Open(path, false); - var calls = 0; - - var result = cache.Update(Key, counter => { calls++; Rival(rival, 50); return Plus(counter, 1); }, maxAttempts: 1); - - Assert.That(calls, Is.EqualTo(1)); - Assert.That(result.Outcome, Is.EqualTo(CultUpdateOutcome.LostRace)); - Assert.That(() => cache.Update(Key, counter => counter, maxAttempts: -1), Throws.TypeOf()); - Assert.That(calls, Is.EqualTo(1)); - } - - [TestCase(false)] - [TestCase(true)] - public void UpdatedDocumentIsTheHeldInstanceAndOwnWritesCauseNoRetry(bool directory) - { - var path = PathOf("own.cc"); - Seed(path, directory, 1); - using var cache = Open(path, directory); - cache.UpsertAsync(new UpdateCounter { Name = "counter", Value = 10 }, new CultRecordHandle(Key)).GetAwaiter().GetResult(); - cache.FlushAsync().GetAwaiter().GetResult(); - var calls = 0; - - var first = cache.Update(Key, counter => { calls++; return Plus(counter, 1); }); - var second = cache.Update(Key, counter => { calls++; return Plus(counter, 1); }); - - Assert.That(calls, Is.EqualTo(2), "the cache's own writes made a later Update retry"); - Assert.That(first.Document, Is.Not.Null); - Assert.That(second.Document, Is.SameAs(cache.Get(Key)), "Updated returned a copy, not the held instance"); - Assert.That(StoredValue(path, directory), Is.EqualTo(12)); - } - - [Test] - public void ApplyRunsOutsideTheCacheGate() - { - var path = PathOf("gate.cc"); - Seed(path, false, 1); - using var cache = Open(path, false); - var reached = false; - - cache.Update(Key, counter => - { - reached = Task.Run(() => cache.Get(Key)).Wait(TimeSpan.FromSeconds(10)); - return Plus(counter, 1); - }); - - Assert.That(reached, Is.True, "another thread could not read the cache while apply ran"); - } - - [TestCase(false)] - [TestCase(true)] - public void ContendedLedgerStoredValueEqualsUpdatedCount(bool directory) - { - var path = PathOf("ledger.cc"); - Seed(path, directory, 0); - var caches = Enumerable.Range(0, 3).Select(_ => Open(path, directory)).ToArray(); - try - { - var tasks = caches.Select(cache => Task.Run(() => - { - var updated = 0; - for (var i = 0; i < 25; i++) - if (cache.Update(Key, counter => Plus(counter, 1)).Outcome == CultUpdateOutcome.Updated) - updated++; - return updated; - })).ToArray(); - Assert.That(Task.WaitAll(tasks, TimeSpan.FromMinutes(2)), Is.True); - - Assert.That(StoredValue(path, directory), Is.EqualTo(tasks.Sum(task => task.Result)), "an Updated outcome was lost or a LostRace wrote"); - } - finally - { - foreach (var cache in caches) - cache.Dispose(); - } - } - - [Test] - public void InMemoryCacheUpdatesUnderThreadRace() - { - using var cache = new CultCache(Registry, CultCacheMessagePack.CreateCodec(Registry)); - cache.Commit(batch => batch.Upsert(new UpdateCounter { Name = "counter" }, new CultRecordHandle(Key))); - - var threads = Enumerable.Range(0, 2).Select(_ => Task.Run(() => - { - for (var i = 0; i < 100; i++) - Assert.That(cache.Update(Key, counter => Plus(counter, 1), maxAttempts: 200).Outcome, Is.EqualTo(CultUpdateOutcome.Updated)); - })).ToArray(); - Assert.That(Task.WaitAll(threads, TimeSpan.FromMinutes(1)), Is.True); - - Assert.That(cache.Get(Key)!.Value, Is.EqualTo(200)); - } - - [CultDocument("tests.update_counter", "tests.update_counter.v1")] - [MessagePackObject(AllowPrivate = true)] - internal sealed class UpdateCounter - { - [Key(0)] - [CultName] - public string Name = string.Empty; - - [Key(1)] - public int Value; - } - - [CultDocument("tests.update_tally", "tests.update_tally.v1")] - [MessagePackObject(AllowPrivate = true)] - internal sealed class UpdateTally - { - [Key(0)] - [CultName] - public string Name = string.Empty; - } - - [CultDocument("tests.update_gear", "tests.update_gear.v1")] - [MessagePackObject(AllowPrivate = true)] - internal sealed class UpdateGear - { - [Key(0)] - [CultName] - public string Name = string.Empty; - - [Key(1)] - public int Power; - } - } -} diff --git a/tests/GameCult.Caching.Tests/stryker-config.json b/tests/GameCult.Caching.Tests/stryker-config.json deleted file mode 100644 index ac366203..00000000 --- a/tests/GameCult.Caching.Tests/stryker-config.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "stryker-config": { - "project": "GameCult.Caching.csproj", - "mutate": [ - "**/src/GameCult.Caching/**/*.cs" - ], - "mutation-level": "Standard", - "reporters": [ "progress", "html", "json" ], - "thresholds": { "high": 80, "low": 60, "break": 0 } - } -} diff --git a/tests/GameCult.Networking.Tests/SelectionParityVectorTests.cs b/tests/GameCult.Networking.Tests/SelectionParityVectorTests.cs index ad7dcaca..3a076d16 100644 --- a/tests/GameCult.Networking.Tests/SelectionParityVectorTests.cs +++ b/tests/GameCult.Networking.Tests/SelectionParityVectorTests.cs @@ -341,7 +341,7 @@ namespace GameCult.Networking.Tests { var path = Path.Combine(RepoRoot(), "contracts", "cultnet", "interop", "selection-vectors.rs-written.json"); if (!File.Exists(path)) - Assert.Inconclusive($"{path} is missing - run cargo test --locked -p cultnet-rs --test selection with CULTNET_WRITE_VECTORS=1 first."); + Assert.Inconclusive($"{path} is missing - run cargo test -p cultnet-rs --test selection with CULTNET_WRITE_VECTORS=1 first."); var (registry, rows, _) = BuildFixture(); using var document = JsonDocument.Parse(File.ReadAllText(path)); diff --git a/tests/vectors/document-variants-c2a/old-reader-refusal.sh b/tests/vectors/document-variants-c2a/old-reader-refusal.sh index 5b9165f0..bfafa01d 100755 --- a/tests/vectors/document-variants-c2a/old-reader-refusal.sh +++ b/tests/vectors/document-variants-c2a/old-reader-refusal.sh @@ -111,7 +111,6 @@ probe() { node ../../node_modules/typescript/bin/tsc -p tsconfig.test.json node --test dist-test/test/old-reader-v3.test.js ;; python) PYTHONPATH=packages/cultcache-py/src python3 packages/cultcache-py/tests/test_old_reader_v3.py -v ;; - # The staged tree is the frozen cultcache-rs-v0.3.1 revision, which has no committed Cargo.lock: --locked cannot work here. rust) cd packages/cultcache-rs && cargo test --test old_reader_v3 -- --nocapture ;; *) usage ;; esac