180 lines
11 KiB
C#
180 lines
11 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using MagnetismHeroes.Core;
|
|
using MagnetismHeroes.UnityRuntime.Persistence;
|
|
using NUnit.Framework;
|
|
|
|
namespace MagnetismHeroes.Tests
|
|
{
|
|
public sealed class LocalProgressStoreTests
|
|
{
|
|
private string _directory, _path;
|
|
private StageDefinition _stage;
|
|
[SetUp] public void SetUp()
|
|
{
|
|
_directory = Path.Combine(Path.GetTempPath(), "MagnetismHeroes-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(_directory);
|
|
_path = Path.Combine(_directory, "progress.json");
|
|
_stage = new StageDefinition(1, StageKind.Counting, new[] { 1 }, new[] { 1 });
|
|
}
|
|
[TearDown] public void TearDown() => Directory.Delete(_directory, true);
|
|
private LocalProgressStore Store() => new LocalProgressStore(_path, new[] { _stage });
|
|
private ProgressSave Puzzle()
|
|
{ var board = new BoardRules(_stage); return new ProgressSave { puzzleStates = new[] { board.ToPuzzleState(GameSession.DefaultCamera()) } }; }
|
|
|
|
[Test] public void CommittedPositionAndCameraRoundTripWithoutPartialLoad()
|
|
{
|
|
var progress = Puzzle(); progress.puzzleStates[0].balls[0].position = new Position3(1, -2, 3);
|
|
progress.puzzleStates[0].camera.distance = 10;
|
|
Assert.That(Store().TrySave(progress, out var error), Is.True, error);
|
|
Assert.That(Store().TryLoad(out var loaded, out error), Is.True, error);
|
|
Assert.That(loaded.puzzleStates[0].balls[0].position, Is.EqualTo(new Position3(1, -2, 3)));
|
|
Assert.That(loaded.puzzleStates[0].camera.distance, Is.EqualTo(10));
|
|
}
|
|
[TestCase("position")]
|
|
[TestCase("x")]
|
|
[TestCase("camera")]
|
|
[TestCase("countedBallIds")]
|
|
[TestCase("latestFreePlay")]
|
|
public void MissingRequiredFieldsAreRejectedInsteadOfDefaulted(string field)
|
|
{
|
|
Assert.That(Store().TrySave(Puzzle(), out _), Is.True);
|
|
var json = File.ReadAllText(_path).Replace("\"" + field + "\":", "\"removed_" + field + "\":");
|
|
File.WriteAllText(_path, json);
|
|
Assert.That(Store().TryLoad(out var loaded, out var error), Is.False);
|
|
Assert.That(loaded, Is.Null); Assert.That(error, Does.Contain(field));
|
|
}
|
|
[Test] public void DuplicateJsonKeysAndTrailingContentAreRejected()
|
|
{
|
|
File.WriteAllText(_path, "{\"schemaVersion\":1,\"schemaVersion\":1}");
|
|
Assert.That(Store().TryLoad(out _, out _), Is.False);
|
|
File.Delete(_path); Assert.That(Store().TrySave(Puzzle(), out _), Is.True);
|
|
File.WriteAllText(_path, File.ReadAllText(_path) + " {}");
|
|
Assert.That(Store().TryLoad(out _, out _), Is.False);
|
|
}
|
|
[Test] public void ValidBackupRecoversWholeBoardAndDamagedOriginalSurvivesNextSave()
|
|
{
|
|
var store = Store(); var progress = Puzzle(); Assert.That(store.TrySave(progress, out _), Is.True);
|
|
progress.puzzleStates[0].camera.distance = 12; Assert.That(store.TrySave(progress, out _), Is.True);
|
|
File.WriteAllText(_path, "broken-primary");
|
|
Assert.That(store.TryLoad(out var loaded, out var error), Is.True, error);
|
|
Assert.That(loaded.puzzleStates[0].camera.distance, Is.EqualTo(8));
|
|
Assert.That(error, Does.Contain("Recovered"));
|
|
Assert.That(store.TrySave(loaded, out error), Is.True, error);
|
|
var damaged = Directory.GetFiles(_directory, "*.damaged-*").Single();
|
|
Assert.That(File.ReadAllText(damaged), Is.EqualTo("broken-primary"));
|
|
Assert.That(File.ReadAllText(_path + ".bak"), Does.Not.Contain("broken-primary"));
|
|
Assert.That(File.Exists(_path + ".tmp"), Is.False);
|
|
}
|
|
[Test] public void WithoutValidBackupLoaderDoesNotOverwriteDamagedEvidence()
|
|
{
|
|
File.WriteAllText(_path, "broken-primary"); File.WriteAllText(_path + ".bak", "broken-backup");
|
|
var store = Store(); Assert.That(store.TryLoad(out _, out _), Is.False);
|
|
Assert.That(File.ReadAllText(_path), Is.EqualTo("broken-primary"));
|
|
Assert.That(store.TrySave(new ProgressSave(), out var error), Is.True, error);
|
|
Assert.That(Directory.GetFiles(_directory, "*.damaged-*").Select(File.ReadAllText), Does.Contain("broken-primary"));
|
|
Assert.That(File.ReadAllText(_path + ".bak"), Is.EqualTo("broken-backup"));
|
|
}
|
|
[Test] public void BothDamagedOriginalsSurviveTwoSubsequentSuccessfulWrites()
|
|
{
|
|
File.WriteAllText(_path, "distinct-broken-primary"); File.WriteAllText(_path + ".bak", "distinct-broken-backup");
|
|
var store = Store(); Assert.That(store.TryLoad(out _, out _), Is.False);
|
|
Assert.That(store.TrySave(new ProgressSave(), out var error), Is.True, error);
|
|
Assert.That(store.TrySave(Puzzle(), out error), Is.True, error);
|
|
var forensic = Directory.GetFiles(_directory, "*.damaged-*").Select(File.ReadAllText).ToArray();
|
|
Assert.That(forensic, Does.Contain("distinct-broken-primary"));
|
|
Assert.That(forensic, Does.Contain("distinct-broken-backup"));
|
|
Assert.That(store.TryLoad(out var restored, out error), Is.True, error);
|
|
Assert.That(restored.puzzleStates.Single().stageId, Is.EqualTo(1));
|
|
}
|
|
[Test] public void MissingPrimaryRestoresEntireValidBackup()
|
|
{
|
|
var definitions = new[] { _stage,
|
|
new StageDefinition(2, StageKind.Counting, new[] { 1, 1 }, new[] { 2 }),
|
|
new StageDefinition(6, StageKind.Composition, new[] { 1, 1 }, new[] { 2 }) };
|
|
var session = new GameSession(definitions, new LocalProgressStore(_path, definitions));
|
|
session.StartStage(1); session.MoveGroup(session.Board.Groups[0].id, new Position3(1, 2, 3));
|
|
session.Count(session.Board.Balls[0].id); session.MarkTutorialSeen("merge");
|
|
session.StartStage(2); session.MoveAndMerge(session.Board.Groups[0].id, new Position3(2, 1, -3), session.Board.Groups[1].id);
|
|
session.Count(session.Board.Balls[0].id);
|
|
session.CommitCamera(new CameraState { target = new Position3(1, 2, 3), rotation = new Rotation4(0, .6f, 0, .8f), distance = 11 });
|
|
var store = new LocalProgressStore(_path, definitions);
|
|
Assert.That(store.TryLoad(out var expected, out var error), Is.True, error);
|
|
var six = new BoardRules(definitions[2]); six.TryMoveGroup(six.Groups[0].id, new Position3(-1, 2, 4));
|
|
expected.puzzleStates = expected.puzzleStates.Concat(new[] { six.ToPuzzleState(GameSession.DefaultCamera()) }).ToArray();
|
|
Assert.That(store.TrySave(expected, out error), Is.True, error);
|
|
var expectedJson = File.ReadAllText(_path);
|
|
Assert.That(store.TrySave(expected, out error), Is.True, error); File.Delete(_path);
|
|
Assert.That(store.TryLoad(out var loaded, out error), Is.True, error);
|
|
Assert.That(error, Does.Contain("Recovered valid backup"));
|
|
Assert.That(loaded.puzzleStates.Length, Is.EqualTo(2));
|
|
Assert.That(loaded.puzzleStates.Single(state => state.stageId == 2).countedBallIds.Length, Is.EqualTo(1));
|
|
Assert.That(store.TrySave(loaded, out error), Is.True, error);
|
|
Assert.That(File.ReadAllText(_path), Is.EqualTo(expectedJson), "Every contract field, including free play and tutorial progress, must survive.");
|
|
}
|
|
[Test] public void MissingPrimaryWithInvalidBackupPreservesEvidenceThroughTwoSaves()
|
|
{
|
|
File.WriteAllText(_path + ".bak", "distinct-backup-only-damage");
|
|
var store = Store(); Assert.That(store.TryLoad(out var loaded, out var error), Is.False);
|
|
Assert.That(loaded, Is.Null); Assert.That(error, Does.Contain("Invalid primary and backup"));
|
|
Assert.That(File.Exists(_path), Is.False);
|
|
Assert.That(File.ReadAllText(_path + ".bak"), Is.EqualTo("distinct-backup-only-damage"));
|
|
Assert.That(store.TrySave(new ProgressSave(), out error), Is.True, error);
|
|
Assert.That(store.TrySave(Puzzle(), out error), Is.True, error);
|
|
Assert.That(Directory.GetFiles(_directory, "*.bak.damaged-*").Select(File.ReadAllText), Does.Contain("distinct-backup-only-damage"));
|
|
Assert.That(store.TryLoad(out loaded, out error), Is.True, error);
|
|
Assert.That(loaded.puzzleStates.Single().stageId, Is.EqualTo(1));
|
|
}
|
|
[Test] public void MissingPrimaryAndBackupStartsFreshWithoutRecoveryError()
|
|
{
|
|
Assert.That(Store().TryLoad(out var loaded, out var error), Is.True, error);
|
|
Assert.That(error, Is.Null); Assert.That(loaded.puzzleStates, Is.Empty);
|
|
Assert.That(loaded.completedStageIds, Is.Empty); Assert.That(loaded.seenTutorialIds, Is.Empty);
|
|
Assert.That(loaded.latestFreePlay, Is.Null); Assert.That(Directory.GetFiles(_directory), Is.Empty);
|
|
}
|
|
[Test] public void InvalidGroupMembershipDoesNotReplaceAValidSave()
|
|
{
|
|
var store = Store(); var progress = Puzzle(); Assert.That(store.TrySave(progress, out _), Is.True);
|
|
var before = File.ReadAllBytes(_path); progress.puzzleStates[0].groups[0].ballIds = new[] { "missing" };
|
|
Assert.That(store.TrySave(progress, out _), Is.False); Assert.That(File.ReadAllBytes(_path), Is.EqualTo(before));
|
|
}
|
|
[TestCase("schemaVersion", "null")]
|
|
[TestCase("schemaVersion", "\"1\"")]
|
|
[TestCase("schemaVersion", "1.5")]
|
|
[TestCase("distance", "null")]
|
|
[TestCase("distance", "\"8\"")]
|
|
[TestCase("x", "null")]
|
|
[TestCase("x", "\"0\"")]
|
|
public void WrongScalarTypesAreRejected(string field, string value)
|
|
{
|
|
Assert.That(Store().TrySave(Puzzle(), out _), Is.True);
|
|
var json = File.ReadAllText(_path);
|
|
json = System.Text.RegularExpressions.Regex.Replace(json, "\"" + field + "\":\\s*-?[0-9]+(?:\\.[0-9]+)?", "\"" + field + "\":" + value);
|
|
File.WriteAllText(_path, json); Assert.That(Store().TryLoad(out var loaded, out _), Is.False); Assert.That(loaded, Is.Null);
|
|
}
|
|
[Test] public void SavedJsonContainsOnlyContractFields()
|
|
{
|
|
Assert.That(Store().TrySave(Puzzle(), out _), Is.True);
|
|
Assert.That(File.ReadAllText(_path), Does.Not.Contain("IsValid"));
|
|
}
|
|
[Test] public void LockedTemporaryFileReportsFailureAndKeepsTheLastValidSave()
|
|
{
|
|
var store = Store(); Assert.That(store.TrySave(Puzzle(), out _), Is.True);
|
|
Assert.That(store.TrySave(Puzzle(), out _), Is.True);
|
|
var before = File.ReadAllBytes(_path);
|
|
var backupBefore = File.ReadAllBytes(_path + ".bak");
|
|
using (var locked = new FileStream(_path + ".tmp", FileMode.Create, FileAccess.ReadWrite, FileShare.None))
|
|
{
|
|
string error = null; bool saved = true;
|
|
Assert.DoesNotThrow(() => saved = store.TrySave(Puzzle(), out error));
|
|
Assert.That(saved, Is.False); Assert.That(error, Is.Not.Empty);
|
|
Assert.That(File.ReadAllBytes(_path), Is.EqualTo(before));
|
|
Assert.That(File.ReadAllBytes(_path + ".bak"), Is.EqualTo(backupBefore));
|
|
}
|
|
Assert.That(store.TryLoad(out var restored, out _), Is.True);
|
|
Assert.That(restored.puzzleStates.Length, Is.EqualTo(1));
|
|
Assert.That(store.TrySave(Puzzle(), out var recoveredError), Is.True, recoveredError);
|
|
}
|
|
}
|
|
}
|