Pin CultLib abd1a92e; [CultRole] audit; ElementId on every list element type #35
11 changed files with 284 additions and 32 deletions
|
|
@ -2,8 +2,8 @@
|
|||
<PropertyGroup>
|
||||
<CultLibRoot Condition="'$(CultLibRoot)' == '' and '$(CULTLIB_ROOT)' != ''">$(CULTLIB_ROOT)</CultLibRoot>
|
||||
<CultLibRoot Condition="'$(CultLibRoot)' == ''">$(MSBuildThisFileDirectory)..\CultLib</CultLibRoot>
|
||||
<!-- CultLib 2.0.0: tag cultlib-unity-v2.0.0. -->
|
||||
<CultLibRevision>42dce3611a81adc5c5fac54cadbaa5637d6022d3</CultLibRevision>
|
||||
<!-- CultLib abd1a92e (#91; floor 7760219b for [CultIndex] uniqueness and [CultElementId]). -->
|
||||
<CultLibRevision>abd1a92eb24e1af3fc7cdd1bf98a88dbdebc2938</CultLibRevision>
|
||||
<GitExecutable Condition="'$(GitExecutable)' == '' and Exists('C:\Program Files\Git\cmd\git.exe')">C:\Program Files\Git\cmd\git.exe</GitExecutable>
|
||||
<GitExecutable Condition="'$(GitExecutable)' == ''">git</GitExecutable>
|
||||
</PropertyGroup>
|
||||
|
|
|
|||
|
|
@ -156,11 +156,11 @@ handoff state lives in [`state/spine.yaml`](state/spine.yaml).
|
|||
|
||||
## Building against CultLib
|
||||
|
||||
Builds are pinned to CultLib 2.0.0 (tag `cultlib-unity-v2.0.0`, revision `42dce3611a81adc5c5fac54cadbaa5637d6022d3`); the build fails if the CultLib checkout differs or is dirty. Create a pinned worktree and point the build at it:
|
||||
Builds are pinned to CultLib revision `abd1a92eb24e1af3fc7cdd1bf98a88dbdebc2938` (#91); the build fails if the CultLib checkout differs or is dirty. Create a pinned worktree and point the build at it:
|
||||
|
||||
```powershell
|
||||
git -C <CultLib> worktree add --detach <path> cultlib-unity-v2.0.0
|
||||
git -C <CultLib> worktree add --detach <path> abd1a92eb24e1af3fc7cdd1bf98a88dbdebc2938
|
||||
dotnet build -p:CultLibRoot=<path> # or set CULTLIB_ROOT=<path>
|
||||
```
|
||||
|
||||
Without an override the sibling `..\CultLib` checkout is used and must itself be at that tag.
|
||||
Without an override the sibling `..\CultLib` checkout is used and must itself be at that revision.
|
||||
|
|
|
|||
|
|
@ -27,7 +27,8 @@ public sealed record CultCachePatchDatabaseEntry(
|
|||
[property: Key(1)] string Family,
|
||||
[property: Key(2)] string Name,
|
||||
[property: Key(3)] string Source,
|
||||
[property: Key(4)] string Notes);
|
||||
[property: Key(4)] string Notes,
|
||||
[property: Key(5), CultElementId("Path")] string ElementId = "");
|
||||
|
||||
[MessagePackObject]
|
||||
public sealed record CultCachePatchClaimCard(
|
||||
|
|
@ -36,7 +37,8 @@ public sealed record CultCachePatchClaimCard(
|
|||
[property: Key(2)] CultCacheReferenceTruth Reference,
|
||||
[property: Key(3)] CultCacheIntentTruth Intent,
|
||||
[property: Key(4)] string Tier,
|
||||
[property: Key(5)] CultCacheProofTruth Proof);
|
||||
[property: Key(5)] CultCacheProofTruth Proof,
|
||||
[property: Key(6), CultElementId("Id")] string ElementId = "");
|
||||
|
||||
[MessagePackObject]
|
||||
public sealed record CultCacheSpeechClaimCard(
|
||||
|
|
@ -45,7 +47,8 @@ public sealed record CultCacheSpeechClaimCard(
|
|||
[property: Key(2)] CultCacheSpeechTargetTruth Target,
|
||||
[property: Key(3)] CultCacheIntentTruth Intent,
|
||||
[property: Key(4)] string Tier,
|
||||
[property: Key(5)] CultCacheProofTruth Proof);
|
||||
[property: Key(5)] CultCacheProofTruth Proof,
|
||||
[property: Key(6), CultElementId("Id")] string ElementId = "");
|
||||
|
||||
[MessagePackObject]
|
||||
public sealed record CultCacheReferenceTruth(
|
||||
|
|
|
|||
|
|
@ -40,7 +40,8 @@ public sealed record PrimitiveTimelineFact(
|
|||
[property: Key(4)] string Unit,
|
||||
[property: Key(5)] int BlockStart,
|
||||
[property: Key(6)] int BlockEnd,
|
||||
[property: Key(7)] string Summary);
|
||||
[property: Key(7)] string Summary,
|
||||
[property: Key(8), CultElementId] string ElementId = "");
|
||||
|
||||
[CultDocument("aquasynth.song_challenge_evidence", "aquasynth.song_challenge_evidence.v1")]
|
||||
[MessagePackObject]
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ public sealed record SpeechRenderResult(
|
|||
[property: CultName]
|
||||
string ResultId,
|
||||
[property: Key(1)]
|
||||
[property: CultIndex("request")]
|
||||
[property: CultRole("request")]
|
||||
string RequestId,
|
||||
[property: Key(2)] string WorkerId,
|
||||
[property: Key(3)] string RendererToolchain,
|
||||
|
|
@ -67,7 +67,8 @@ public sealed record SpeechTimingReceipt(
|
|||
[property: Key(2)] double LatencyMilliseconds,
|
||||
[property: Key(3)] double BudgetMilliseconds,
|
||||
[property: Key(4)] float Confidence,
|
||||
[property: Key(5)] string Notes);
|
||||
[property: Key(5)] string Notes,
|
||||
[property: Key(6), CultElementId] string ElementId = "");
|
||||
|
||||
[MessagePackObject]
|
||||
public sealed record SpeechUtteranceEmbeddingInputSnapshot(
|
||||
|
|
@ -107,13 +108,15 @@ public sealed record SpeechPhoneticEventSnapshot(
|
|||
public sealed record SpeechScoreMetric(
|
||||
[property: Key(0)] string Name,
|
||||
[property: Key(1)] float Value,
|
||||
[property: Key(2)] float Weight);
|
||||
[property: Key(2)] float Weight,
|
||||
[property: Key(3), CultElementId] string ElementId = "");
|
||||
|
||||
[MessagePackObject]
|
||||
public sealed record SpeechRenderArtifact(
|
||||
[property: Key(0)] string Kind,
|
||||
[property: Key(1)] string Uri,
|
||||
[property: Key(2)] string ContentHash);
|
||||
[property: Key(2)] string ContentHash,
|
||||
[property: Key(3), CultElementId] string ElementId = "");
|
||||
|
||||
public enum SpeechRenderStatus
|
||||
{
|
||||
|
|
@ -141,7 +144,8 @@ public sealed record SpeechWorkerPayloadArtifact(
|
|||
[property: Key(0)] string Kind,
|
||||
[property: Key(1)] string Uri,
|
||||
[property: Key(2)] string ContentHash,
|
||||
[property: Key(3)] string MediaType);
|
||||
[property: Key(3)] string MediaType,
|
||||
[property: Key(4), CultElementId] string ElementId = "");
|
||||
|
||||
[CultDocument("aquasynth.speech_worker_admission_receipt", "aquasynth.speech_worker_admission_receipt.v1")]
|
||||
[MessagePackObject]
|
||||
|
|
@ -150,7 +154,7 @@ public sealed record SpeechWorkerAdmissionReceipt(
|
|||
[property: CultName]
|
||||
string ReceiptId,
|
||||
[property: Key(1)]
|
||||
[property: CultIndex("payload")]
|
||||
[property: CultRole("payload")]
|
||||
string PayloadId,
|
||||
[property: Key(2)] string PeerId,
|
||||
[property: Key(3)] string LeaseId,
|
||||
|
|
|
|||
|
|
@ -77,7 +77,8 @@ public sealed record AquaSynthAutomationStreamCommand(
|
|||
[MessagePackObject]
|
||||
public sealed record AquaSynthAutomationControlFrame(
|
||||
[property: Key(0)] int Block,
|
||||
[property: Key(1)] Dictionary<string, float> Controls);
|
||||
[property: Key(1)] Dictionary<string, float> Controls,
|
||||
[property: Key(2), CultElementId] string ElementId = "");
|
||||
|
||||
[CultDocument("aquasynth.command.instrument_open", AquaSynthDaemonSchemas.InstrumentOpenCommand)]
|
||||
[MessagePackObject]
|
||||
|
|
@ -95,14 +96,14 @@ public sealed record AquaSynthInstrumentOpenCommand(
|
|||
[MessagePackObject]
|
||||
public sealed record AquaSynthInstrumentControlCommand(
|
||||
[property: Key(0), CultName] string CommandId,
|
||||
[property: Key(1), CultIndex("session")] string SessionId,
|
||||
[property: Key(1), CultRole("session")] string SessionId,
|
||||
[property: Key(2)] Dictionary<string, float> Controls);
|
||||
|
||||
[CultDocument("aquasynth.command.instrument_block", AquaSynthDaemonSchemas.InstrumentBlockCommand)]
|
||||
[MessagePackObject]
|
||||
public sealed record AquaSynthInstrumentBlockCommand(
|
||||
[property: Key(0), CultName] string CommandId,
|
||||
[property: Key(1), CultIndex("session")] string SessionId,
|
||||
[property: Key(1), CultRole("session")] string SessionId,
|
||||
[property: Key(2)] int FrameCount = 128,
|
||||
[property: Key(3)] float Gain = 1.0f,
|
||||
[property: Key(4)] Dictionary<string, float>? Controls = null);
|
||||
|
|
@ -111,13 +112,13 @@ public sealed record AquaSynthInstrumentBlockCommand(
|
|||
[MessagePackObject]
|
||||
public sealed record AquaSynthInstrumentCloseCommand(
|
||||
[property: Key(0), CultName] string CommandId,
|
||||
[property: Key(1), CultIndex("session")] string SessionId);
|
||||
[property: Key(1), CultRole("session")] string SessionId);
|
||||
|
||||
[CultDocument("aquasynth.compiled_instrument_session", AquaSynthDaemonSchemas.CompiledInstrumentSession)]
|
||||
[MessagePackObject]
|
||||
public sealed record AquaSynthCompiledInstrumentSession(
|
||||
[property: Key(0), CultName] string SessionId,
|
||||
[property: Key(1), CultIndex("patch")] string PatchId,
|
||||
[property: Key(1), CultRole("patch")] string PatchId,
|
||||
[property: Key(2)] string CompileKey,
|
||||
[property: Key(3)] string FaustName,
|
||||
[property: Key(4)] int Revision,
|
||||
|
|
@ -134,7 +135,7 @@ public sealed record AquaSynthCompiledInstrumentSession(
|
|||
[MessagePackObject]
|
||||
public sealed record AquaSynthLiveInstrumentSessionState(
|
||||
[property: Key(0), CultName] string SessionId,
|
||||
[property: Key(1), CultIndex("patch")] string PatchId,
|
||||
[property: Key(1), CultRole("patch")] string PatchId,
|
||||
[property: Key(2)] string Status,
|
||||
[property: Key(3)] string OpenedAtUtc,
|
||||
[property: Key(4)] string UpdatedAtUtc,
|
||||
|
|
@ -154,7 +155,7 @@ public sealed record AquaSynthLiveInstrumentSessionState(
|
|||
public sealed record AquaSynthPatchCompileReceipt(
|
||||
[property: Key(0), CultName] string ReceiptId,
|
||||
[property: Key(1), CultIndex("command")] string CommandId,
|
||||
[property: Key(2), CultIndex("patch")] string PatchId,
|
||||
[property: Key(2), CultRole("patch")] string PatchId,
|
||||
[property: Key(3)] string SessionId,
|
||||
[property: Key(4)] string Status,
|
||||
[property: Key(5)] string DecidedAtUtc,
|
||||
|
|
@ -170,7 +171,7 @@ public sealed record AquaSynthPatchCompileReceipt(
|
|||
public sealed record AquaSynthRenderSampleReceipt(
|
||||
[property: Key(0), CultName] string RenderId,
|
||||
[property: Key(1), CultIndex("command")] string CommandId,
|
||||
[property: Key(2), CultIndex("patch")] string PatchId,
|
||||
[property: Key(2), CultRole("patch")] string PatchId,
|
||||
[property: Key(3)] string SessionId,
|
||||
[property: Key(4)] string Status,
|
||||
[property: Key(5)] string CompletedAtUtc,
|
||||
|
|
@ -190,7 +191,7 @@ public sealed record AquaSynthRenderSampleReceipt(
|
|||
public sealed record AquaSynthAutomationStreamReceipt(
|
||||
[property: Key(0), CultName] string StreamRunId,
|
||||
[property: Key(1), CultIndex("command")] string CommandId,
|
||||
[property: Key(2), CultIndex("patch")] string PatchId,
|
||||
[property: Key(2), CultRole("patch")] string PatchId,
|
||||
[property: Key(3)] string SessionId,
|
||||
[property: Key(4)] string Status,
|
||||
[property: Key(5)] string CompletedAtUtc,
|
||||
|
|
@ -210,8 +211,8 @@ public sealed record AquaSynthAutomationStreamReceipt(
|
|||
public sealed record AquaSynthInstrumentOpenReceipt(
|
||||
[property: Key(0), CultName] string ReceiptId,
|
||||
[property: Key(1), CultIndex("command")] string CommandId,
|
||||
[property: Key(2), CultIndex("patch")] string PatchId,
|
||||
[property: Key(3), CultIndex("session")] string SessionId,
|
||||
[property: Key(2), CultRole("patch")] string PatchId,
|
||||
[property: Key(3), CultRole("session")] string SessionId,
|
||||
[property: Key(4)] string Status,
|
||||
[property: Key(5)] string OpenedAtUtc,
|
||||
[property: Key(6)] int SampleRate,
|
||||
|
|
@ -228,7 +229,7 @@ public sealed record AquaSynthInstrumentOpenReceipt(
|
|||
public sealed record AquaSynthInstrumentControlReceipt(
|
||||
[property: Key(0), CultName] string ReceiptId,
|
||||
[property: Key(1), CultIndex("command")] string CommandId,
|
||||
[property: Key(2), CultIndex("session")] string SessionId,
|
||||
[property: Key(2), CultRole("session")] string SessionId,
|
||||
[property: Key(3)] string Status,
|
||||
[property: Key(4)] string AppliedAtUtc,
|
||||
[property: Key(5)] int ControlCount,
|
||||
|
|
@ -240,7 +241,7 @@ public sealed record AquaSynthInstrumentControlReceipt(
|
|||
public sealed record AquaSynthInstrumentBlockReceipt(
|
||||
[property: Key(0), CultName] string BlockId,
|
||||
[property: Key(1), CultIndex("command")] string CommandId,
|
||||
[property: Key(2), CultIndex("session")] string SessionId,
|
||||
[property: Key(2), CultRole("session")] string SessionId,
|
||||
[property: Key(3)] string Status,
|
||||
[property: Key(4)] string CompletedAtUtc,
|
||||
[property: Key(5)] ulong Sequence,
|
||||
|
|
@ -258,7 +259,7 @@ public sealed record AquaSynthInstrumentBlockReceipt(
|
|||
public sealed record AquaSynthInstrumentCloseReceipt(
|
||||
[property: Key(0), CultName] string ReceiptId,
|
||||
[property: Key(1), CultIndex("command")] string CommandId,
|
||||
[property: Key(2), CultIndex("session")] string SessionId,
|
||||
[property: Key(2), CultRole("session")] string SessionId,
|
||||
[property: Key(3)] string Status,
|
||||
[property: Key(4)] string ClosedAtUtc,
|
||||
[property: Key(5)] bool Released,
|
||||
|
|
@ -276,7 +277,8 @@ public sealed record AquaSynthCultMeshStreamDescriptor(
|
|||
[property: Key(6)] int Channels,
|
||||
[property: Key(7)] int FramesPerPacket,
|
||||
[property: Key(8)] string[] PreferredTransports,
|
||||
[property: Key(9)] string? MetadataSchemaId);
|
||||
[property: Key(9)] string? MetadataSchemaId,
|
||||
[property: Key(10), CultElementId("StreamId")] string ElementId = "");
|
||||
|
||||
[MessagePackObject]
|
||||
public sealed record AquaSynthAutomationPacketReceipt(
|
||||
|
|
@ -289,7 +291,8 @@ public sealed record AquaSynthAutomationPacketReceipt(
|
|||
[property: Key(6)] string Float32Uri,
|
||||
[property: Key(7)] string ContentHash,
|
||||
[property: Key(8)] string PageRef,
|
||||
[property: Key(9)] int ControlCount);
|
||||
[property: Key(9)] int ControlCount,
|
||||
[property: Key(10), CultElementId("Sequence")] string ElementId = "");
|
||||
|
||||
[CultDocument("aquasynth.operator_state", AquaSynthDaemonSchemas.OperatorState)]
|
||||
[CultGlobal]
|
||||
|
|
|
|||
|
|
@ -83,7 +83,8 @@ public sealed class CultCachePatchDocumentTests
|
|||
Path.Combine(RepositoryRoot(), "patches", "aquasynth-patch-cultcache.cc"));
|
||||
var fromCatalog = CultCachePatchDocumentCatalog.CreateDefault();
|
||||
|
||||
Assert.Equal(fromCatalog.PatchDatabase.Entries, fromFile.PatchDatabase.Entries);
|
||||
// The checked-in file predates element ids; a load mints them (derived from Path), so they are not part of the catalog's content.
|
||||
Assert.Equal(fromCatalog.PatchDatabase.Entries, fromFile.PatchDatabase.Entries.Select(entry => entry with { ElementId = "" }));
|
||||
Assert.Equal(fromCatalog.PatchClaims.Select(claim => claim.Id), fromFile.PatchClaims.Select(claim => claim.Id));
|
||||
Assert.Equal(fromCatalog.SpeechClaims.Select(claim => claim.Id), fromFile.SpeechClaims.Select(claim => claim.Id));
|
||||
}
|
||||
|
|
|
|||
110
tests/AquaSynth.Dsl.Tests/CultIndexUniquenessTests.cs
Normal file
110
tests/AquaSynth.Dsl.Tests/CultIndexUniquenessTests.cs
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
using System.Reflection;
|
||||
using AquaSynth.Dsl;
|
||||
using GameCult.Caching;
|
||||
using GameCult.Caching.MessagePack;
|
||||
|
||||
namespace AquaSynth.Dsl.Tests;
|
||||
|
||||
// CultLib #90 (7760219b): [CultIndex] means unique, enforced at commit and load; [CultRole] names a many-row selection role.
|
||||
// A member that two rows can share must be a role. The only [CultIndex] left is the CommandId of a receipt: every receipt is stored
|
||||
// under a key derived from its CommandId alone (AquaSynthCultNetDaemon.*ReceiptKey), so a second row with that CommandId replaces the first.
|
||||
public sealed class CultIndexUniquenessTests
|
||||
{
|
||||
private static readonly string[] ReceiptTypes =
|
||||
[
|
||||
nameof(AquaSynthPatchCompileReceipt), nameof(AquaSynthRenderSampleReceipt), nameof(AquaSynthAutomationStreamReceipt), nameof(AquaSynthInstrumentOpenReceipt),
|
||||
nameof(AquaSynthInstrumentControlReceipt), nameof(AquaSynthInstrumentBlockReceipt), nameof(AquaSynthInstrumentCloseReceipt),
|
||||
];
|
||||
|
||||
[Fact]
|
||||
public void TheOnlyCultIndexMembersAreReceiptCommandIds()
|
||||
{
|
||||
var indexed = new[] { typeof(AquaSynthDaemonService).Assembly, typeof(SpeechRenderResult).Assembly }.Distinct()
|
||||
.SelectMany(assembly => assembly.GetTypes())
|
||||
.SelectMany(type => type.GetMembers(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(member => member is PropertyInfo or FieldInfo && member.GetCustomAttribute<CultIndexAttribute>() is not null)
|
||||
.Select(member => $"{type.Name}.{member.Name}"))
|
||||
.OrderBy(name => name, StringComparer.Ordinal).ToArray();
|
||||
Assert.Equal(ReceiptTypes.Select(name => $"{name}.CommandId").OrderBy(name => name, StringComparer.Ordinal), indexed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ManyRowsMaySharePatchAndSessionAcrossDaemonDocuments()
|
||||
{
|
||||
var path = TempFile();
|
||||
try
|
||||
{
|
||||
using (var cache = await CultCacheMessagePack.OpenAsync(path))
|
||||
{
|
||||
string[] paths = [];
|
||||
for (var i = 0; i < 3; i++)
|
||||
{
|
||||
// Three commands, one patch, one session: the shape a client that opens, drives and closes one instrument produces.
|
||||
await Put(cache, $"cmd-{i}-control", new AquaSynthInstrumentControlCommand($"cmd-{i}-control", "session-1", new Dictionary<string, float>()));
|
||||
await Put(cache, $"cmd-{i}-block", new AquaSynthInstrumentBlockCommand($"cmd-{i}-block", "session-1"));
|
||||
await Put(cache, $"cmd-{i}-close", new AquaSynthInstrumentCloseCommand($"cmd-{i}-close", "session-1"));
|
||||
await Put(cache, $"sess-{i}", new AquaSynthCompiledInstrumentSession($"sess-{i}", "patch-1", $"key-{i}", "f", 1, 48000, 0, 1, 128, 0.1f, "t", paths, paths));
|
||||
await Put(cache, $"live-{i}", new AquaSynthLiveInstrumentSessionState($"live-{i}", "patch-1", "open", "t", "t", "", 48000, 0, 1, 128, 1f, 0, new Dictionary<string, float>(), "", ""));
|
||||
await Put(cache, $"cmd-{i}-compile", new AquaSynthPatchCompileReceipt($"cmd-{i}-compile", $"cmd-{i}", "patch-1", "session-1", "succeeded", "t", "k", 1, paths, paths));
|
||||
await Put(cache, $"cmd-{i}-render", new AquaSynthRenderSampleReceipt($"cmd-{i}-render", $"cmd-{i}", "patch-1", "session-1", "succeeded", "t", 48000, 1, 0.1f, 0, 0, "", "", ""));
|
||||
await Put(cache, $"cmd-{i}-stream", new AquaSynthAutomationStreamReceipt($"cmd-{i}-stream", $"cmd-{i}", "patch-1", "session-1", "succeeded", "t", "v", "a", "c", 48000, 128, 1, [], []));
|
||||
await Put(cache, $"cmd-{i}-open", new AquaSynthInstrumentOpenReceipt($"cmd-{i}-open", $"cmd-{i}", "patch-1", "session-1", "succeeded", "t", 48000, 0, 1, 128, paths, paths));
|
||||
await Put(cache, $"cmd-{i}-ctl", new AquaSynthInstrumentControlReceipt($"cmd-{i}-ctl", $"cmd-{i}", "session-1", "applied", "t", 0));
|
||||
await Put(cache, $"cmd-{i}-blk", new AquaSynthInstrumentBlockReceipt($"cmd-{i}-blk", $"cmd-{i}", "session-1", "succeeded", "t", 0, 48000, 128, 0, 0, "", ""));
|
||||
await Put(cache, $"cmd-{i}-cls", new AquaSynthInstrumentCloseReceipt($"cmd-{i}-cls", $"cmd-{i}", "session-1", "closed", "t", true));
|
||||
}
|
||||
|
||||
await cache.FlushAsync();
|
||||
}
|
||||
|
||||
// Reopening hydrates and revalidates: a unique index would refuse the file here.
|
||||
using var reloaded = await CultCacheMessagePack.OpenAsync(path);
|
||||
Assert.Equal(3, reloaded.GetAll<AquaSynthInstrumentBlockCommand>().Count());
|
||||
Assert.Equal(3, reloaded.GetAll<AquaSynthCompiledInstrumentSession>().Count(session => session.PatchId == "patch-1"));
|
||||
Assert.Equal(3, reloaded.GetAll<AquaSynthInstrumentOpenReceipt>().Count(receipt => receipt.SessionId == "session-1" && receipt.PatchId == "patch-1"));
|
||||
Assert.Equal(3, reloaded.GetAll<AquaSynthInstrumentBlockReceipt>().Count(receipt => receipt.SessionId == "session-1"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OneRowPerCommandIdPerReceiptType()
|
||||
{
|
||||
var path = TempFile();
|
||||
try
|
||||
{
|
||||
using var cache = await CultCacheMessagePack.OpenAsync(path);
|
||||
string[] paths = [];
|
||||
AquaSynthPatchCompileReceipt Receipt(string command, string status) =>
|
||||
new(AquaSynthCultNetDaemon.CompileReceiptKey(command).Value, command, "patch-1", "session-1", status, "t", "k", 1, paths, paths);
|
||||
foreach (var (command, status) in new[] { ("a", "failed"), ("a", "succeeded"), ("b", "succeeded") })
|
||||
{
|
||||
var receipt = Receipt(command, status);
|
||||
await cache.UpsertAsync(receipt, new CultRecordHandle<AquaSynthPatchCompileReceipt>(AquaSynthCultNetDaemon.CompileReceiptKey(command)));
|
||||
}
|
||||
|
||||
// A retried command replaces its own receipt; it cannot add a second row that carries the same CommandId.
|
||||
var all = cache.GetAll<AquaSynthPatchCompileReceipt>().OrderBy(receipt => receipt.CommandId, StringComparer.Ordinal).ToArray();
|
||||
Assert.Equal(["a", "b"], all.Select(receipt => receipt.CommandId));
|
||||
Assert.Equal("succeeded", all[0].Status);
|
||||
foreach (var kind in new Func<string, CultRecordKey>[] { AquaSynthCultNetDaemon.CompileReceiptKey, AquaSynthCultNetDaemon.RenderReceiptKey, AquaSynthCultNetDaemon.StreamReceiptKey,
|
||||
AquaSynthCultNetDaemon.OpenReceiptKey, AquaSynthCultNetDaemon.ControlReceiptKey, AquaSynthCultNetDaemon.BlockReceiptKey, AquaSynthCultNetDaemon.CloseReceiptKey })
|
||||
{
|
||||
Assert.NotEqual(kind("a"), kind("b"));
|
||||
Assert.Equal(kind("a"), kind("a"));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
private static Task Put<T>(CultCache cache, string key, T document) where T : class =>
|
||||
cache.UpsertAsync(document, new CultRecordHandle<T>(new CultRecordKey(key)));
|
||||
|
||||
private static string TempFile() => Path.Combine(Path.GetTempPath(), $"aquasynth-cultindex-{Guid.NewGuid():N}.cc");
|
||||
}
|
||||
104
tests/AquaSynth.Dsl.Tests/ElementIdTests.cs
Normal file
104
tests/AquaSynth.Dsl.Tests/ElementIdTests.cs
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
using AquaSynth.Dsl;
|
||||
using GameCult.Caching;
|
||||
using GameCult.Caching.MessagePack;
|
||||
|
||||
namespace AquaSynth.Dsl.Tests;
|
||||
|
||||
// CultLib abd1a92e refuses to register a document whose list element type has no [CultElementId] member (CultCache.cs:1008-1010);
|
||||
// opening any cache here registers every AquaSynth document type, so these tests also fail if one element type loses its id.
|
||||
public sealed class ElementIdTests
|
||||
{
|
||||
// Fixtures/CultCache/pre-element-ids.cc was written by the previous pin (CultLib 42dce361), before list elements had ids:
|
||||
// an automation-stream command with 2 control frames and a stream receipt with 2 stream descriptors and 2 packets.
|
||||
[Fact]
|
||||
public async Task AStoreWrittenBeforeElementIdsReopensWithStableUniqueIds()
|
||||
{
|
||||
var path = CopyFixture();
|
||||
try
|
||||
{
|
||||
string[] first;
|
||||
using (var cache = await CultCacheMessagePack.OpenAsync(path))
|
||||
{
|
||||
var command = cache.GetAll<AquaSynthAutomationStreamCommand>().Single();
|
||||
var receipt = cache.GetAll<AquaSynthAutomationStreamReceipt>().Single();
|
||||
Assert.Equal([0, 4], command.ControlFrames!.Select(frame => frame.Block));
|
||||
Assert.Equal(220f, command.ControlFrames![0].Controls["/freq"]);
|
||||
Assert.Equal(["audio-1", "control-1"], receipt.Streams.Select(stream => stream.StreamId));
|
||||
Assert.Equal([0UL, 1UL], receipt.Packets.Select(packet => packet.Sequence));
|
||||
first = Ids(command, receipt);
|
||||
Assert.All(first, id => Assert.False(string.IsNullOrEmpty(id)));
|
||||
Assert.Equal(2, command.ControlFrames!.Select(frame => frame.ElementId).Distinct().Count());
|
||||
Assert.Equal(2, receipt.Packets.Select(packet => packet.ElementId).Distinct().Count());
|
||||
}
|
||||
|
||||
// A second reader of the same bytes mints the same ids (load mints from the record key and element path).
|
||||
using var again = await CultCacheMessagePack.OpenAsync(path);
|
||||
Assert.Equal(first, Ids(again.GetAll<AquaSynthAutomationStreamCommand>().Single(), again.GetAll<AquaSynthAutomationStreamReceipt>().Single()));
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IdsSurviveAWriteAndReload()
|
||||
{
|
||||
var path = CopyFixture();
|
||||
try
|
||||
{
|
||||
string[] first;
|
||||
using (var cache = await CultCacheMessagePack.OpenAsync(path))
|
||||
{
|
||||
first = Ids(cache.GetAll<AquaSynthAutomationStreamCommand>().Single(), cache.GetAll<AquaSynthAutomationStreamReceipt>().Single());
|
||||
// An unrelated write rewrites the store in the current shape.
|
||||
await cache.UpsertAsync(new AquaSynthInstrumentCloseCommand("c-1", "s-1"), new CultRecordHandle<AquaSynthInstrumentCloseCommand>(new CultRecordKey("c-1")));
|
||||
await cache.FlushAsync();
|
||||
}
|
||||
|
||||
using var reloaded = await CultCacheMessagePack.OpenAsync(path);
|
||||
Assert.Equal(first, Ids(reloaded.GetAll<AquaSynthAutomationStreamCommand>().Single(), reloaded.GetAll<AquaSynthAutomationStreamReceipt>().Single()));
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NewWritesKeepNaturalIdsAndDeriveThePacketIdFromItsSequence()
|
||||
{
|
||||
var path = Path.Combine(Path.GetTempPath(), $"aquasynth-elementid-{Guid.NewGuid():N}.cc");
|
||||
try
|
||||
{
|
||||
using (var cache = await CultCacheMessagePack.OpenAsync(path))
|
||||
{
|
||||
var packets = new[] { new AquaSynthAutomationPacketReceipt(7, 0, 1, 128, 0.5f, 0.1f, "u", "h", "p", 0), new AquaSynthAutomationPacketReceipt(9, 1, 1, 128, 0.5f, 0.1f, "u", "h", "p", 0) };
|
||||
var streams = new[] { new AquaSynthCultMeshStreamDescriptor("audio-9", "v", "p", "audio", "f32", 48000, 1, 128, [], null) };
|
||||
await cache.UpsertAsync(new AquaSynthAutomationStreamReceipt("run", "cmd", "patch", "session", "succeeded", "t", "v", "a", "c", 48000, 128, 1, streams, packets),
|
||||
new CultRecordHandle<AquaSynthAutomationStreamReceipt>(new CultRecordKey("run")));
|
||||
await cache.FlushAsync();
|
||||
}
|
||||
|
||||
using var reloaded = await CultCacheMessagePack.OpenAsync(path);
|
||||
var receipt = reloaded.GetAll<AquaSynthAutomationStreamReceipt>().Single();
|
||||
Assert.Equal(["7", "9"], receipt.Packets.Select(packet => packet.ElementId));
|
||||
Assert.Equal("audio-9", receipt.Streams.Single().StreamId);
|
||||
Assert.Equal("audio-9", receipt.Streams.Single().ElementId);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
private static string[] Ids(AquaSynthAutomationStreamCommand command, AquaSynthAutomationStreamReceipt receipt) =>
|
||||
[.. command.ControlFrames!.Select(frame => frame.ElementId), .. receipt.Packets.Select(packet => packet.ElementId)];
|
||||
|
||||
private static string CopyFixture()
|
||||
{
|
||||
var path = Path.Combine(Path.GetTempPath(), $"aquasynth-preids-{Guid.NewGuid():N}.cc");
|
||||
File.Copy(Path.Combine(AppContext.BaseDirectory, "Fixtures", "CultCache", "pre-element-ids.cc"), path);
|
||||
return path;
|
||||
}
|
||||
}
|
||||
BIN
tests/AquaSynth.Dsl.Tests/Fixtures/CultCache/pre-element-ids.cc
Normal file
BIN
tests/AquaSynth.Dsl.Tests/Fixtures/CultCache/pre-element-ids.cc
Normal file
Binary file not shown.
|
|
@ -180,6 +180,8 @@ public sealed class SpeechDistributedTrainingTests
|
|||
Assert.True(after < before, $"expected CultMesh worker gradients to reduce loss; before={before}, after={after}");
|
||||
Assert.Single(payloads);
|
||||
Assert.Equal(SpeechCultMeshRoles.RenderWorker, payloads[0].WorkerRole);
|
||||
// One payload, four receipts: PayloadId is a role (many rows share it), not a unique index (CultLib #90).
|
||||
Assert.Equal(4, admissions.Count(receipt => receipt.PayloadId == "payload-cultmesh-0001"));
|
||||
Assert.Equal(2, admissions.Count(receipt => receipt.Status == SpeechWorkerAdmissionStatus.Admitted));
|
||||
Assert.Equal(2, admissions.Count(receipt => receipt.Status == SpeechWorkerAdmissionStatus.Rejected));
|
||||
Assert.All(admissions.Where(receipt => receipt.Status == SpeechWorkerAdmissionStatus.Admitted), receipt => Assert.NotEmpty(receipt.AssignedRequestIds));
|
||||
|
|
@ -284,6 +286,30 @@ public sealed class SpeechDistributedTrainingTests
|
|||
return loss / actual.Count;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OneRequestMayHaveAFailedAndAScoredResultInTheSameStore()
|
||||
{
|
||||
var directory = TempDirectory();
|
||||
try
|
||||
{
|
||||
var resultStore = Path.Combine(directory, "speech-results.cc");
|
||||
var request = SpeechDistributedTrainingCoordinator.CreateRenderRequests("batch-roles", Pipeline(), TrainingSet())[0];
|
||||
var scored = SpeechDistributedTrainingCoordinator.ScoreControlVectorRequests([request], "worker-a");
|
||||
var failed = SpeechDistributedTrainingCoordinator.ScoreControlVectorRequests([request with { CandidateControlVector = [] }], "worker-b");
|
||||
await SpeechDistributedTrainingCultCacheStore.UpsertResultsAsync(resultStore, [.. scored, .. failed]);
|
||||
|
||||
// RequestId is a role: a retried request keeps its failed result beside the later scored one (CultLib #90 makes CultIndex unique).
|
||||
var loaded = await SpeechDistributedTrainingCultCacheStore.ReadResultsAsync(resultStore);
|
||||
Assert.Equal(2, loaded.Count(result => result.RequestId == request.RequestId));
|
||||
Assert.Contains(loaded, result => result.Status == SpeechRenderStatus.Failed);
|
||||
Assert.Contains(loaded, result => result.Status == SpeechRenderStatus.Succeeded);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(directory, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
private static string TempDirectory()
|
||||
{
|
||||
var path = Path.Combine(Path.GetTempPath(), $"aquasynth-speech-distributed-{Guid.NewGuid():N}");
|
||||
|
|
|
|||
Loading…
Reference in a new issue