XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Reflection;
using Microsoft.Data.Sqlite;
using XFEExtension.NetCore.XUnit.Attributes;
using XFEEDToolBox.Core.Data;
using XFEEDToolBox.Core.Models;

namespace XFEEDToolBox.Test;

[TestFixture]
public class MarketCacheCleanupTests
{
    private static MarketRepository Cheap(TemporaryDirectory directory)
    {var repository=new MarketRepository(directory.File("markets.db"));repository.InitializeCheap();return repository;}
    private static SqliteConnection Open(string path)
    {var connection=new SqliteConnection(new SqliteConnectionStringBuilder{DataSource=path,Pooling=false}.ToString());connection.Open();return connection;}

    [Test]public async Task CheapStartupAndFirstObservationDoNotValidateBackupOrCreateReceipts()
    {
        using var directory=new TemporaryDirectory();var repository=Cheap(directory);
        await repository.SaveSnapshotAsync(new(Check.Station(1),[Check.Quote(1,"water",10,9)],DateTimeOffset.UtcNow,DataSource.Journal));
        Check.That(!File.Exists(repository.VerifiedBackupPath));Check.That(!File.Exists(repository.DatabasePath+".verified-generation.json"));
        Check.That(!await repository.FlushVerifiedGenerationAsync());Check.Equal(1L,repository.Statistics().Quotes);
        SqliteConnection.ClearAllPools();File.WriteAllBytes(repository.VerifiedBackupPath,new byte[4096]);
        var reopened=new MarketRepository(repository.DatabasePath);reopened.InitializeCheap();
        Check.Equal(4096L,new FileInfo(repository.VerifiedBackupPath).Length);Check.Equal(1,reopened.Search("",1).Count);
    }

    [Test]public async Task CheapModeRefusesCorruptReadsAndWritesWithoutAutomaticRestore()
    {
        using var directory=new TemporaryDirectory();var repository=Cheap(directory);
        await repository.SaveSnapshotAsync(new(Check.Station(1),[Check.Quote(1,"water",10,9)],DateTimeOffset.UtcNow,DataSource.Journal));SqliteConnection.ClearAllPools();
        long root,size;using(var connection=Open(repository.DatabasePath))
        {using var query=connection.CreateCommand();query.CommandText="PRAGMA wal_checkpoint(TRUNCATE)";query.ExecuteNonQuery();query.CommandText="SELECT rootpage FROM sqlite_schema WHERE name='quotes'";root=(long)query.ExecuteScalar()!;query.CommandText="PRAGMA page_size";size=(long)query.ExecuteScalar()!;}
        using(var file=File.Open(repository.DatabasePath,FileMode.Open,FileAccess.Write,FileShare.None)){file.Position=(root-1)*size;file.Write(new byte[size]);file.Flush(true);}
        var reopened=new MarketRepository(repository.DatabasePath);reopened.InitializeCheap();
        Check.Throws<SqliteException>(()=>reopened.Search("",1));
        try{await reopened.RecoverAsync();throw new Exception("Cheap recovery performed full validation");}catch(InvalidDataException){}
        try{await reopened.SaveSnapshotAsync(new(Check.Station(1),[Check.Quote(1,"water",12,10)],DateTimeOffset.UtcNow,DataSource.Journal));throw new Exception("Corrupt write unexpectedly succeeded");}catch(SqliteException){}
        Check.That(!File.Exists(directory.File("markets.db.active")));Check.That(!Directory.EnumerateFiles(directory.Path,"markets.db.recovered-*").Any());
    }

    [Test]public async Task ExplicitClearRemovesOnlyOwnedMarketFamilyAndRebuildsAnEmptySmallCache()
    {
        using var directory=new TemporaryDirectory();var repository=Cheap(directory);
        await repository.SaveSnapshotAsync(new(Check.Station(1),[Check.Quote(1,"water",10,9)],DateTimeOffset.UtcNow,DataSource.Journal));SqliteConnection.ClearAllPools();
        var original=repository.DatabasePath;var generation=directory.File("markets.db.recovered-download-fixture.db");File.Copy(original,generation);
        File.WriteAllText(original+".active",Path.GetFileName(generation));
        foreach(var suffix in new[]{"-wal","-shm",".verified-generation.json",".diagnostics.jsonl"})File.WriteAllText(generation+suffix,"old-cache-evidence");
        File.WriteAllText(original+".verified.db","old verified");File.WriteAllText(directory.File("markets.db.corrupt-fixture.db"),"old corrupt");
        File.WriteAllText(original+".verified.db."+Guid.NewGuid().ToString("N")+".tmp","unfinished backup");
        var personal=new[]{"settings.json","game-events.db","game-events.db.active","Journal.2026.log","trade-route.json","other-markets.db"};
        foreach(var name in personal)File.WriteAllText(directory.File(name),"personal:"+name);
        var before=repository.GetCacheUsage();Check.That(before.HasLegacyFullCache);Check.That(before.Files>=9);
        var cleared=await repository.ClearAsync();Check.That(cleared.Succeeded,cleared.Message);Check.That(cleared.FreedBytes>0);
        foreach(var name in personal)Check.Equal("personal:"+name,File.ReadAllText(directory.File(name)));
        Check.That(!File.Exists(generation));Check.That(!File.Exists(original+".active"));Check.That(!File.Exists(original+".verified.db"));
        Check.That(cleared.Usage.Bytes<1024*1024);Check.That(!cleared.Usage.HasLegacyFullCache);Check.Equal(0L,repository.Statistics().Quotes);
        await repository.SaveSnapshotAsync(new(Check.Station(2),[Check.Quote(2,"food",20,19)],DateTimeOffset.UtcNow,DataSource.Journal));
        Check.Equal(1,repository.Search("",2).Count);Check.That(!File.Exists(repository.VerifiedBackupPath));
    }

