AsmRecordReader.LeggiMembri ora lancia InvalidDataException quando un assieme e' esso stesso un'istanza di family table (members descriverebbe solo una rappresentazione, non le istanze sorelle) invece di restituire un risultato sbagliato in silenzio - il chiamante ripiega sull'euristica testuale, che risolve gia' correttamente il caso. Aggiunti due controlli di coerenza analoghi (conteggio grezzo vs interpretato dei record ConfigName; occorrenze multiple di "members"), verificati senza falsi positivi su tutti i 350 .asm reali della share. Documentazione riorganizzata per distinguere cio' che e' verificato contro una BOM Creo di riferimento da cio' che e' verificato solo su scala (conteggi), e cio' che resta genuinamente non risolto. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
176 lines
7.0 KiB
C#
176 lines
7.0 KiB
C#
using System.Text;
|
|
using PrepStep;
|
|
|
|
namespace PrepStep.Tests;
|
|
|
|
public class AsmRecordReaderTests
|
|
{
|
|
private static string ScriviFile(byte[] dati)
|
|
{
|
|
string percorso = Path.GetTempFileName();
|
|
File.WriteAllBytes(percorso, dati);
|
|
return percorso;
|
|
}
|
|
|
|
private static byte[] Bytes(params object[] parti)
|
|
{
|
|
using var ms = new MemoryStream();
|
|
foreach (var p in parti)
|
|
{
|
|
switch (p)
|
|
{
|
|
case byte b: ms.WriteByte(b); break;
|
|
case int i: ms.WriteByte((byte)i); break;
|
|
case string s: var buf = Encoding.Latin1.GetBytes(s); ms.Write(buf); break;
|
|
case byte[] arr: ms.Write(arr); break;
|
|
default: throw new ArgumentException($"tipo non gestito: {p.GetType()}");
|
|
}
|
|
}
|
|
return ms.ToArray();
|
|
}
|
|
|
|
// \xe3 id type name\0 family_name\0 - id/type come singolo byte (0x00-0x7f, un byte)
|
|
private static byte[] Membro(int id, int tipo, string nome, string? nomeFamiglia = null) =>
|
|
Bytes(0xe3, id, tipo, nome, 0, nomeFamiglia ?? nome, 0);
|
|
|
|
// \xe3 ConfigName\0 \xe23 <istanza>\0 <idMembro> \x82\x7b
|
|
private static byte[] ConfigName(string istanza, int idMembro) =>
|
|
Bytes(0xe3, "ConfigName", 0, 0xe2, (byte)'3', istanza, 0, idMembro, 0x82, 0x7b);
|
|
|
|
private static byte[] Asm(int conteggioDichiarato, byte[] membri, byte[]? extra = null) =>
|
|
Bytes("header ", extra ?? Array.Empty<byte>(), "\0members\0", 0xf1, 0xf8, conteggioDichiarato, membri, "#MdlStatus");
|
|
|
|
[Fact]
|
|
public void Legge_membri_semplici_senza_family_table()
|
|
{
|
|
byte[] dati = Asm(2, Bytes(
|
|
Membro(1, 2, "9244206098"),
|
|
Membro(2, 1, "9244228558_C01")));
|
|
|
|
var membri = AsmRecordReader.LeggiMembri(dati);
|
|
|
|
Assert.Equal(2, membri.Count);
|
|
Assert.Equal((1, 2, "9244206098", "9244206098"), (membri[0].Id, membri[0].Tipo, membri[0].Nome, membri[0].Generico));
|
|
Assert.Equal((2, 1, "9244228558_C01", "9244228558_C01"), (membri[1].Id, membri[1].Tipo, membri[1].Nome, membri[1].Generico));
|
|
}
|
|
|
|
[Fact]
|
|
public void Risolve_il_nome_istanza_di_una_family_table_da_ConfigName()
|
|
{
|
|
// Il membro cita solo il generico (UNI6592); il nome che la BOM mostra
|
|
// (R8--_-_U6592) sta nel parametro ConfigName, collegato per id membro.
|
|
byte[] dati = Asm(1, Membro(1, 2, "UNI6592"), extra: ConfigName("R8--_-_U6592", idMembro: 1));
|
|
|
|
var membri = AsmRecordReader.LeggiMembri(dati);
|
|
|
|
Assert.Single(membri);
|
|
Assert.Equal("R8--_-_U6592", membri[0].Nome);
|
|
Assert.Equal("UNI6592", membri[0].Generico);
|
|
}
|
|
|
|
[Fact]
|
|
public void Rimuove_il_suffisso_array_feature_dal_nome_istanza()
|
|
{
|
|
// Regressione: caso reale 9244250136.asm -> ConfigName con suffisso "#AF<n>" per
|
|
// componenti creati da un pattern/array feature (id 45 e id 78, entrambi variante
|
|
// "_C03" con suffisso diverso "#AF0"/"#AF1"): non un'istanza di family table vera,
|
|
// la BOM di Creo mostra il codice pulito e unisce le occorrenze duplicate.
|
|
byte[] dati = Asm(2, Bytes(
|
|
Membro(45, 2, "9244250136_C03"),
|
|
Membro(78, 2, "9244250136_C03")),
|
|
extra: Bytes(
|
|
ConfigName("9244250136_C03#AF0", idMembro: 45),
|
|
ConfigName("9244250136_C03#AF1", idMembro: 78)));
|
|
|
|
var membri = AsmRecordReader.LeggiMembri(dati);
|
|
|
|
Assert.All(membri, m => Assert.Equal("9244250136_C03", m.Nome));
|
|
}
|
|
|
|
[Fact]
|
|
public void Componente_senza_ConfigName_non_e_toccato_da_istanze_di_altri_membri()
|
|
{
|
|
byte[] dati = Asm(2, Bytes(
|
|
Membro(1, 2, "UNI6592"),
|
|
Membro(2, 2, "9244206098")),
|
|
extra: ConfigName("R8--_-_U6592", idMembro: 1));
|
|
|
|
var membri = AsmRecordReader.LeggiMembri(dati);
|
|
|
|
Assert.Equal("R8--_-_U6592", membri.Single(m => m.Id == 1).Nome);
|
|
Assert.Equal("9244206098", membri.Single(m => m.Id == 2).Nome);
|
|
}
|
|
|
|
[Fact]
|
|
public void Conteggio_dichiarato_diverso_da_quello_letto_lancia_eccezione()
|
|
{
|
|
// Dichiara 2 membri ma ne fornisce solo 1: lettura disallineata, va segnalata invece
|
|
// di restituire un risultato parziale silenzioso.
|
|
byte[] dati = Asm(2, Membro(1, 2, "9244206098"));
|
|
|
|
Assert.Throws<InvalidDataException>(() => AsmRecordReader.LeggiMembri(dati));
|
|
}
|
|
|
|
[Fact]
|
|
public void Assieme_family_table_con_generic_name_lancia_eccezione()
|
|
{
|
|
// Regressione: 2546220101.asm (docs/formato-asm-record.md §7) e' esso stesso
|
|
// un'istanza di family table (model_name "2546220101_C01" + generic_name vuoto); il
|
|
// suo "members" descrive solo questa rappresentazione, non le 4 istanze reali della
|
|
// BOM Creo. Va segnalato invece di restituire silenziosamente il contenuto sbagliato.
|
|
byte[] dati = Asm(1, Membro(1, 2, "9244206098"),
|
|
extra: Bytes(0xe0, 0x01, "model_t_type", 0, 2,
|
|
0xe0, 0x0a, "model_name", 0, "2546220101_C01", 0,
|
|
0xe0, 0x0a, "generic_name", 0, 0));
|
|
|
|
Assert.Throws<InvalidDataException>(() => AsmRecordReader.LeggiMembri(dati));
|
|
}
|
|
|
|
[Fact]
|
|
public void Generic_name_senza_suffisso_istanza_non_lancia_eccezione()
|
|
{
|
|
// Regressione: su ~7% dei file reali della share compare un record "model_name" +
|
|
// "generic_name" (vuoto) che NON e' il sintomo della family table di assiemi - il
|
|
// valore non ha il suffisso "_C0n" delle istanze (es. e' un codice qualunque non
|
|
// correlato al nome del file). In quei casi "members" e' letto correttamente e va
|
|
// usato normalmente, non bloccato per un falso positivo.
|
|
byte[] dati = Asm(1, Membro(1, 2, "9244206098"),
|
|
extra: Bytes(0xe0, 0x01, "model_t_type", 0, 2,
|
|
0xe0, 0x0a, "model_name", 0, "9244230117", 0,
|
|
0xe0, 0x0a, "generic_name", 0, 0));
|
|
|
|
var membri = AsmRecordReader.LeggiMembri(dati);
|
|
|
|
Assert.Single(membri);
|
|
}
|
|
|
|
[Fact]
|
|
public void Sezione_members_assente_lancia_eccezione()
|
|
{
|
|
byte[] dati = Encoding.Latin1.GetBytes("un file qualunque senza la sezione attesa");
|
|
|
|
Assert.Throws<InvalidDataException>(() => AsmRecordReader.LeggiMembri(dati));
|
|
}
|
|
|
|
[Fact]
|
|
public void EstraiCodiciReferenziati_restituisce_i_generici_distinti()
|
|
{
|
|
byte[] dati = Asm(3, Bytes(
|
|
Membro(1, 2, "9144210327"),
|
|
Membro(2, 2, "9144210327"), // stesso codice montato due volte: un solo codice distinto
|
|
Membro(3, 1, "9244238563_D100")));
|
|
string percorso = ScriviFile(dati);
|
|
try
|
|
{
|
|
var codici = AsmRecordReader.EstraiCodiciReferenziati(percorso);
|
|
Assert.Equal(new[] { "9144210327", "9244238563_D100" }, codici.OrderBy(c => c));
|
|
}
|
|
finally { File.Delete(percorso); }
|
|
}
|
|
|
|
[Fact]
|
|
public void File_inesistente_lancia_eccezione_IO() =>
|
|
Assert.Throws<DirectoryNotFoundException>(() =>
|
|
AsmRecordReader.EstraiCodiciReferenziati(@"C:\non\esiste\pippo.asm.1"));
|
|
}
|