Interop: a C# in-place edit is written, and Rust and Python read the new value
The C# peer gains edit-in-place (payload and override through the public snapshot API; collection and nested object through the held document), and the four-peer test has Rust and Python read each edit. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2a3773a463
commit
49f8abd1b6
2 changed files with 115 additions and 0 deletions
|
|
@ -1365,6 +1365,50 @@ test("CultCache element ids cross C#, TypeScript, Python and Rust as ordinary me
|
|||
assert.deepEqual(back.ids, tsIds);
|
||||
});
|
||||
|
||||
// A C# record edited in place is written as edited, however the consumer reached it: a decoded snapshot record's payload bytes, a
|
||||
// variant override's value bytes, a collection element or a nested object of the document the cache holds. Rust and Python read the
|
||||
// edit through their own readers; the variant and the nested object are read by decoding the file with Python's msgpack.
|
||||
test("C# writes an edit made in place, and Rust and Python read the new value", async () => {
|
||||
await buildInteropPeers();
|
||||
const tempDir = await mkdtemp(join(tmpdir(), "cultcache-edit-in-place-"));
|
||||
const csharp = (name: string, args: string[]) => runJsonCommand(name, dotnetCommand, [csharpInteropDll, ...args], cultLibRoot);
|
||||
const edit = (file: string, kind: string) => csharp(`csharp-edit-${kind}`, ["edit-in-place", "--file", file, "--kind", kind]);
|
||||
const readers = (file: string) => [
|
||||
runJsonCommand("rust-read", rustInteropBinary, ["read", "--file", file], cultcacheRsRoot),
|
||||
runJsonCommand("python-read", pythonCommand, ["-m", "cultcache_py.interop", "read", "--file", file], cultcachePyRoot, { PYTHONPATH: cultcachePySrc }),
|
||||
];
|
||||
const dump = (file: string) => runJsonCommand("python-dump", pythonCommand, [
|
||||
"-c",
|
||||
"import msgpack,json,sys\n"
|
||||
+ "s=msgpack.unpackb(open(sys.argv[1],'rb').read(),raw=False)\n"
|
||||
+ "print(json.dumps([{'key':r[0],'overrides':[o[3] for o in r[4][1]]} if len(r)>4 else {'key':r[0],'payload':msgpack.unpackb(r[3],raw=False)} for r in s[2]]))",
|
||||
file,
|
||||
], cultcachePyRoot);
|
||||
|
||||
const payload = join(tempDir, "payload.cc");
|
||||
await csharp("csharp-write", ["write", "--file", payload, "--runtime-id", "csharp-writer"]);
|
||||
await edit(payload, "payload");
|
||||
for (const read of await Promise.all(readers(payload))) assert.equal(read.title, "csharp-writer WROTE a CultCache note");
|
||||
|
||||
const collection = join(tempDir, "collection.cc");
|
||||
await csharp("csharp-write", ["write", "--file", collection, "--runtime-id", "csharp-writer"]);
|
||||
await edit(collection, "collection");
|
||||
for (const read of await Promise.all(readers(collection))) assert.deepEqual(read.tags, ["EDITED", "csharp", "interop"]);
|
||||
|
||||
const override = join(tempDir, "override.cc");
|
||||
await csharp("csharp-write", ["write", "--file", override, "--runtime-id", "csharp-writer"]);
|
||||
await edit(override, "override");
|
||||
const variant = (await dump(override)).find((record: any) => record.key === "note:variant");
|
||||
assert.deepEqual(variant.overrides, ["VARIANT title"]);
|
||||
|
||||
const object = join(tempDir, "object.cc");
|
||||
await csharp("csharp-write-deck", ["write-deck", "--file", object]);
|
||||
await edit(object, "object");
|
||||
const deck = (await dump(object)).find((record: any) => record.key === "deck:csharp");
|
||||
assert.equal(deck.payload[1][0][0], "EDITED");
|
||||
assert.equal(deck.payload[1][1][0], "twin", "the neighbouring mark is untouched");
|
||||
});
|
||||
|
||||
test("CultCache interop reader accepts missing compatible trailing slots and rejects mismatched slots", async () => {
|
||||
const tempDir = await mkdtemp(join(tmpdir(), "cultcache-interop-"));
|
||||
const compatible = join(tempDir, "compatible.msgpack");
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
|
@ -47,6 +48,9 @@ static async Task<int> ProgramMainAsync(string[] args)
|
|||
case "read-deck":
|
||||
await ReadDeckAsync(file);
|
||||
return 0;
|
||||
case "edit-in-place":
|
||||
await EditInPlaceAsync(file, RequireArg(options, "kind"));
|
||||
return 0;
|
||||
default:
|
||||
throw new InvalidOperationException($"Unknown mode {mode}.");
|
||||
}
|
||||
|
|
@ -122,6 +126,73 @@ static async Task ReadDeckAsync(string file)
|
|||
static void WriteDeckJsonLine(CultCacheInteropDeck deck) =>
|
||||
Console.Out.WriteLine(JsonSerializer.Serialize(new { documentId = deck.DocumentId, ids = deck.Marks.Select(mark => mark.Id).ToArray() }));
|
||||
|
||||
// Edits a stored record in place, as a consumer holding the object would, and writes it: the file must hold the edit, in whatever
|
||||
// runtime reads it. payload and override go through the public snapshot API (a decoded record's bytes edited, then
|
||||
// SerializeSnapshot); collection and object edit the document the cache holds and put the same instance back.
|
||||
static async Task EditInPlaceAsync(string file, string kind)
|
||||
{
|
||||
switch (kind)
|
||||
{
|
||||
case "payload":
|
||||
{
|
||||
var snapshot = CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(file));
|
||||
OverwriteInPlace(snapshot.Records.Single().Payload, "wrote", "WROTE");
|
||||
File.WriteAllBytes(file, CultDocumentMessagePackSerialization.SerializeSnapshot(snapshot));
|
||||
break;
|
||||
}
|
||||
case "override":
|
||||
{
|
||||
var cache = BuildCache(file);
|
||||
await cache.PullAllBackingStoresAsync();
|
||||
var note = cache.AllEntries.OfType<CultCacheInteropNote>().Single();
|
||||
await cache.UpsertVariantAsync(
|
||||
new CultRecordKey("note:variant"),
|
||||
new CultRecordKey(note.DocumentId),
|
||||
new[] { cache.Override<CultCacheInteropNote>(nameof(CultCacheInteropNote.Title), "variant title") });
|
||||
cache.FlushAllBackingStores();
|
||||
var snapshot = CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(file));
|
||||
var variant = snapshot.Records.Single(record => record.Variant != null).Variant!;
|
||||
OverwriteInPlace(variant.Overrides.Single().Value, "variant", "VARIANT");
|
||||
File.WriteAllBytes(file, CultDocumentMessagePackSerialization.SerializeSnapshot(snapshot));
|
||||
break;
|
||||
}
|
||||
case "collection":
|
||||
{
|
||||
var cache = BuildCache(file);
|
||||
await cache.PullAllBackingStoresAsync();
|
||||
var note = cache.AllEntries.OfType<CultCacheInteropNote>().Single();
|
||||
note.Tags[0] = "EDITED";
|
||||
await cache.UpsertAsync(note, new CultRecordHandle<CultCacheInteropNote>(new CultRecordKey(note.DocumentId)));
|
||||
cache.FlushAllBackingStores();
|
||||
break;
|
||||
}
|
||||
case "object":
|
||||
{
|
||||
var cache = BuildCache(file);
|
||||
await cache.PullAllBackingStoresAsync();
|
||||
var deck = cache.AllEntries.OfType<CultCacheInteropDeck>().Single();
|
||||
deck.Marks[0].Label = "EDITED";
|
||||
await cache.UpsertAsync(deck, new CultRecordHandle<CultCacheInteropDeck>(new CultRecordKey(deck.DocumentId)));
|
||||
cache.FlushAllBackingStores();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new InvalidOperationException($"Unknown edit kind {kind}.");
|
||||
}
|
||||
|
||||
Console.Out.WriteLine(JsonSerializer.Serialize(new { kind }));
|
||||
}
|
||||
|
||||
static void OverwriteInPlace(byte[] bytes, string from, string to)
|
||||
{
|
||||
var was = System.Text.Encoding.UTF8.GetBytes(from);
|
||||
var now = System.Text.Encoding.UTF8.GetBytes(to);
|
||||
var at = bytes.AsSpan().IndexOf(was);
|
||||
if (at < 0 || now.Length != was.Length)
|
||||
throw new InvalidOperationException($"Cannot overwrite '{from}' with '{to}' in place.");
|
||||
now.CopyTo(bytes, at);
|
||||
}
|
||||
|
||||
// Two routed single-file stores: each file is a complete single-store snapshot holding only its own type.
|
||||
static void WriteRouted(string catalogFile, string runFile)
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue