docs/start: verbatim drift check for the C# and TypeScript first-call blocks
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (pull_request) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (pull_request) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Has been cancelled
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (pull_request) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (pull_request) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Has been cancelled
Both blocks now come from a snippet region in their test, and a second test fails when docs/start/README.md stops showing it, as the Rust and Python blocks already do. The TS block differs only in the package import specifier. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
66c9f50993
commit
bb25bf04cb
3 changed files with 129 additions and 75 deletions
|
|
@ -34,6 +34,7 @@ which is what `dotnet test tests/GameCult.Caching.Tests --filter FirstCallTests`
|
||||||
cannot drift without the test failing.
|
cannot drift without the test failing.
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
|
using System.Threading.Tasks;
|
||||||
using GameCult.Caching;
|
using GameCult.Caching;
|
||||||
using GameCult.Caching.MessagePack;
|
using GameCult.Caching.MessagePack;
|
||||||
using MessagePack;
|
using MessagePack;
|
||||||
|
|
@ -47,20 +48,27 @@ public sealed class Item
|
||||||
[Key(2)] public int Value;
|
[Key(2)] public int Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
var store = new SingleFileMessagePackBackingStore("first-call.cc");
|
public static class FirstCall
|
||||||
var cache = new CultCache();
|
{
|
||||||
cache.AddBackingStore(store);
|
public static async Task<Item?> Run(string path)
|
||||||
var handle = await cache.AddAsync(new Item { Name = "Potion", Category = "Consumable", Value = 50 });
|
{
|
||||||
store.PushAll(); // write the .cc file
|
var store = new SingleFileMessagePackBackingStore(path);
|
||||||
|
var cache = new CultCache();
|
||||||
|
cache.AddBackingStore(store);
|
||||||
|
var handle = await cache.AddAsync(new Item { Name = "Potion", Category = "Consumable", Value = 50 });
|
||||||
|
store.PushAll(); // write the .cc file
|
||||||
|
|
||||||
var reopened = new CultCache();
|
var reopened = new CultCache();
|
||||||
reopened.AddBackingStore(new SingleFileMessagePackBackingStore("first-call.cc"));
|
reopened.AddBackingStore(new SingleFileMessagePackBackingStore(path));
|
||||||
await reopened.PullAllBackingStoresAsync(); // read it back
|
await reopened.PullAllBackingStoresAsync(); // read it back
|
||||||
var potion = reopened.Get<Item>(handle.Key); // by key
|
var potion = reopened.Get<Item>(handle.Key); // by key
|
||||||
var byName = reopened.GetByName<Item>("Potion"); // by [CultName]
|
var byName = reopened.GetByName<Item>("Potion"); // by [CultName]
|
||||||
|
return byName ?? potion;
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The test's `Item` is nested and internal, so it carries `[MessagePackObject(AllowPrivate = true)]`; the snippet above is the public-class form and is not itself compiled. More: `src/GameCult.Caching/README.md`.
|
`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`.
|
||||||
|
|
||||||
## TypeScript first call
|
## TypeScript first call
|
||||||
|
|
||||||
|
|
@ -90,23 +98,26 @@ const itemDocument = defineDocumentType({
|
||||||
});
|
});
|
||||||
const registry = defineDocumentRegistry(itemDocument);
|
const registry = defineDocumentRegistry(itemDocument);
|
||||||
|
|
||||||
const cache = CultCache.builder()
|
async function firstCall(path: string) {
|
||||||
.withRegistry(registry)
|
const cache = CultCache.builder()
|
||||||
.withGenericStore(new SingleFileMessagePackBackingStore("first-call.cc"))
|
.withRegistry(registry)
|
||||||
.build();
|
.withGenericStore(new SingleFileMessagePackBackingStore(path))
|
||||||
await cache.pullAllBackingStores();
|
.build();
|
||||||
await cache.put(itemDocument, "item:potion", { name: "Potion", category: "Consumable", value: 50 });
|
await cache.pullAllBackingStores();
|
||||||
|
await cache.put(itemDocument, "item:potion", { name: "Potion", category: "Consumable", value: 50 });
|
||||||
|
|
||||||
const reopened = CultCache.builder()
|
const reopened = CultCache.builder()
|
||||||
.withRegistry(registry)
|
.withRegistry(registry)
|
||||||
.withGenericStore(new SingleFileMessagePackBackingStore("first-call.cc"))
|
.withGenericStore(new SingleFileMessagePackBackingStore(path))
|
||||||
.build();
|
.build();
|
||||||
await reopened.pullAllBackingStores();
|
await reopened.pullAllBackingStores();
|
||||||
const potion = reopened.get(itemDocument, "item:potion");
|
const potion = reopened.get(itemDocument, "item:potion");
|
||||||
const byName = reopened.getByName(itemDocument, "Potion");
|
const byName = reopened.getByName(itemDocument, "Potion");
|
||||||
|
return { potion, byName };
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The test imports from `../src/index`; an application imports `@gamecult/cultcache-ts`. More:
|
The test imports from `../src/index` where the block reads `@gamecult/cultcache-ts`; a second test in that file fails if the block stops matching the rest. More:
|
||||||
`packages/cultcache-ts/README.md`.
|
`packages/cultcache-ts/README.md`.
|
||||||
|
|
||||||
## Rust first call
|
## Rust first call
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { mkdtemp, rm } from "node:fs/promises";
|
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
|
|
||||||
|
// The TypeScript first-call snippet that docs/start/README.md shows: define a typed document, store it
|
||||||
|
// in a .cc file, read it back through a second cache. Keep it small; it is the front door. The docs show
|
||||||
|
// the region between the `snippet` markers verbatim, except that the package import reads
|
||||||
|
// "@gamecult/cultcache-ts" where this file reads "../src/index"; the docs test below fails when they differ.
|
||||||
|
// snippet:begin
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CultCache,
|
CultCache,
|
||||||
SingleFileMessagePackBackingStore,
|
SingleFileMessagePackBackingStore,
|
||||||
|
|
@ -13,8 +17,6 @@ import {
|
||||||
defineDocumentType,
|
defineDocumentType,
|
||||||
} from "../src/index";
|
} from "../src/index";
|
||||||
|
|
||||||
// The TypeScript first-call snippet that docs/start/README.md links: define a typed document,
|
|
||||||
// store it in a .cc file, read it back through a second cache. Keep it small; it is the front door.
|
|
||||||
const itemDocument = defineDocumentType({
|
const itemDocument = defineDocumentType({
|
||||||
type: "item",
|
type: "item",
|
||||||
schemaId: "gamecult.first_call_item.v1",
|
schemaId: "gamecult.first_call_item.v1",
|
||||||
|
|
@ -26,26 +28,43 @@ const itemDocument = defineDocumentType({
|
||||||
});
|
});
|
||||||
const registry = defineDocumentRegistry(itemDocument);
|
const registry = defineDocumentRegistry(itemDocument);
|
||||||
|
|
||||||
|
async function firstCall(path: string) {
|
||||||
|
const cache = CultCache.builder()
|
||||||
|
.withRegistry(registry)
|
||||||
|
.withGenericStore(new SingleFileMessagePackBackingStore(path))
|
||||||
|
.build();
|
||||||
|
await cache.pullAllBackingStores();
|
||||||
|
await cache.put(itemDocument, "item:potion", { name: "Potion", category: "Consumable", value: 50 });
|
||||||
|
|
||||||
|
const reopened = CultCache.builder()
|
||||||
|
.withRegistry(registry)
|
||||||
|
.withGenericStore(new SingleFileMessagePackBackingStore(path))
|
||||||
|
.build();
|
||||||
|
await reopened.pullAllBackingStores();
|
||||||
|
const potion = reopened.get(itemDocument, "item:potion");
|
||||||
|
const byName = reopened.getByName(itemDocument, "Potion");
|
||||||
|
return { potion, byName };
|
||||||
|
}
|
||||||
|
// snippet:end
|
||||||
|
|
||||||
test("define, store and read back a typed document", async () => {
|
test("define, store and read back a typed document", async () => {
|
||||||
const dir = await mkdtemp(join(tmpdir(), "cultcache-first-call-"));
|
const dir = await mkdtemp(join(tmpdir(), "cultcache-first-call-"));
|
||||||
const path = join(dir, "first-call.cc");
|
|
||||||
try {
|
try {
|
||||||
const cache = CultCache.builder()
|
const { potion, byName } = await firstCall(join(dir, "first-call.cc"));
|
||||||
.withRegistry(registry)
|
assert.equal(potion?.value, 50);
|
||||||
.withGenericStore(new SingleFileMessagePackBackingStore(path))
|
assert.equal(byName?.category, "Consumable");
|
||||||
.build();
|
|
||||||
await cache.pullAllBackingStores();
|
|
||||||
await cache.put(itemDocument, "item:potion", { name: "Potion", category: "Consumable", value: 50 });
|
|
||||||
|
|
||||||
const reopened = CultCache.builder()
|
|
||||||
.withRegistry(registry)
|
|
||||||
.withGenericStore(new SingleFileMessagePackBackingStore(path))
|
|
||||||
.build();
|
|
||||||
await reopened.pullAllBackingStores();
|
|
||||||
|
|
||||||
assert.equal(reopened.get(itemDocument, "item:potion")?.value, 50);
|
|
||||||
assert.equal(reopened.getByName(itemDocument, "Potion")?.category, "Consumable");
|
|
||||||
} finally {
|
} finally {
|
||||||
await rm(dir, { recursive: true, force: true });
|
await rm(dir, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("docs show the tested snippet", async () => {
|
||||||
|
// Compiled to dist-test/test/first-call.test.js, so the package root is two levels up.
|
||||||
|
const source = await readFile(join(__dirname, "..", "..", "test", "first-call.test.ts"), "utf8");
|
||||||
|
const snippet = source
|
||||||
|
.split("// snippet:begin\n")[1]
|
||||||
|
.split("// snippet:end\n")[0]
|
||||||
|
.replace('"../src/index"', '"@gamecult/cultcache-ts"');
|
||||||
|
const docs = (await readFile(join(__dirname, "..", "..", "..", "..", "docs", "start", "README.md"), "utf8")).replace(/\r\n/g, "\n");
|
||||||
|
assert.ok(docs.includes("```ts\n" + snippet + "```"), "docs/start/README.md no longer shows test/first-call.test.ts");
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,54 +1,78 @@
|
||||||
#nullable enable
|
#nullable enable
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using NUnit.Framework;
|
||||||
|
|
||||||
|
// snippet:begin
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using GameCult.Caching;
|
using GameCult.Caching;
|
||||||
using GameCult.Caching.MessagePack;
|
using GameCult.Caching.MessagePack;
|
||||||
using MessagePack;
|
using MessagePack;
|
||||||
using NUnit.Framework;
|
|
||||||
|
[CultDocument("gamecult.first_call_item", "gamecult.first_call_item.v1")]
|
||||||
|
[MessagePackObject]
|
||||||
|
public sealed class Item
|
||||||
|
{
|
||||||
|
[Key(0)] [CultName] public string Name = string.Empty;
|
||||||
|
[Key(1)] [CultIndex] public string Category = string.Empty;
|
||||||
|
[Key(2)] public int Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class FirstCall
|
||||||
|
{
|
||||||
|
public static async Task<Item?> Run(string path)
|
||||||
|
{
|
||||||
|
var store = new SingleFileMessagePackBackingStore(path);
|
||||||
|
var cache = new CultCache();
|
||||||
|
cache.AddBackingStore(store);
|
||||||
|
var handle = await cache.AddAsync(new Item { Name = "Potion", Category = "Consumable", Value = 50 });
|
||||||
|
store.PushAll(); // write the .cc file
|
||||||
|
|
||||||
|
var reopened = new CultCache();
|
||||||
|
reopened.AddBackingStore(new SingleFileMessagePackBackingStore(path));
|
||||||
|
await reopened.PullAllBackingStoresAsync(); // read it back
|
||||||
|
var potion = reopened.Get<Item>(handle.Key); // by key
|
||||||
|
var byName = reopened.GetByName<Item>("Potion"); // by [CultName]
|
||||||
|
return byName ?? potion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// snippet:end
|
||||||
|
|
||||||
namespace GameCult.Caching.Tests
|
namespace GameCult.Caching.Tests
|
||||||
{
|
{
|
||||||
// The C# first-call snippet that docs/start/README.md links: define a typed document, store it in a
|
// The C# first-call snippet that docs/start/README.md shows verbatim: define a typed document, store it
|
||||||
// .cc file, read it back through a second cache. Keep it small; it is the front door.
|
// in a .cc file, read it back through a second cache. DocsShowTheTestedSnippet fails when they differ.
|
||||||
public class FirstCallTests
|
public class FirstCallTests
|
||||||
{
|
{
|
||||||
[CultDocument("gamecult.first_call_item", "gamecult.first_call_item.v1")]
|
|
||||||
[MessagePackObject(AllowPrivate = true)]
|
|
||||||
internal sealed class Item
|
|
||||||
{
|
|
||||||
[Key(0)] [CultName] public string Name = string.Empty;
|
|
||||||
[Key(1)] [CultIndex] public string Category = string.Empty;
|
|
||||||
[Key(2)] public int Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task DefineStoreAndReadBackATypedDocument()
|
public async Task DefineStoreAndReadBackATypedDocument()
|
||||||
{
|
{
|
||||||
var path = Path.Combine(Path.GetTempPath(), $"first-call-{Guid.NewGuid():N}.cc");
|
var path = Path.Combine(Path.GetTempPath(), $"first-call-{Guid.NewGuid():N}.cc");
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Write: a cache with one backing store, one typed document.
|
var potion = await FirstCall.Run(path);
|
||||||
var store = new SingleFileMessagePackBackingStore(path);
|
Assert.That(potion, Is.Not.Null);
|
||||||
var cache = new CultCache();
|
Assert.That(potion!.Value, Is.EqualTo(50));
|
||||||
cache.AddBackingStore(store);
|
Assert.That(potion.Category, Is.EqualTo("Consumable"));
|
||||||
var handle = await cache.AddAsync(new Item { Name = "Potion", Category = "Consumable", Value = 50 });
|
|
||||||
store.PushAll();
|
|
||||||
|
|
||||||
// Read: a fresh cache over the same file.
|
|
||||||
var reopened = new CultCache();
|
|
||||||
reopened.AddBackingStore(new SingleFileMessagePackBackingStore(path));
|
|
||||||
await reopened.PullAllBackingStoresAsync();
|
|
||||||
|
|
||||||
var byKey = reopened.Get<Item>(handle.Key);
|
|
||||||
Assert.That(byKey, Is.Not.Null);
|
|
||||||
Assert.That(byKey!.Value, Is.EqualTo(50));
|
|
||||||
Assert.That(reopened.GetByName<Item>("Potion")!.Category, Is.EqualTo("Consumable"));
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
File.Delete(path);
|
File.Delete(path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void DocsShowTheTestedSnippet()
|
||||||
|
{
|
||||||
|
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())!, "..", "..", "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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue