using Microsoft.Data.Sqlite; using PrepStep; namespace PrepStep.Tests; /// Integration test di Indexer.ScanAsync: disco temporaneo reale + DB SQLite reale, /// nessun dato inserito a mano (a differenza di CaseInsensitiveLookupIntegrationTests, che /// verifica solo le query a valle su righe già presenti). Copre la scansione vera - upsert /// incrementale, rilevamento nuovi/aggiornati/invariati/rimossi, esclusione cartelle "temp_" - /// che prima di questi test non aveva nessuna copertura (solo Indexer.ParseFilename era /// testato). public class IndexerScanTests : IDisposable { private readonly string _root = Path.Combine(Path.GetTempPath(), $"cadsearch_scan_{Guid.NewGuid():N}"); private readonly string _dbPath; public IndexerScanTests() { Directory.CreateDirectory(_root); _dbPath = Path.Combine(_root, "indice.db"); } public void Dispose() { SqliteConnection.ClearAllPools(); Directory.Delete(_root, recursive: true); } private static void ScriviFile(string percorso, string contenuto = "dati") { Directory.CreateDirectory(Path.GetDirectoryName(percorso)!); File.WriteAllText(percorso, contenuto); } /// IProgress<T> sincrono: Progress<T> marshala il report tramite /// SynchronizationContext.Post (o il thread pool, se assente, come nei test) - l'ultimo /// valore non è garantito visibile subito dopo l'await. Qui Report gira sullo stesso /// thread di background di Indexer.Scan, prima che Task.Run completi: l'ultimo valore è /// quindi affidabile non appena l'await ritorna. private sealed class ProgressoSincrono : IProgress { public ScanProgress? Ultimo { get; private set; } public void Report(ScanProgress value) => Ultimo = value; } private async Task ScanAsync() { await Indexer.ScanAsync(_root, _dbPath, new ProgressoSincrono(), CancellationToken.None); } private List<(string Codice, string Tipo, string? Famiglia, string CartellaRadice)> LeggiFiles() { using var connection = new SqliteConnection($"Data Source={_dbPath};Mode=ReadOnly"); connection.Open(); using var cmd = connection.CreateCommand(); cmd.CommandText = "SELECT codice, tipo_file, famiglia, cartella_radice FROM files"; using var reader = cmd.ExecuteReader(); var righe = new List<(string, string, string?, string)>(); while (reader.Read()) righe.Add((reader.GetString(0), reader.GetString(1), reader.IsDBNull(2) ? null : reader.GetString(2), reader.GetString(3))); return righe; } [Fact] public async Task Indicizza_un_file_estraendo_codice_tipo_versione_e_famiglia() { ScriviFile(Path.Combine(_root, "__Corpi", "442", "9244228573.asm.16")); await ScanAsync(); var righe = LeggiFiles(); Assert.Single(righe); Assert.Equal(("9244228573", "asm", "442", "__Corpi"), righe[0]); } [Fact] public async Task Esclude_le_cartelle_radice_che_iniziano_con_temp_() { ScriviFile(Path.Combine(_root, "temp_paris", "vecchio.asm.1")); ScriviFile(Path.Combine(_root, "__Corpi", "9244228573.asm.16")); await ScanAsync(); var righe = LeggiFiles(); Assert.Single(righe); Assert.Equal("__Corpi", righe[0].CartellaRadice); } [Fact] public async Task File_non_modificato_resta_invariato_tra_due_scansioni() { string percorso = Path.Combine(_root, "__Corpi", "file.prt.1"); ScriviFile(percorso); await ScanAsync(); var progress = new ProgressoSincrono(); await Indexer.ScanAsync(_root, _dbPath, progress, CancellationToken.None); Assert.NotNull(progress.Ultimo); Assert.Equal(0, progress.Ultimo!.Nuovi); Assert.Equal(0, progress.Ultimo.Aggiornati); Assert.Equal(1, progress.Ultimo.Invariati); Assert.Equal(0, progress.Ultimo.Rimossi); } [Fact] public async Task File_modificato_viene_rilevato_come_aggiornato() { string percorso = Path.Combine(_root, "__Corpi", "file.prt.1"); ScriviFile(percorso, "versione 1"); await ScanAsync(); // Contenuto più lungo -> dimensione diversa, quindi rilevato come modifica anche se // il file system non aggiornasse data_modifica entro la risoluzione del clock. ScriviFile(percorso, "versione 2 con più testo dentro"); var progress = new ProgressoSincrono(); await Indexer.ScanAsync(_root, _dbPath, progress, CancellationToken.None); Assert.NotNull(progress.Ultimo); Assert.Equal(1, progress.Ultimo!.Aggiornati); Assert.Equal(0, progress.Ultimo.Nuovi); } [Fact] public async Task File_cancellato_dal_disco_viene_rimosso_dal_db_alla_scansione_successiva() { string percorso = Path.Combine(_root, "__Corpi", "file.prt.1"); ScriviFile(percorso); await ScanAsync(); Assert.Single(LeggiFiles()); File.Delete(percorso); await ScanAsync(); Assert.Empty(LeggiFiles()); } [Fact] public async Task Files_ultima_versione_tiene_solo_la_versione_piu_recente_per_codice_e_tipo() { ScriviFile(Path.Combine(_root, "__Corpi", "9244228573.asm.40")); ScriviFile(Path.Combine(_root, "__Corpi", "9244228573.asm.41")); await ScanAsync(); using var connection = new SqliteConnection($"Data Source={_dbPath};Mode=ReadOnly"); connection.Open(); using var cmd = connection.CreateCommand(); cmd.CommandText = "SELECT versione FROM files_ultima_versione WHERE codice = '9244228573'"; using var reader = cmd.ExecuteReader(); Assert.True(reader.Read()); Assert.Equal(41L, reader.GetInt64(0)); Assert.False(reader.Read()); } [Fact] public async Task File_senza_pattern_codice_tipo_versione_va_in_tipo_altro() { ScriviFile(Path.Combine(_root, "__Corpi", "leggimi.txt")); await ScanAsync(); var righe = LeggiFiles(); Assert.Single(righe); Assert.Equal("altro", righe[0].Tipo); } }