using AcDream.Content; using AcDream.Content.Pak; namespace AcDream.Bake.Tests; public sealed class BakeOutputTransactionTests : IDisposable { private readonly string _directory = Path.Combine(Path.GetTempPath(), $"acdream-bake-transaction-{Guid.NewGuid():N}"); public BakeOutputTransactionTests() => Directory.CreateDirectory(_directory); public void Dispose() { try { if (Directory.Exists(_directory)) Directory.Delete(_directory, recursive: true); } catch { // Best-effort test cleanup. } } [Fact] public void Publish_ReplacesExistingDestinationOnlyAfterValidation() { string destination = Path.Combine(_directory, "content.pak"); File.WriteAllText(destination, "old"); int result = BakeOutputTransaction.WriteValidateAndPublish( destination, temporary => { File.WriteAllText(temporary, "new"); return 42; }, (temporary, value) => { Assert.Equal(42, value); Assert.Equal("new", File.ReadAllText(temporary)); Assert.Equal("old", File.ReadAllText(destination)); }); Assert.Equal(42, result); Assert.Equal("new", File.ReadAllText(destination)); AssertNoTemporaryFiles(); } [Theory] [InlineData(true)] [InlineData(false)] public void Failure_PreservesExistingDestinationAndCleansTemporary(bool failDuringWrite) { string destination = Path.Combine(_directory, "content.pak"); File.WriteAllText(destination, "old"); Assert.Throws( () => BakeOutputTransaction.WriteValidateAndPublish( destination, temporary => { File.WriteAllText(temporary, "partial"); if (failDuringWrite) throw new InvalidOperationException("write failed"); return 1; }, (_, _) => throw new InvalidOperationException("validation failed"))); Assert.Equal("old", File.ReadAllText(destination)); AssertNoTemporaryFiles(); } [Fact] public void CancellationBeforeWrite_PreservesExistingDestination() { string destination = Path.Combine(_directory, "content.pak"); File.WriteAllText(destination, "old"); using var cancellation = new CancellationTokenSource(); cancellation.Cancel(); Assert.Throws( () => BakeOutputTransaction.WriteValidateAndPublish( destination, temporary => { File.WriteAllText(temporary, "new"); return 1; }, (_, _) => { }, cancellation.Token)); Assert.Equal("old", File.ReadAllText(destination)); AssertNoTemporaryFiles(); } [Fact] public void ArtifactValidator_RejectsStaleBakeToolVersion() { string pak = Path.Combine(_directory, "stale.pak"); var expected = Header(); using (var writer = new PakWriter(pak, expected)) { writer.AddBlob( PakKey.Compose(PakAssetType.GfxObjMesh, 1), new ObjectMeshData { ObjectId = 1 }); writer.Finish(); } using (var stream = new FileStream(pak, FileMode.Open, FileAccess.ReadWrite)) { stream.Position = 36; Span stale = stackalloc byte[4]; System.Buffers.Binary.BinaryPrimitives.WriteUInt32LittleEndian(stale, 1); stream.Write(stale); } var exception = Assert.Throws( () => BakeArtifactValidator.Validate(pak, expected, expectedTocCount: 1)); Assert.Contains("bake tool version", exception.Message); } [Fact] public void ArtifactValidator_RejectsWrongDatIterationAndCorruptTocRange() { string pak = Path.Combine(_directory, "content.pak"); var expected = Header(); using (var writer = new PakWriter(pak, expected)) { writer.AddBlob( PakKey.Compose(PakAssetType.GfxObjMesh, 1), new ObjectMeshData { ObjectId = 1 }); writer.Finish(); } var wrongIteration = expected; wrongIteration.CellIteration++; Assert.Throws( () => BakeArtifactValidator.Validate(pak, wrongIteration, expectedTocCount: 1)); using (var stream = new FileStream(pak, FileMode.Open, FileAccess.ReadWrite)) { var header = PakHeader.ReadFrom(stream); stream.Position = checked((long)header.TocOffset + 16); Span invalidLength = stackalloc byte[4]; System.Buffers.Binary.BinaryPrimitives.WriteUInt32LittleEndian( invalidLength, uint.MaxValue); stream.Write(invalidLength); } Assert.Throws( () => BakeArtifactValidator.Validate(pak, expected, expectedTocCount: 1)); } private PakHeader Header() => new() { PortalIteration = 10, CellIteration = 20, HighResIteration = 30, LanguageIteration = 40, BakeToolVersion = PakFormat.CurrentBakeToolVersion, }; private void AssertNoTemporaryFiles() => Assert.Empty(Directory.EnumerateFiles(_directory, ".*.tmp")); }