The write replaced the file without reading it, so a 2.0 writer pointed at a store a newer format stamped (or one holding other records) destroyed it. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
84 lines
2.7 KiB
C#
84 lines
2.7 KiB
C#
using System;
|
|
using System.IO;
|
|
using FluentAssertions;
|
|
using GameCult.Caching;
|
|
using GameCult.Caching.MessagePack;
|
|
using NUnit.Framework;
|
|
|
|
#nullable enable
|
|
|
|
namespace GameCult.Mesh.Tests;
|
|
|
|
// A CultMesh single-file write replaces the whole file, so it must read what it replaces first. A store stamped with a
|
|
// newer format version, one it cannot read, or one holding a record the write would drop is refused and left byte-identical.
|
|
public sealed class CultMeshSingleFileWriteGuardTests
|
|
{
|
|
private static readonly CultMeshSingleFileDocumentSchema Schema = new("sha256:guard", "guard.item", "1");
|
|
private string _root = null!;
|
|
private string _path = null!;
|
|
|
|
[SetUp]
|
|
public void SetUp()
|
|
{
|
|
_root = Path.Combine(Path.GetTempPath(), "cultmesh-write-guard", Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(_root);
|
|
_path = Path.Combine(_root, "store.cc");
|
|
}
|
|
|
|
[TearDown]
|
|
public void TearDown()
|
|
{
|
|
if (Directory.Exists(_root)) Directory.Delete(_root, true);
|
|
}
|
|
|
|
private void Write(string key, byte payload) =>
|
|
CultMesh.WriteSingleFileDocumentPayload(_path, new CultRecordKey(key), Schema, null, new[] { payload });
|
|
|
|
[TestCase("cultcache.store.v3")]
|
|
[TestCase("cultcache.store.v9")]
|
|
public void WriteRefusesAStoreStampedWithANewerFormat(string newer)
|
|
{
|
|
Write("item:a", 1);
|
|
var snapshot = CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(_path));
|
|
snapshot.FormatVersion = newer;
|
|
File.WriteAllBytes(_path, CultDocumentMessagePackSerialization.SerializeSnapshot(snapshot));
|
|
var before = File.ReadAllBytes(_path);
|
|
|
|
var error = Assert.Throws<NotSupportedException>(() => Write("item:a", 2))!;
|
|
|
|
error.Message.Should().Contain(newer);
|
|
File.ReadAllBytes(_path).Should().Equal(before);
|
|
}
|
|
|
|
[Test]
|
|
public void WriteRefusesAFileItCannotRead()
|
|
{
|
|
var garbage = new byte[] { 0xc1, 0xc1, 0xc1, 0xc1 };
|
|
File.WriteAllBytes(_path, garbage);
|
|
|
|
Assert.Catch(() => Write("item:a", 1));
|
|
|
|
File.ReadAllBytes(_path).Should().Equal(garbage);
|
|
}
|
|
|
|
[Test]
|
|
public void WriteRefusesToDropAnotherRecord()
|
|
{
|
|
Write("item:a", 1);
|
|
var before = File.ReadAllBytes(_path);
|
|
|
|
var error = Assert.Throws<InvalidOperationException>(() => Write("item:b", 2))!;
|
|
|
|
error.Message.Should().Contain("item:a");
|
|
File.ReadAllBytes(_path).Should().Equal(before);
|
|
}
|
|
|
|
[Test]
|
|
public void WriteReplacesTheSameRecordAndCreatesAMissingFile()
|
|
{
|
|
Write("item:a", 1);
|
|
Write("item:a", 2);
|
|
|
|
CultMesh.ReadSingleFileDocumentPayload(_path, new CultRecordKey("item:a"), Schema.SchemaId).Should().Equal(2);
|
|
}
|
|
}
|