    [Test]public async Task AnOccupiedFamilyMemberRefusesClearBeforeAnyOriginalIsMoved()
    {
        using var directory=new TemporaryDirectory();var repository=Cheap(directory);
        await repository.SaveSnapshotAsync(new(Check.Station(1),[Check.Quote(1,"water",10,9)],DateTimeOffset.UtcNow,DataSource.Journal));SqliteConnection.ClearAllPools();
        var before=File.ReadAllBytes(repository.DatabasePath);File.WriteAllText(repository.VerifiedBackupPath,"do not move this occupied backup");
        using(var foreign=File.OpenHandle(repository.VerifiedBackupPath,FileMode.Open,FileAccess.Read,FileShare.Read))
        {var result=await repository.ClearAsync();Check.That(!result.Succeeded);}
        Check.That(File.ReadAllBytes(repository.DatabasePath).SequenceEqual(before));Check.Equal("do not move this occupied backup",File.ReadAllText(repository.VerifiedBackupPath));
        Check.That(!Directory.EnumerateDirectories(directory.Path,"markets.db.cleanup-*").Any());Check.Equal(1,repository.Search("",1).Count);
    }

    [Test]public async Task InvalidActivePointerCanBeClearedWithoutFollowingItOutsideTheFamily()
    {
        using var directory=new TemporaryDirectory();var repository=Cheap(directory);var original=repository.DatabasePath;SqliteConnection.ClearAllPools();
        File.WriteAllText(directory.File("settings.json"),"preserve me");File.WriteAllText(original+".active","../settings.json");
        Check.That(repository.GetCacheUsage().Files>=2);
        var result=await repository.ClearAsync();Check.That(result.Succeeded,result.Message);
        Check.Equal("preserve me",File.ReadAllText(directory.File("settings.json")));Check.Equal(original,repository.DatabasePath);Check.Equal(0L,repository.Statistics().Quotes);
    }

    [Test]public void FailedReplacementRollsBackEveryQuarantinedOriginal()
    {
        using var directory=new TemporaryDirectory();var repository=Cheap(directory);SqliteConnection.ClearAllPools();
        var original=repository.DatabasePath;var before=File.ReadAllBytes(original);File.WriteAllText(repository.VerifiedBackupPath,"old verified backup");
        var cleanup=typeof(MarketRepository).Assembly.GetType("XFEEDToolBox.Core.Data.MarketCacheCleanup")!;
        var clear=cleanup.GetMethod("Clear",BindingFlags.NonPublic|BindingFlags.Static)!;
        var result=(MarketCacheClearResult)clear.Invoke(null,[original,(Action)(()=>throw new InvalidDataException("fixture replacement refused")),(Action)SqliteConnection.ClearAllPools,CancellationToken.None])!;
        Check.That(!result.Succeeded);Check.That(File.ReadAllBytes(original).SequenceEqual(before));Check.Equal("old verified backup",File.ReadAllText(repository.VerifiedBackupPath));
        Check.That(!Directory.EnumerateDirectories(directory.Path,"markets.db.cleanup-*").Any());
    }

    [Test]public async Task PublicFilteringRunsBeforeTheGateAndAlwaysRetainsLocalJournalObservations()
    {
        using var directory=new TemporaryDirectory();var repository=Cheap(directory);
        repository.PublicDataFilter=station=>{_ = repository.GetStation(station.MarketId);return station.Position?.X<=150;};
        await repository.SaveStationAsync(Check.Station(1,1000));Check.That(repository.GetStation(1) is null);
        await repository.SaveStationAsync(Check.Station(2,1000),localObservation:true);Check.That(repository.GetStation(2) is not null);
        Check.That(!await repository.SaveSnapshotAsync(new(Check.Station(3,1000),[Check.Quote(3,"water",10,9)],DateTimeOffset.UtcNow,DataSource.Spansh)));
        Check.That(repository.GetStation(3) is null);
        Check.That(await repository.SaveSnapshotAsync(new(Check.Station(4,1000),[Check.Quote(4,"water",10,9)],DateTimeOffset.UtcNow,DataSource.Journal)));
        Check.That(await repository.SaveSnapshotAsync(new(Check.Station(5,10),[Check.Quote(5,"food",20,19)],DateTimeOffset.UtcNow,DataSource.Capture)));
        Check.Equal(2L,repository.Statistics().Quotes);
    }
    [Test]public void PostCommitPurgeFailureNeverRemovesTheReplacementOrRestoresDeletedOldFiles()
    {
        using var directory=new TemporaryDirectory();var original=directory.File("markets.db");File.WriteAllText(original,"old cache");File.WriteAllText(original+".verified.db","old backup");
        var cleanup=typeof(MarketRepository).Assembly.GetType("XFEEDToolBox.Core.Data.MarketCacheCleanup")!;
        var clear=cleanup.GetMethod("Clear",BindingFlags.NonPublic|BindingFlags.Static)!;string? extra=null;
        Action createEmpty=()=>
        {
            File.WriteAllText(original,"new smaller cache");
            var quarantine=Directory.EnumerateDirectories(directory.Path,"markets.db.cleanup-*").Single();
            extra=Path.Combine(quarantine,"unexpected-empty-child");Directory.CreateDirectory(extra);
        };
        var result=(MarketCacheClearResult)clear.Invoke(null,[original,createEmpty,(Action)(()=>{}),CancellationToken.None])!;
        Check.That(result.Succeeded,result.Message);Check.Equal("new smaller cache",File.ReadAllText(original));Check.That(!File.Exists(original+".verified.db"));
        Check.That(Directory.Exists(extra));Check.Equal(0L,result.FreedBytes);
    }
}