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>
This commit is contained in:
1 parent
353a1028d9
commit
5c9661eb12
8 files changed
+514
-171
No files matched your search
@@ -128,12 +128,18 @@ disco di default `X:\`, hardcoded in `Form1.RadiceDiscoDefault`).
|
||||
## Script standalone (`scripts/`)
|
||||
|
||||
`scripts/` contiene C# file-based apps (.NET 10, `dotnet run scripts/Nome.cs -- arg1 arg2`),
|
||||
senza csproj — dipendenze NuGet extra si aggiungono con `#:package Nome@versione` in cima
|
||||
al file. `ExtractCreoStrings.cs` estrae la BOM di un `.asm` risolvendo ricorsivamente i
|
||||
sotto-assiemi contro `cad_index.db` (stessa logica di `Form1.AssiemeCompleto`, solo codici
|
||||
numerici Pro-E); `BomViaDb.cs` fa lo stesso ma con la tokenizzazione generica validata contro
|
||||
il DB, usata per intercettare anche i nomi alfabetici delle parti commerciali (vedi
|
||||
`docs/formato-asm.md`); `Analyze.cs` dumpa il contesto testo/hex attorno a un pattern in un
|
||||
senza csproj proprio — dipendenze NuGet extra si aggiungono con `#:package Nome@versione` in
|
||||
cima al file. `ExtractCreoStrings.cs` e `BomViaDb.cs` risolvono ricorsivamente l'albero
|
||||
componenti di un `.asm` contro `cad_index.db` da riga di comando (stessa logica di
|
||||
`Form1.RisolviAlberoComponenti`/`AssiemeCompleto`): entrambi referenziano
|
||||
`PrepStep/PrepStep.csproj` via `#:project` (più `#:property TargetFramework=net10.0-windows`,
|
||||
richiesto perché il progetto principale è WinForms) invece di duplicare l'euristica di
|
||||
estrazione/placement, che vive solo in `PrepStep/AssemblyComponentScanner.cs` — un'unica
|
||||
fonte di verità condivisa da GUI e script, vedi `docs/formato-asm.md`. Le due varianti restano
|
||||
distinte solo nel candidato passato a `EstraiCodiciReferenziati`: `ExtractCreoStrings.cs` non
|
||||
passa `codiciNoti` (riconosce solo i codici numerici Pro-E, dalla forma, senza bisogno del
|
||||
DB); `BomViaDb.cs` lo passa, per intercettare anche i nomi alfabetici delle parti commerciali
|
||||
validati contro l'indice. `Analyze.cs` dumpa il contesto testo/hex attorno a un pattern in un
|
||||
file binario, per investigare a mano nuovi falsi positivi prima di toccare la regex; `Query.cs`
|
||||
esegue una query SQL ad-hoc contro `cad_index.db` da riga di comando.
|
||||
|
||||
|
||||
@@ -148,4 +148,54 @@ public class AssemblyComponentScannerTests
|
||||
}
|
||||
finally { File.Delete(percorso); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Riconosce_codice_con_suffisso_anche_senza_dettaglio_nel_placement()
|
||||
{
|
||||
// Regressione: caso reale 9284218024b.asm -> 2546220101_C01.._C04. Il placement
|
||||
// riporta solo il codice padre senza suffisso ("Componente ID N (2546220101.PRT)"),
|
||||
// il dettaglio "_C01" compare solo altrove come stringa ripetuta, mai dentro un
|
||||
// placement esplicito. Un codice nudo nella stessa situazione andrebbe scartato
|
||||
// (vedi Scarta_codice_ripetuto_senza_un_vero_placement), ma un codice con suffisso
|
||||
// di variante è considerato "forma nota": bastano le occorrenze ripetute. Prima
|
||||
// che questa eccezione fosse portata anche in scripts/BomViaDb.cs, lo script
|
||||
// perdeva l'intero sotto-assieme (vedi docs/formato-asm.md).
|
||||
string percorso = ScriviAsmFinto(
|
||||
"Componente ID 1 (2546220101.PRT) x 2546220101_C01 y 2546220101_C01 z");
|
||||
try
|
||||
{
|
||||
var codici = AssemblyComponentScanner.EstraiCodiciReferenziati(percorso);
|
||||
Assert.Contains("2546220101_C01", codici);
|
||||
}
|
||||
finally { File.Delete(percorso); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Il_placement_e_indipendente_per_ogni_file_scansionato()
|
||||
{
|
||||
// Regressione: caso reale 9244228558.asm -> 9244236104 dentro 9244228558_C01.asm.
|
||||
// Lo stesso codice compare come riferimento fantasma (nudo, senza placement) nel
|
||||
// file dell'assieme padre, e con un vero placement nel file del sotto-assieme che
|
||||
// lo contiene realmente. La proprietà che rende sicura la BFS del chiamante
|
||||
// (Form1.RisolviAlberoComponenti, scripts/BomViaDb.cs, scripts/ExtractCreoStrings.cs)
|
||||
// è che EstraiCodiciReferenziati valuta il placement da zero per ogni file, senza
|
||||
// stato condiviso tra le chiamate: un fallimento nel file padre non deve
|
||||
// condizionare il risultato quando lo stesso codice viene ripassato per il file
|
||||
// figlio (vedi docs/formato-asm.md, "Trappola nella BFS").
|
||||
string filePadre = ScriviAsmFinto("assieme 11112222 padre 11112222 fine");
|
||||
string fileFiglio = ScriviAsmFinto("Componente ID 1 (11112222.PRT) x 11112222 y");
|
||||
try
|
||||
{
|
||||
Assert.DoesNotContain("11112222", AssemblyComponentScanner.EstraiCodiciReferenziati(filePadre));
|
||||
Assert.Contains("11112222", AssemblyComponentScanner.EstraiCodiciReferenziati(fileFiglio));
|
||||
}
|
||||
finally { File.Delete(filePadre); File.Delete(fileFiglio); }
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("9284208188", "9284208188")]
|
||||
[InlineData("9284208188_D600", "9284208188")]
|
||||
[InlineData("9244228558_C03-01", "9244228558")]
|
||||
public void CodiceRadice_estrae_la_parte_prima_del_primo_underscore(string codice, string radiceAttesa) =>
|
||||
Assert.Equal(radiceAttesa, AssemblyComponentScanner.CodiceRadice(codice));
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
using Microsoft.Data.Sqlite;
|
||||
using PrepStep;
|
||||
|
||||
namespace PrepStep.Tests;
|
||||
|
||||
/// <summary>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).</summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>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.</summary>
|
||||
private sealed class ProgressoSincrono : IProgress<ScanProgress>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,11 @@ namespace PrepStep;
|
||||
/// silenzio come "nudo senza placement" - lì verrà comunque scartato come "non in indice",
|
||||
/// ma in modo tracciabile.
|
||||
/// </summary>
|
||||
static class AssemblyComponentScanner
|
||||
// Pubblica (non solo internal) perché è anche l'unica fonte di verità per l'euristica di
|
||||
// estrazione, referenziata via "#:project" dagli script standalone (scripts/BomViaDb.cs,
|
||||
// scripts/ExtractCreoStrings.cs) invece che duplicata al loro interno - vedi
|
||||
// docs/formato-asm.md, sezione sulla trappola del "codice duplicato in tre punti".
|
||||
public static class AssemblyComponentScanner
|
||||
{
|
||||
private static readonly Regex CodiceNumericoRegex = new(
|
||||
@"(?<!ModelView#)\b\d{8,12}[A-Za-z]{0,2}(?:_[A-Za-z]?\d{2,4})?\b",
|
||||
|
||||
@@ -281,6 +281,82 @@ per `data_modifica DESC` e tenuta solo la riga più recente per tipo, altrimenti
|
||||
si rischia di risolvere un codice contro una copia diversa (e potenzialmente
|
||||
diversa nel contenuto) da quella realmente referenziata.
|
||||
|
||||
### Trappola nella BFS: un fallimento di placement non è definitivo
|
||||
|
||||
Un codice può comparire come riferimento fantasma (senza vero placement, vedi
|
||||
sopra) in un file scansionato **prima** di quello in cui invece ha un vero
|
||||
placement — tipicamente perché lo stesso codice è mostrato anche nel file
|
||||
dell'assieme padre come promemoria/metadato, oltre che nel file del proprio
|
||||
assieme dove è effettivamente posizionato. Osservato su un caso reale
|
||||
(assieme `9244228558.asm`): il componente `9244236104` compare nel file
|
||||
dell'assieme generale come riferimento nudo senza placement (viene scartato
|
||||
lì, correttamente), ma ha un vero `Component id 45 (9244236104.PRT)` dentro
|
||||
`9244228558_c01.asm`, il sotto-assieme che lo contiene realmente — scansionato
|
||||
solo _dopo_ il file padre nella BFS.
|
||||
|
||||
Una BFS che marca un candidato come "già visto" al primo incontro, anche
|
||||
quando quell'incontro è un fallimento di placement, blocca per sempre ogni
|
||||
ri-verifica successiva: il componente resta perso anche quando il file giusto
|
||||
viene scansionato più avanti. Nei casi più gravi il codice bloccato è esso
|
||||
stesso un **sotto-assieme** (osservato con `9256510047` dentro
|
||||
`9256518042.asm`): se il suo codice viene scartato prematuramente, il suo file
|
||||
non viene nemmeno accodato, e si perde l'intero sotto-albero sottostante
|
||||
(compresi `9056510047`/`9056510047_C01`/`9056510047_C02` in questo caso).
|
||||
|
||||
`scripts/BomViaDb.cs` (versione iniziale) aveva esattamente questo bug: un set
|
||||
`codiciVisti` unico bloccava sia i candidati confermati sia quelli scartati
|
||||
per mancato placement. Corretto separando le due cose: un fallimento di
|
||||
placement non aggiunge il codice a nessun set di blocco, quindi resta
|
||||
ritestabile ogni volta che lo stesso candidato ricompare in un file successivo
|
||||
già in coda. L'accodamento di un nuovo file da scansionare resta invece
|
||||
condizionato al placement confermato (non al solo essere un codice noto nel
|
||||
DB) — senza questo secondo vincolo, un codice generico che per puro caso
|
||||
coincide con un codice noto (es. `"915"`, `"932"`) farebbe esplodere la
|
||||
scansione su file estranei in tutto l'indice (verificato: passa da ~100 a
|
||||
quasi 19000 "componenti trovati" e la scansione si blocca).
|
||||
|
||||
**Il codice della GUI (`Form1.RisolviAlberoComponenti` +
|
||||
`AssemblyComponentScanner.EstraiCodiciReferenziati`) non ha questo bug**,
|
||||
verificato riproducendo lo stesso caso reale con un test dedicato: la
|
||||
classificazione placement-sì/no è già interamente delegata a
|
||||
`EstraiCodiciReferenziati`, chiamata da zero per ogni file scansionato — un
|
||||
fallimento in un file non lascia traccia nel set `codiciVisti` del chiamante,
|
||||
che accumula solo codici già confermati. Il file successivo che referenzia
|
||||
davvero il componente lo ritrova quindi senza ostacoli, senza bisogno di
|
||||
nessuna modifica. Vale comunque come nota di design da preservare: il giorno
|
||||
in cui la logica di `RisolviAlberoComponenti` venisse riscritta per fare la
|
||||
verifica di placement inline nella BFS (come `BomViaDb.cs`), andrebbe
|
||||
applicata la stessa correzione.
|
||||
|
||||
### Seconda discrepanza trovata: l'eccezione dei codici con suffisso
|
||||
|
||||
Confrontando `scripts/BomViaDb.cs` (corretto come sopra) con la GUI sullo
|
||||
stesso assieme di un caso reale più grande (`9284218024b.asm`, ~530
|
||||
componenti), le due liste **non coincidevano**: lo script perdeva l'intero
|
||||
sotto-assieme `2546220101` (`_C01`..`_C04`, inclusi i cuscinetti
|
||||
`INAFAG_NA6901-XL_...` dentro `_C04`), presente invece nella BOM ufficiale
|
||||
Creo e trovato correttamente dalla GUI.
|
||||
|
||||
Causa: `2546220101_C01`..`_C04` hanno un vero placement nel proprio file
|
||||
(`2546220101.asm`), ma **senza il suffisso** - il testo riporta solo
|
||||
`Componente ID N (2546220101.PRT)`, il dettaglio `_C01` compare solo altrove
|
||||
nei metadati, non vicino al placement (stesso fenomeno già documentato sopra
|
||||
in "Placement senza il dettaglio del suffisso/bracket", osservato lì solo per
|
||||
famiglie UNI/dimensionali). `AssemblyComponentScanner.EstraiCodiciReferenziati`
|
||||
già gestisce questo caso: un codice con suffisso di revisione/variante
|
||||
(`SuffissoRegex`, es. `_C01`, `_D100`) o prefisso di family table nota (`UNI`,
|
||||
`CUSCI_`) è considerato "forma nota" ed esentato dal placement esplicito -
|
||||
basta che occorra almeno 2 volte nel file. `BomViaDb.cs` richiedeva invece
|
||||
sempre il placement letterale per ogni codice, senza questa eccezione.
|
||||
|
||||
Corretto portando la stessa regola (`FormaNota` + conteggio occorrenze) in
|
||||
`BomViaDb.cs`. Riverificato su entrambi i casi reali (`9244228558.asm`,
|
||||
`9284218024b.asm`): script e GUI producono ora **esattamente lo stesso
|
||||
insieme** di componenti, e in entrambi coincide con la BOM ufficiale Creo (a
|
||||
meno delle sole istanze di family table risolte al nome del file realmente
|
||||
indicizzato, es. `2502020105<CUSCI_DGBB7>` → `CUSCI_DGBB7` - comportamento
|
||||
corretto, non una discrepanza).
|
||||
|
||||
## Strumento di analisi manuale
|
||||
|
||||
[`scripts/Analyze.cs`](../scripts/Analyze.cs) stampa il contesto testuale ed
|
||||
|
||||
+24
-54
@@ -1,17 +1,16 @@
|
||||
#:project ../PrepStep/PrepStep.csproj
|
||||
#:property TargetFramework=net10.0-windows
|
||||
#:package Microsoft.Data.Sqlite@10.0.12
|
||||
// dotnet run scripts/BomViaDb.cs -- <file.asm> [cad_index.db] [output.txt]
|
||||
//
|
||||
// PROVA: a differenza di ExtractCreoStrings.cs (che restringe i candidati a un solo
|
||||
// pattern di codice, tarato sui codici numerici Pro-E), qui l'estrazione è generica -
|
||||
// qualunque run di lettere/cifre/underscore/trattino - e il DB decide se è un codice
|
||||
// vero, così si intercettano anche i nomi alfabetici delle parti commerciali scaricate
|
||||
// come STEP (es. "ANELLO_ESTERNO-1-SOLID1"). L'essere nel DB da solo non basta però:
|
||||
// codici corti o generici (es. "907", "NONE") comparirebbero per puro rumore binario,
|
||||
// quindi si richiede comunque il placement esplicito nel file, come in
|
||||
// ExtractCreoStrings.cs. Stessa espansione ricorsiva sui sotto-assiemi.
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
// Stessa risoluzione ricorsiva dell'albero componenti di Form1.RisolviAlberoComponenti
|
||||
// nella GUI, da riga di comando: usa direttamente PrepStep.AssemblyComponentScanner (via
|
||||
// "#:project", niente ProjectReference separato) invece di duplicare l'euristica di
|
||||
// estrazione/placement. Un'unica fonte di verità - niente rischio che script e GUI
|
||||
// divergano silenziosamente, come accaduto prima di questa riscrittura (vedi
|
||||
// docs/formato-asm.md).
|
||||
using Microsoft.Data.Sqlite;
|
||||
using PrepStep;
|
||||
|
||||
if (args.Length < 1)
|
||||
{
|
||||
@@ -23,20 +22,12 @@ string asmIniziale = args[0];
|
||||
string dbPath = args.Length > 1 ? args[1] : "cad_index.db";
|
||||
string outPath = args.Length > 2 ? args[2] : "BOM.txt";
|
||||
|
||||
// Oltre il 90% dei codici in indice sono numerici Pro-E, ma una minoranza (parti
|
||||
// commerciali scaricate come STEP, es. "ANELLO_ESTERNO-1-SOLID1") ha nomi alfabetici
|
||||
// con trattini/underscore. Niente forma di codice ipotizzata a mano: si tokenizza in
|
||||
// modo generico (run di lettere/cifre/underscore/trattino) e si lascia al DB decidere
|
||||
// cos'è un codice vero - stesso principio già usato per il filtro, esteso anche
|
||||
// all'estrazione dei candidati. Cattura anche "UNI6592"/"CUSCI_ACBB7" dentro
|
||||
// "<UNI6592>" gratis: '<' e '>' non fanno parte del charset e delimitano il token.
|
||||
var codiceRegex = new Regex(@"[A-Za-z0-9_-]{3,}", RegexOptions.Compiled);
|
||||
|
||||
using var connection = new SqliteConnection($"Data Source={dbPath};Mode=ReadOnly");
|
||||
connection.Open();
|
||||
|
||||
// Tutti i codici prt/asm noti all'indice, caricati una volta sola: il lookup per ogni
|
||||
// candidato estratto dal binario diventa O(1) invece di una query per file.
|
||||
// candidato estratto dal binario diventa O(1) invece di una query per file, e serve anche
|
||||
// a EstraiCodiciReferenziati per validare i token generici delle parti commerciali.
|
||||
var codiciNoti = new Dictionary<string, List<(string Tipo, string Percorso)>>(StringComparer.OrdinalIgnoreCase);
|
||||
using (var command = connection.CreateCommand())
|
||||
{
|
||||
@@ -54,51 +45,31 @@ using (var command = connection.CreateCommand())
|
||||
}
|
||||
Console.WriteLine($"Codici prt/asm noti nell'indice: {codiciNoti.Count}");
|
||||
|
||||
// Stesso controllo di placement di ExtractCreoStrings.cs/AssemblyComponentScanner.cs:
|
||||
// la sola presenza del codice nel DB non basta, perché codici corti o generici
|
||||
// (es. "907", "NONE", "start_part") possono comparire per puro rumore binario. Un
|
||||
// componente vero è sempre referenziato in una di queste forme (vedi
|
||||
// docs/formato-asm.md): "Componente ID N (<codice>...)"/"Component id N (<codice>...)",
|
||||
// il codice ripetuto due volte separato da un byte NUL, il campo
|
||||
// "family_name\0<codice>\0", o il codice seguito da un bracket "<codice><GENERICO>".
|
||||
bool HaPlacement(string testo, string codice) =>
|
||||
Regex.IsMatch(testo, $@"Component(?:e)? id \d+ \([^()]*{Regex.Escape(codice)}[^()]*\)", RegexOptions.IgnoreCase)
|
||||
|| testo.Contains(codice + "\0" + codice + "\0")
|
||||
|| testo.Contains("family_name\0" + codice + "\0")
|
||||
|| testo.Contains(codice + "<");
|
||||
|
||||
var codiceProprio = Path.GetFileName(asmIniziale).Split('.')[0];
|
||||
// Stessa esclusione di ExtractCreoStrings.cs: un assieme "_D..." referenzia sempre al
|
||||
// suo interno anche l'assieme generale a cui appartiene (riferimento fantasma verso
|
||||
// l'alto), da escludere subito o si risale tutto l'albero del macchinario.
|
||||
var codiceRadice = codiceProprio.Split('_')[0];
|
||||
var codiceRadice = AssemblyComponentScanner.CodiceRadice(codiceProprio);
|
||||
|
||||
var componentiTrovati = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var scartatiNonInIndice = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var scartatiSenzaPlacement = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var nonTrovati = new List<string>();
|
||||
var codiciVisti = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { codiceProprio, codiceRadice };
|
||||
var daScansionare = new Queue<string>(new[] { asmIniziale });
|
||||
|
||||
while (daScansionare.Count > 0)
|
||||
{
|
||||
var asm = daScansionare.Dequeue();
|
||||
var testo = Encoding.Latin1.GetString(File.ReadAllBytes(asm));
|
||||
var candidati = codiceRegex.Matches(testo).Select(m => m.Value).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var candidato in candidati)
|
||||
foreach (var codice in AssemblyComponentScanner.EstraiCodiciReferenziati(asm, codiciNoti.Keys))
|
||||
{
|
||||
if (!codiciVisti.Add(candidato))
|
||||
// Solo i codici già confermati da EstraiCodiciReferenziati arrivano qui: nessun
|
||||
// rischio del bug "primo fallimento blocca per sempre" (vedi docs/formato-asm.md),
|
||||
// perché il placement è già stato verificato per questo file specifico al suo
|
||||
// interno, da zero, indipendentemente da cosa è successo negli altri file.
|
||||
if (!codiciVisti.Add(codice))
|
||||
continue;
|
||||
if (!codiciNoti.TryGetValue(candidato, out var componenti))
|
||||
if (!codiciNoti.TryGetValue(codice, out var componenti))
|
||||
{
|
||||
scartatiNonInIndice.Add(candidato);
|
||||
nonTrovati.Add(codice);
|
||||
continue;
|
||||
}
|
||||
if (!HaPlacement(testo, candidato))
|
||||
{
|
||||
scartatiSenzaPlacement.Add(candidato);
|
||||
continue;
|
||||
}
|
||||
componentiTrovati.Add(candidato);
|
||||
componentiTrovati.Add(codice);
|
||||
foreach (var (tipo, percorso) in componenti)
|
||||
if (tipo == "asm")
|
||||
daScansionare.Enqueue(percorso);
|
||||
@@ -107,7 +78,6 @@ while (daScansionare.Count > 0)
|
||||
|
||||
File.WriteAllLines(outPath, componentiTrovati);
|
||||
Console.WriteLine($"{componentiTrovati.Count} componenti scritti in {outPath}");
|
||||
Console.WriteLine($"Candidati nell'indice ma senza placement (scartati, {scartatiSenzaPlacement.Count}): {string.Join(", ", scartatiSenzaPlacement.Take(30))}"
|
||||
+ (scartatiSenzaPlacement.Count > 30 ? ", ..." : ""));
|
||||
Console.WriteLine($"Candidati non nell'indice (rumore binario): {scartatiNonInIndice.Count}");
|
||||
Console.WriteLine($"Candidati non nell'indice (scartati, {nonTrovati.Count}): {string.Join(", ", nonTrovati.Take(30))}"
|
||||
+ (nonTrovati.Count > 30 ? ", ..." : ""));
|
||||
return 0;
|
||||
+22
-110
@@ -1,15 +1,15 @@
|
||||
#:project ../PrepStep/PrepStep.csproj
|
||||
#:property TargetFramework=net10.0-windows
|
||||
#:package Microsoft.Data.Sqlite@10.0.12
|
||||
// dotnet run scripts/ExtractCreoStrings.cs -- <file.asm> [cad_index.db] [output.txt]
|
||||
//
|
||||
// Estrae dal file .asm i codici dei componenti referenziati, espandendo ricorsivamente
|
||||
// ogni sotto-assieme (.asm) trovato nell'indice, finché non restano solo PRT (foglie) o
|
||||
// codici non presenti nell'indice. Stessa euristica di PrepStep/AssemblyComponentScanner.cs:
|
||||
// i codici veri compaiono più volte nel file, il rumore binario quasi sempre una sola volta;
|
||||
// gli ID interni "ModelView#<numero>" sono esclusi via lookbehind - vedi scripts/Analyze.cs.
|
||||
// Ogni codice trovato viene validato contro l'indice cad_index.db.
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
// Come scripts/BomViaDb.cs, ma senza passare "codiciNoti" a EstraiCodiciReferenziati: si
|
||||
// riconoscono solo i codici numerici Pro-E (che si validano dalla forma, senza bisogno del
|
||||
// DB - vedi PrepStep/AssemblyComponentScanner.cs), non le parti commerciali con nome
|
||||
// alfabetico. Prova più semplice/mirata rispetto a BomViaDb.cs, utile quando serve solo
|
||||
// individuare/segnalare codici numerici mancanti dall'indice.
|
||||
using Microsoft.Data.Sqlite;
|
||||
using PrepStep;
|
||||
|
||||
if (args.Length < 1)
|
||||
{
|
||||
@@ -21,114 +21,29 @@ string asmIniziale = args[0];
|
||||
string dbPath = args.Length > 1 ? args[1] : "cad_index.db";
|
||||
string outPath = args.Length > 2 ? args[2] : "BOM.txt";
|
||||
|
||||
// Seconda alternativa: componenti normalizzati definiti come family table in Creo (viti/
|
||||
// dadi/rosette UNI, ma anche cuscinetti CUSCI_...). Compaiono come "Componente ID N
|
||||
// (R12--_-_U6592<UNI6592>.PRT)": "R12--_-_U6592" è il nome della singola istanza (riga
|
||||
// della family table, non un file separato su disco), il file reale indicizzato è il
|
||||
// generico "uni6592.prt.<versione>"/"cusci_acbb7.prt.<versione>" - va quindi estratto solo
|
||||
// il contenuto tra "<" e ">".
|
||||
// Terza alternativa: varianti dimensionali con suffisso puramente numerico (es. cinghie
|
||||
// "2702220182_8188", dove "8188" è una misura, non una lettera di revisione/sotto-assieme
|
||||
// come "_C01"/"_D100") - la lettera dopo "_" è quindi opzionale, non obbligatoria. La
|
||||
// lettera di revisione e il suffisso "_" possono anche comparire insieme sullo stesso
|
||||
// codice (es. "9284200366A_C01", componente di un sotto-assieme già di per sé revisionato).
|
||||
var codiceRegex = new Regex(
|
||||
@"(?<!ModelView#)\b\d{8,12}[A-Za-z]{0,2}(?:_[A-Za-z]?\d{2,4})?\b|(?<=<)(?:UNI\d+|CUSCI_[A-Za-z0-9]+)(?=>)",
|
||||
RegexOptions.Compiled);
|
||||
const int occorrenzeMinime = 2;
|
||||
|
||||
HashSet<string> EstraiCodiciReferenziati(string percorsoAsm)
|
||||
{
|
||||
var testo = Encoding.Latin1.GetString(File.ReadAllBytes(percorsoAsm));
|
||||
var occorrenze = new Dictionary<string, int>();
|
||||
foreach (Match m in codiceRegex.Matches(testo))
|
||||
occorrenze[m.Value] = occorrenze.GetValueOrDefault(m.Value) + 1;
|
||||
var candidati = occorrenze.Where(kv => kv.Value >= occorrenzeMinime).Select(kv => kv.Key).ToHashSet();
|
||||
|
||||
// Ripetersi >=2 volte nel file non basta per un codice "nudo" (senza suffisso "_..."):
|
||||
// riferimenti al proprio assieme padre (nome sessione/file) o a codici superati/
|
||||
// rinominati (caso "fantasma", docs/formato-asm.md) ripetono comunque il codice più
|
||||
// volte pur non essendo mai un componente vero. Per questi si richiede un vero
|
||||
// placement, in una delle quattro forme osservate nel binario: "Componente ID N
|
||||
// (<codice>...)"/"Component id N (<codice>...)" (la stringa cambia con la
|
||||
// localizzazione Creo del file); il codice ripetuto due volte di seguito separato da
|
||||
// un singolo byte NUL ("<codice>\0<codice>\0", osservato per componenti di family
|
||||
// table non-UNI, es. cuscinetti "CUSCI_..."); il campo "family_name\0<codice>\0"
|
||||
// (osservato una sola volta per file, su un sotto-assieme referenziato come family
|
||||
// table generica invece che con un placement testuale esplicito); oppure il codice
|
||||
// seguito immediatamente da un bracket "<codice><GENERICO>" - l'istanza di una family
|
||||
// table non-UNI vista nel nome (es. "2510060303<CUSCI_ACBB7>", un cuscinetto): il
|
||||
// codice dell'istanza non è mai indicizzato a sé (solo il generico tra "<" e ">" lo è,
|
||||
// vedi sotto), ma questo segnale lo fa comunque sopravvivere fino alla ricerca nel DB
|
||||
// invece di scomparire in silenzio come "nudo senza placement" - lì verrà comunque
|
||||
// scartato come "non in indice", ma in modo tracciabile. Non si applica alle varianti
|
||||
// con suffisso "_..." né alle istanze family table UNI: in alcuni file il placement
|
||||
// riporta solo il nome istanza/generico senza il dettaglio del suffisso o del bracket
|
||||
// "<...>", che vive altrove nei metadati - lì l'occorrenza ripetuta resta l'unico
|
||||
// segnale disponibile.
|
||||
bool HaPlacement(string codice) =>
|
||||
Regex.IsMatch(testo, $@"Component(?:e)? id \d+ \([^()]*{Regex.Escape(codice)}[^()]*\)", RegexOptions.IgnoreCase)
|
||||
|| testo.Contains(codice + "\0" + codice + "\0")
|
||||
|| testo.Contains("family_name\0" + codice + "\0")
|
||||
|| testo.Contains(codice + "<");
|
||||
|
||||
candidati.RemoveWhere(c => Regex.IsMatch(c, @"^\d{8,12}[A-Za-z]{0,2}$") && !HaPlacement(c));
|
||||
return candidati;
|
||||
}
|
||||
|
||||
using var connection = new SqliteConnection($"Data Source={dbPath};Mode=ReadOnly");
|
||||
connection.Open();
|
||||
|
||||
// "files_ultima_versione" è una VIEW con ROW_NUMBER() OVER (...), che SQLite non
|
||||
// materializza: ogni query la ricalcola sull'intera tabella "files" (~230ms anche per un
|
||||
// solo codice, su ~300k righe). Batchare tutti i codici trovati in un file in un'unica
|
||||
// query paga quel costo una volta per file invece che una volta per codice - decisivo
|
||||
// sugli assiemi con centinaia di sotto-componenti referenziati.
|
||||
Dictionary<string, List<(string Tipo, string Percorso)>> CercaComponenti(IReadOnlyCollection<string> codici)
|
||||
var codiciNoti = new Dictionary<string, List<(string Tipo, string Percorso)>>(StringComparer.OrdinalIgnoreCase);
|
||||
using (var command = connection.CreateCommand())
|
||||
{
|
||||
var risultato = new Dictionary<string, List<(string, string)>>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var codice in codici)
|
||||
risultato[codice] = new List<(string, string)>();
|
||||
if (codici.Count == 0)
|
||||
return risultato;
|
||||
|
||||
using var command = connection.CreateCommand();
|
||||
var nomiParametri = codici.Select((_, i) => $"@c{i}").ToList();
|
||||
// Lo stesso codice+tipo può esistere in più cartelle radice (duplicati/copie storiche
|
||||
// sulla share, es. un generico family table ripetuto in 5 progetti scollegati): ordinato
|
||||
// per data_modifica DESC, si tiene solo il più recente per codice+tipo (stessa regola di
|
||||
// Form1.RisolviAlberoComponenti, vedi docs/formato-asm.md).
|
||||
command.CommandText = "SELECT codice, tipo_file, percorso_completo FROM files_ultima_versione "
|
||||
+ $"WHERE LOWER(codice) IN ({string.Join(",", nomiParametri)}) AND LOWER(tipo_file) IN ('prt', 'asm') "
|
||||
+ "ORDER BY codice, tipo_file, data_modifica DESC";
|
||||
int i = 0;
|
||||
foreach (var codice in codici)
|
||||
command.Parameters.AddWithValue(nomiParametri[i++], codice.ToLowerInvariant());
|
||||
|
||||
var tipiVisti = new Dictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase);
|
||||
+ "WHERE LOWER(tipo_file) IN ('prt', 'asm') ORDER BY codice, tipo_file, data_modifica DESC";
|
||||
using var reader = command.ExecuteReader();
|
||||
while (reader.Read())
|
||||
{
|
||||
string codiceDb = reader.GetString(0);
|
||||
string codice = reader.GetString(0);
|
||||
string tipo = reader.GetString(1).ToLowerInvariant();
|
||||
var visti = tipiVisti.TryGetValue(codiceDb, out var v) ? v : tipiVisti[codiceDb] = new HashSet<string>();
|
||||
if (visti.Add(tipo))
|
||||
risultato[codiceDb].Add((tipo, reader.GetString(2)));
|
||||
var lista = codiciNoti.TryGetValue(codice, out var l) ? l : codiciNoti[codice] = new();
|
||||
if (!lista.Any(x => x.Tipo == tipo))
|
||||
lista.Add((tipo, reader.GetString(2)));
|
||||
}
|
||||
return risultato;
|
||||
}
|
||||
Console.WriteLine($"Codici prt/asm noti nell'indice: {codiciNoti.Count}");
|
||||
|
||||
// Espansione ricorsiva: ogni sotto-assieme (.asm) trovato viene a sua volta letto e i suoi
|
||||
// componenti aggiunti alla coda, finché non restano solo PRT (foglie) o codici non presenti
|
||||
// nell'indice - stessa logica di Form1.RisolviAlberoComponenti.
|
||||
var codiceProprio = Path.GetFileName(asmIniziale).Split('.')[0];
|
||||
// Un assieme con suffisso "_D..." referenzia sempre al suo interno anche l'assieme
|
||||
// generale (codice nudo, senza suffisso) a cui appartiene - riferimento fantasma verso
|
||||
// l'alto, non un componente vero (vedi docs/formato-asm.md). Va escluso subito: se
|
||||
// seguito, riporta dentro l'intero albero del macchinario invece del solo sotto-assieme
|
||||
// richiesto (visto su 9284208188_D600 -> 9284208188, che da solo genera 598 falsi
|
||||
// positivi invece di ~90 componenti reali).
|
||||
var codiceRadice = codiceProprio.Split('_')[0];
|
||||
var codiceRadice = AssemblyComponentScanner.CodiceRadice(codiceProprio);
|
||||
|
||||
var componentiTrovati = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var nonTrovati = new List<string>();
|
||||
var codiciVisti = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { codiceProprio, codiceRadice };
|
||||
@@ -137,14 +52,11 @@ var daScansionare = new Queue<string>(new[] { asmIniziale });
|
||||
while (daScansionare.Count > 0)
|
||||
{
|
||||
var asm = daScansionare.Dequeue();
|
||||
var candidati = EstraiCodiciReferenziati(asm)
|
||||
.Where(codiciVisti.Add)
|
||||
.ToList();
|
||||
var trovatiPerCodice = CercaComponenti(candidati);
|
||||
foreach (var codice in candidati)
|
||||
foreach (var codice in AssemblyComponentScanner.EstraiCodiciReferenziati(asm))
|
||||
{
|
||||
var componenti = trovatiPerCodice[codice];
|
||||
if (componenti.Count == 0)
|
||||
if (!codiciVisti.Add(codice))
|
||||
continue;
|
||||
if (!codiciNoti.TryGetValue(codice, out var componenti))
|
||||
{
|
||||
nonTrovati.Add(codice);
|
||||
continue;
|
||||
|
||||
Reference in new issue
Block a user