335 lines
13 KiB
C#
335 lines
13 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
|
|
using RolemasterDb.App.Data;
|
|
using RolemasterDb.App.Domain;
|
|
using RolemasterDb.ImportTool.Parsing;
|
|
|
|
namespace RolemasterDb.ImportTool.Tests;
|
|
|
|
public sealed class CriticalImportMergeIntegrationTests
|
|
{
|
|
private static readonly PdfXmlExtractor Extractor = new();
|
|
private static readonly StandardCriticalTableParser StandardParser = new();
|
|
|
|
[Fact]
|
|
public async Task Reimport_updates_uncurated_results_and_refreshes_source_metadata()
|
|
{
|
|
var (parseResult, _) = await LoadPreparedSlashParseResultAsync();
|
|
var databasePath = CreateEmptyDatabasePath();
|
|
var loader = new CriticalImportLoader(databasePath);
|
|
|
|
await loader.LoadAsync(parseResult.Table);
|
|
|
|
await using (var dbContext = CreateDbContext(databasePath))
|
|
{
|
|
var result = await LoadResultAsync(dbContext, "36-45", "B");
|
|
result.IsCurated = false;
|
|
result.RawCellText = "Temporary raw text";
|
|
result.DescriptionText = "Temporary description";
|
|
result.RawAffixText = "+99H";
|
|
result.ParseStatus = "manually_curated";
|
|
result.SourcePageNumber = null;
|
|
result.SourceImagePath = null;
|
|
result.SourceImageCropJson = null;
|
|
|
|
dbContext.CriticalEffects.RemoveRange(result.Effects);
|
|
foreach (var branch in result.Branches)
|
|
{
|
|
dbContext.CriticalEffects.RemoveRange(branch.Effects);
|
|
}
|
|
|
|
dbContext.CriticalBranches.RemoveRange(result.Branches);
|
|
result.Effects.Clear();
|
|
result.Branches.Clear();
|
|
await dbContext.SaveChangesAsync();
|
|
}
|
|
|
|
await loader.LoadAsync(parseResult.Table);
|
|
|
|
await using (var dbContext = CreateDbContext(databasePath))
|
|
{
|
|
var result = await LoadResultAsync(dbContext, "36-45", "B");
|
|
|
|
Assert.NotEqual("Temporary raw text", result.RawCellText);
|
|
Assert.NotEqual("Temporary description", result.DescriptionText);
|
|
Assert.NotEqual("+99H", result.RawAffixText);
|
|
Assert.NotEqual("manually_curated", result.ParseStatus);
|
|
Assert.NotNull(result.SourcePageNumber);
|
|
Assert.False(string.IsNullOrWhiteSpace(result.SourceImagePath));
|
|
Assert.False(string.IsNullOrWhiteSpace(result.SourceImageCropJson));
|
|
Assert.NotEmpty(result.Branches);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Reimport_preserves_curated_results_and_child_rows()
|
|
{
|
|
var (parseResult, _) = await LoadPreparedSlashParseResultAsync();
|
|
var databasePath = CreateEmptyDatabasePath();
|
|
var loader = new CriticalImportLoader(databasePath);
|
|
|
|
await loader.LoadAsync(parseResult.Table);
|
|
|
|
await using (var dbContext = CreateDbContext(databasePath))
|
|
{
|
|
var result = await LoadResultAsync(dbContext, "36-45", "B");
|
|
|
|
result.IsCurated = true;
|
|
result.RawCellText = "Curated raw text";
|
|
result.DescriptionText = "Curated result prose.";
|
|
result.RawAffixText = "+12H";
|
|
result.ParseStatus = "manually_curated";
|
|
result.ParsedJson = "{\"reviewed\":true}";
|
|
|
|
dbContext.CriticalEffects.RemoveRange(result.Effects);
|
|
foreach (var branch in result.Branches)
|
|
{
|
|
dbContext.CriticalEffects.RemoveRange(branch.Effects);
|
|
}
|
|
|
|
dbContext.CriticalBranches.RemoveRange(result.Branches);
|
|
result.Effects.Clear();
|
|
result.Branches.Clear();
|
|
|
|
result.Effects.Add(new CriticalEffect
|
|
{
|
|
EffectCode = CriticalEffectCodes.DirectHits,
|
|
ValueInteger = 12,
|
|
IsPermanent = false,
|
|
SourceType = "manual",
|
|
SourceText = "+12H"
|
|
});
|
|
|
|
result.Branches.Add(new CriticalBranch
|
|
{
|
|
BranchKind = "conditional",
|
|
ConditionKey = "with_shield",
|
|
ConditionText = "with shield",
|
|
ConditionJson = "{}",
|
|
RawText = "with shield: glancing blow.",
|
|
DescriptionText = "Glancing blow.",
|
|
ParsedJson = "{}",
|
|
SortOrder = 1,
|
|
Effects =
|
|
[
|
|
new CriticalEffect
|
|
{
|
|
EffectCode = CriticalEffectCodes.MustParryRounds,
|
|
DurationRounds = 2,
|
|
IsPermanent = false,
|
|
SourceType = "manual",
|
|
SourceText = "2mp"
|
|
}
|
|
]
|
|
});
|
|
|
|
await dbContext.SaveChangesAsync();
|
|
}
|
|
|
|
await loader.LoadAsync(parseResult.Table);
|
|
|
|
await using (var dbContext = CreateDbContext(databasePath))
|
|
{
|
|
var result = await LoadResultAsync(dbContext, "36-45", "B");
|
|
|
|
Assert.True(result.IsCurated);
|
|
Assert.Equal("Curated raw text", result.RawCellText);
|
|
Assert.Equal("Curated result prose.", result.DescriptionText);
|
|
Assert.Equal("+12H", result.RawAffixText);
|
|
Assert.Equal("manually_curated", result.ParseStatus);
|
|
Assert.Equal("{\"reviewed\":true}", result.ParsedJson);
|
|
Assert.Single(result.Effects);
|
|
Assert.Equal(12, result.Effects[0].ValueInteger);
|
|
Assert.Single(result.Branches);
|
|
Assert.Equal("with_shield", result.Branches[0].ConditionKey);
|
|
Assert.Single(result.Branches[0].Effects);
|
|
Assert.Equal(2, result.Branches[0].Effects[0].DurationRounds);
|
|
Assert.False(string.IsNullOrWhiteSpace(result.SourceImagePath));
|
|
Assert.False(string.IsNullOrWhiteSpace(result.SourceImageCropJson));
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Reimport_deletes_only_unmatched_uncurated_results()
|
|
{
|
|
var (parseResult, _) = await LoadPreparedSlashParseResultAsync();
|
|
var databasePath = CreateEmptyDatabasePath();
|
|
var loader = new CriticalImportLoader(databasePath);
|
|
|
|
await loader.LoadAsync(parseResult.Table);
|
|
|
|
await using (var dbContext = CreateDbContext(databasePath))
|
|
{
|
|
var curatedMissing = await LoadResultAsync(dbContext, "01-05", "A");
|
|
curatedMissing.IsCurated = true;
|
|
curatedMissing.DescriptionText = "Keep me.";
|
|
|
|
var uncuratedMissing = await LoadResultAsync(dbContext, "01-05", "B");
|
|
uncuratedMissing.IsCurated = false;
|
|
|
|
await dbContext.SaveChangesAsync();
|
|
}
|
|
|
|
var trimmedTable = CreateTrimmedTable(
|
|
parseResult.Table,
|
|
("01-05", "A"),
|
|
("01-05", "B"));
|
|
|
|
await loader.LoadAsync(trimmedTable);
|
|
|
|
await using (var dbContext = CreateDbContext(databasePath))
|
|
{
|
|
var results = await dbContext.CriticalResults
|
|
.Include(item => item.CriticalTable)
|
|
.Include(item => item.CriticalColumn)
|
|
.Include(item => item.CriticalRollBand)
|
|
.Where(item => item.CriticalTable.Slug == "slash" && item.CriticalRollBand.Label == "01-05")
|
|
.ToListAsync();
|
|
|
|
Assert.Contains(results, item => item.CriticalColumn.ColumnKey == "A" && item.IsCurated && item.DescriptionText == "Keep me.");
|
|
Assert.DoesNotContain(results, item => item.CriticalColumn.ColumnKey == "B");
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Reimport_images_only_refreshes_provenance_without_touching_curated_content()
|
|
{
|
|
var (parseResult, _) = await LoadPreparedSlashParseResultAsync();
|
|
var databasePath = CreateEmptyDatabasePath();
|
|
var loader = new CriticalImportLoader(databasePath);
|
|
|
|
await loader.LoadAsync(parseResult.Table);
|
|
|
|
await using (var dbContext = CreateDbContext(databasePath))
|
|
{
|
|
var result = await LoadResultAsync(dbContext, "36-45", "B");
|
|
|
|
result.IsCurated = true;
|
|
result.RawCellText = "Curated raw text";
|
|
result.DescriptionText = "Curated description";
|
|
result.RawAffixText = "+12H";
|
|
result.ParseStatus = "manually_curated";
|
|
result.SourcePageNumber = null;
|
|
result.SourceImagePath = null;
|
|
result.SourceImageCropJson = null;
|
|
|
|
await dbContext.SaveChangesAsync();
|
|
}
|
|
|
|
await loader.RefreshImageArtifactsAsync(parseResult.Table);
|
|
|
|
await using (var dbContext = CreateDbContext(databasePath))
|
|
{
|
|
var result = await LoadResultAsync(dbContext, "36-45", "B");
|
|
|
|
Assert.True(result.IsCurated);
|
|
Assert.Equal("Curated raw text", result.RawCellText);
|
|
Assert.Equal("Curated description", result.DescriptionText);
|
|
Assert.Equal("+12H", result.RawAffixText);
|
|
Assert.Equal("manually_curated", result.ParseStatus);
|
|
Assert.NotNull(result.SourcePageNumber);
|
|
Assert.False(string.IsNullOrWhiteSpace(result.SourceImagePath));
|
|
Assert.False(string.IsNullOrWhiteSpace(result.SourceImageCropJson));
|
|
}
|
|
}
|
|
|
|
private static ParsedCriticalTable CreateTrimmedTable(
|
|
ParsedCriticalTable table,
|
|
params (string RollBandLabel, string ColumnKey)[] excludedResults)
|
|
{
|
|
var excludedKeys = excludedResults
|
|
.Select(item => $"{item.RollBandLabel}|{item.ColumnKey}")
|
|
.ToHashSet(StringComparer.Ordinal);
|
|
|
|
return new ParsedCriticalTable(
|
|
table.Slug,
|
|
table.DisplayName,
|
|
table.Family,
|
|
table.SourceDocument,
|
|
table.Notes,
|
|
table.Groups,
|
|
table.Columns,
|
|
table.RollBands,
|
|
table.Results
|
|
.Where(item => !excludedKeys.Contains($"{item.RollBandLabel}|{item.ColumnKey}"))
|
|
.ToList());
|
|
}
|
|
|
|
private static async Task<(CriticalTableParseResult ParseResult, ImportArtifactPaths ArtifactPaths)> LoadPreparedSlashParseResultAsync()
|
|
{
|
|
var entry = LoadManifest().Tables.Single(item => item.Slug == "slash");
|
|
var xmlPath = Path.Combine(GetArtifactCacheRoot(), $"{entry.Slug}.xml");
|
|
|
|
if (!File.Exists(xmlPath))
|
|
{
|
|
await Extractor.ExtractAsync(Path.Combine(GetRepositoryRoot(), entry.PdfPath), xmlPath);
|
|
}
|
|
|
|
var parseResult = StandardParser.Parse(entry, await File.ReadAllTextAsync(xmlPath));
|
|
var artifactRoot = Path.Combine(GetArtifactCacheRoot(), Guid.NewGuid().ToString("N"));
|
|
var artifactPaths = ImportArtifactPaths.Create(artifactRoot, entry.Slug);
|
|
var generator = new CriticalSourceImageArtifactGenerator(new PdfXmlExtractor());
|
|
|
|
await generator.GenerateAsync(Path.Combine(GetRepositoryRoot(), entry.PdfPath), artifactPaths, parseResult);
|
|
return (parseResult, artifactPaths);
|
|
}
|
|
|
|
private static async Task<CriticalResult> LoadResultAsync(RolemasterDbContext dbContext, string rollBandLabel, string columnKey) =>
|
|
await dbContext.CriticalResults
|
|
.Include(item => item.CriticalTable)
|
|
.Include(item => item.CriticalColumn)
|
|
.Include(item => item.CriticalRollBand)
|
|
.Include(item => item.Effects)
|
|
.Include(item => item.Branches)
|
|
.ThenInclude(branch => branch.Effects)
|
|
.SingleAsync(item =>
|
|
item.CriticalTable.Slug == "slash" &&
|
|
item.CriticalRollBand.Label == rollBandLabel &&
|
|
item.CriticalColumn.ColumnKey == columnKey);
|
|
|
|
private static ParsedCriticalResult FindResult(CriticalTableParseResult parseResult, string rollBandLabel, string columnKey) =>
|
|
parseResult.Table.Results.Single(item =>
|
|
item.GroupKey is null &&
|
|
item.RollBandLabel == rollBandLabel &&
|
|
item.ColumnKey == columnKey);
|
|
|
|
private static CriticalImportManifest LoadManifest() =>
|
|
new CriticalImportManifestLoader().Load(Path.Combine(GetRepositoryRoot(), "sources", "critical-import-manifest.json"));
|
|
|
|
private static RolemasterDbContext CreateDbContext(string databasePath)
|
|
{
|
|
var options = new DbContextOptionsBuilder<RolemasterDbContext>()
|
|
.UseSqlite($"Data Source={databasePath}")
|
|
.Options;
|
|
|
|
return new RolemasterDbContext(options);
|
|
}
|
|
|
|
private static string CreateEmptyDatabasePath() =>
|
|
Path.Combine(GetArtifactCacheRoot(), $"critical-import-{Guid.NewGuid():N}.db");
|
|
|
|
private static string GetArtifactCacheRoot()
|
|
{
|
|
var cacheRoot = Path.Combine(Path.GetTempPath(), "RolemasterDb.ImportTool.MergeTests");
|
|
Directory.CreateDirectory(cacheRoot);
|
|
return cacheRoot;
|
|
}
|
|
|
|
private static string GetRepositoryRoot()
|
|
{
|
|
var probe = new DirectoryInfo(AppContext.BaseDirectory);
|
|
|
|
while (probe is not null)
|
|
{
|
|
if (File.Exists(Path.Combine(probe.FullName, "RolemasterDB.slnx")))
|
|
{
|
|
return probe.FullName;
|
|
}
|
|
|
|
probe = probe.Parent;
|
|
}
|
|
|
|
throw new InvalidOperationException("Could not find the repository root for integration tests.");
|
|
}
|
|
}
|