Files
prep-step/PrepStep.Tests/PipelineEndToEndTests.cs
T
davideandClaude Sonnet 5 5c9661eb12 Fix euristica BOM, unifica script/GUI su un'unica fonte, aggiungi test integrazione/E2E
Due bug reali trovati confrontando l'estrazione BOM con la BOM ufficiale Creo:
un componente scartato per placement fallito in un file veniva bloccato per
sempre anche quando il file giusto aveva un vero placement (perdendo interi
sotto-alberi come 9256510047), e i codici con suffisso (_C01 ecc.) richiedevano
sempre placement esplicito invece dell'eccezione per occorrenze ripetute già
presente nella GUI. Entrambi corretti.

AssemblyComponentScanner reso pubblico e scripts/BomViaDb.cs, ExtractCreoStrings.cs
riscritti per referenziarlo via "#:project" invece di duplicare l'euristica:
un'unica fonte di verità condivisa da GUI e script standalone.

Aggiunti test di integrazione per Indexer.ScanAsync (mai coperto oltre al
parsing del nome file) e test end-to-end che ricostruiscono l'intera pipeline
disco -> indice -> risoluzione albero componenti senza toccare la UI.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 13:43:54 +02:00

157 lines
7.4 KiB
C#

using Microsoft.Data.Sqlite;
using PrepStep;
namespace PrepStep.Tests;
/// <summary>End-to-end: disco CAD finto -> Indexer.ScanAsync -> cad_index.db reale -> BFS di
/// risoluzione albero componenti (stesso algoritmo di Form1.RisolviAlberoComponenti e
/// scripts/BomViaDb.cs, che ora delegano entrambi ad AssemblyComponentScanner - vedi
/// docs/formato-asm.md). Nessuna UI coinvolta: verifica che le tre fasi si compongano
/// correttamente, non solo che ciascuna funzioni isolata. I contenuti .asm/.prt sono testo
/// sintetico con gli stessi marcatori documentati per un vero file Creo ("Componente ID N
/// (...)"), non file binari reali - sufficiente perché AssemblyComponentScanner legge il file
/// come testo Latin1 indipendentemente dal resto del formato.</summary>
public class PipelineEndToEndTests : IDisposable
{
private readonly string _root = Path.Combine(Path.GetTempPath(), $"cadsearch_e2e_{Guid.NewGuid():N}");
private readonly string _dbPath;
public PipelineEndToEndTests()
{
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)
{
Directory.CreateDirectory(Path.GetDirectoryName(percorso)!);
File.WriteAllText(percorso, contenuto);
}
/// <summary>Stesso algoritmo BFS di scripts/BomViaDb.cs: qui non è duplicazione a rischio
/// come lo era prima della riscrittura (vedi docs/formato-asm.md) - è la prova, in un
/// test, che comporre AssemblyComponentScanner con un semplice ciclo di accodamento dà il
/// risultato atteso sopra un indice reale, non un mock.</summary>
private static (SortedSet<string> Trovati, List<string> NonTrovati) RisolviAlbero(
string asmIniziale, Dictionary<string, List<(string Tipo, string Percorso)>> codiciNoti)
{
string codiceProprio = Path.GetFileName(asmIniziale).Split('.')[0];
string codiceRadice = AssemblyComponentScanner.CodiceRadice(codiceProprio);
var trovati = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
var nonTrovati = new List<string>();
var visti = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { codiceProprio, codiceRadice };
var coda = new Queue<string>(new[] { asmIniziale });
while (coda.Count > 0)
{
var asm = coda.Dequeue();
foreach (var codice in AssemblyComponentScanner.EstraiCodiciReferenziati(asm, codiciNoti.Keys))
{
if (!visti.Add(codice))
continue;
if (!codiciNoti.TryGetValue(codice, out var componenti))
{
nonTrovati.Add(codice);
continue;
}
trovati.Add(codice);
foreach (var (tipo, percorso) in componenti)
if (tipo == "asm")
coda.Enqueue(percorso);
}
}
return (trovati, nonTrovati);
}
private Dictionary<string, List<(string Tipo, string Percorso)>> CaricaCodiciNoti()
{
using var connection = new SqliteConnection($"Data Source={_dbPath};Mode=ReadOnly");
connection.Open();
using var cmd = connection.CreateCommand();
cmd.CommandText = "SELECT codice, tipo_file, percorso_completo FROM files_ultima_versione "
+ "WHERE LOWER(tipo_file) IN ('prt', 'asm')";
using var reader = cmd.ExecuteReader();
var codiciNoti = new Dictionary<string, List<(string, string)>>(StringComparer.OrdinalIgnoreCase);
while (reader.Read())
{
string codice = reader.GetString(0);
var lista = codiciNoti.TryGetValue(codice, out var l) ? l : codiciNoti[codice] = new();
lista.Add((reader.GetString(1), reader.GetString(2)));
}
return codiciNoti;
}
[Fact]
public async Task Risolve_lalbero_completo_a_partire_dal_disco_indicizzato()
{
// TOP -> CHILD (vero placement, 2 occorrenze) -> LEAF (vero placement, 2 occorrenze).
// "99999999" compare una sola volta in TOP: rumore binario, va scartato anche se
// fosse indicizzato (vedi AssemblyComponentScannerTests.Scarta_codici_con_una_sola_occorrenza).
ScriviFile(Path.Combine(_root, "MACCHINE", "GRUPPO1", "12345678.asm.1"),
"inizio Componente ID 1 (23456789.ASM) testo Componente ID 2 (23456789.ASM) rumore 99999999 fine");
ScriviFile(Path.Combine(_root, "MACCHINE", "GRUPPO1", "23456789.asm.1"),
"Componente ID 1 (34567890.PRT) blah Componente ID 2 (34567890.PRT) fine");
ScriviFile(Path.Combine(_root, "MACCHINE", "GRUPPO1", "34567890.prt.1"), "dati foglia");
ScriviFile(Path.Combine(_root, "MACCHINE", "GRUPPO1", "99999999.prt.1"), "mai referenziato davvero");
var progress = new Progress<ScanProgress>();
await Indexer.ScanAsync(_root, _dbPath, progress, CancellationToken.None);
var codiciNoti = CaricaCodiciNoti();
var asmIniziale = Path.Combine(_root, "MACCHINE", "GRUPPO1", "12345678.asm.1");
var (trovati, nonTrovati) = RisolviAlbero(asmIniziale, codiciNoti);
Assert.Equal(new[] { "23456789", "34567890" }, trovati);
Assert.DoesNotContain("99999999", trovati);
Assert.Empty(nonTrovati);
}
[Fact]
public async Task Segnala_come_non_trovato_un_componente_con_placement_vero_ma_mai_indicizzato()
{
// "23456789" ha un vero placement in TOP ma il suo file non esiste sul disco finto
// (mai scansionato -> non in cad_index.db): deve finire tra i "non trovati", non far
// fallire la risoluzione dell'albero.
ScriviFile(Path.Combine(_root, "MACCHINE", "GRUPPO1", "12345678.asm.1"),
"Componente ID 1 (23456789.ASM) testo Componente ID 2 (23456789.ASM) fine");
var progress = new Progress<ScanProgress>();
await Indexer.ScanAsync(_root, _dbPath, progress, CancellationToken.None);
var codiciNoti = CaricaCodiciNoti();
var asmIniziale = Path.Combine(_root, "MACCHINE", "GRUPPO1", "12345678.asm.1");
var (trovati, nonTrovati) = RisolviAlbero(asmIniziale, codiciNoti);
Assert.Empty(trovati);
Assert.Contains("23456789", nonTrovati);
}
[Fact]
public async Task File_in_cartella_temp_non_entra_nellindice_e_quindi_non_viene_risolto()
{
// Stesso codice "23456789" esiste per davvero, ma solo dentro una cartella radice
// "temp_..." - esclusa dalla scansione (vedi Indexer.cs e docs/formato-asm.md, falso
// positivo su copie scollegate dentro temp_*). Il componente deve restare "non
// trovato", non risolversi per sbaglio contro la copia in temp_.
ScriviFile(Path.Combine(_root, "MACCHINE", "GRUPPO1", "12345678.asm.1"),
"Componente ID 1 (23456789.ASM) testo Componente ID 2 (23456789.ASM) fine");
ScriviFile(Path.Combine(_root, "temp_davide", "23456789.asm.1"), "copia scollegata");
var progress = new Progress<ScanProgress>();
await Indexer.ScanAsync(_root, _dbPath, progress, CancellationToken.None);
var codiciNoti = CaricaCodiciNoti();
var asmIniziale = Path.Combine(_root, "MACCHINE", "GRUPPO1", "12345678.asm.1");
var (trovati, nonTrovati) = RisolviAlbero(asmIniziale, codiciNoti);
Assert.Empty(trovati);
Assert.Contains("23456789", nonTrovati);
}
}