Terzo caso di "[-4] Dati di input non completi" in Creo Distributed Batch, dopo i soppressi e le copie ambigue del commit precedente: falliva 9255728001, e solo lui fra i sei assiemi del batch. Il file in raw/ era identico bit per bit a quello sulla share, quindi non era il modello: mancava il contorno. Il suo sottoassieme _200 e' legato ad altri due (9255708017, 9255708018) da assembly feature e datum copiati, e monta una vite V6-12--_-_U5933_C il cui generico uni5933.prt non era in raw/. Nessuno dei sei file compariva in "members" ne' fra i piazzamenti: i due lettori esistenti non potevano vederli. Quei modelli stanno in una tabella a parte del .asm - array \xf2\xf7\x05 … \xf3 con elementi \xe3\xe2<tipo><nome>\0 - che ora si legge con EstraiCodiciRiferimentiEsterni, riusando la normalizzazione di #AF<n> e istanza<generico> gia' fatta per i membri. A differenza dei soppressi si ricorre dentro: Creo carica il modello citato con tutto il suo albero, e infatti 9255700182 arriva solo per quella via. La tabella contiene anche nomi di feature (MIRROR3, CHAMFER1) che dal record non si distinguono dai modelli. Non e' un problema: come per i soppressi si cerca ogni nome nell'indice e si ignora quello che non c'e'. Per lo stesso motivo questi codici restano fuori dai "non trovati", e la coda di scansione porta ora un flag che tiene fuori anche i rami raggiunti per riferimento esterno - la lista dei componenti mancanti continua a parlare solo della distinta. Documentato in docs/formato-asm-record.md §7.5. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
294 lines
12 KiB
C#
294 lines
12 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);
|
|
|
|
// \0objname\0 \xe2 \xe0\x01 object_type\0 <int> \xe0\x0a name\0 <nome>\0 - il nome proprio
|
|
// del modello (quello che la BOM intesta), vedi docs/formato-asm-record.md §5
|
|
private static byte[] ObjName(string nome) =>
|
|
Bytes(0, "objname", 0, 0xe2, 0xe0, 0x01, "object_type", 0, 2, 0xe0, 0x0a, "name", 0, nome, 0);
|
|
|
|
// identita' dichiarata dal file: \xe0\x01 model_t_type\0 <int> \xe0\x0a model_name\0 <nome>\0
|
|
// \xe0\x0a generic_name\0 \0 (vuoto)
|
|
private static byte[] ModelName(string nome) =>
|
|
Bytes(0xe0, 0x01, "model_t_type", 0, 2, 0xe0, 0x0a, "model_name", 0, nome, 0,
|
|
0xe0, 0x0a, "generic_name", 0, 0);
|
|
|
|
// array dei modelli referenziati: \xf2\xf7\x05 <elementi> \xf3, ogni elemento
|
|
// \xe3\xe2<tipo><nome>\0 - vedi docs/formato-asm-record.md §7.5
|
|
private static byte[] Riferimenti(params string[] nomi)
|
|
{
|
|
var parti = new List<object> { 0xf2, 0xf7, 0x05 };
|
|
foreach (var nome in nomi)
|
|
parti.AddRange(new object[] { 0xe3, 0xe2, 0x02, nome, 0 });
|
|
parti.Add(0xf3);
|
|
return Bytes(parti.ToArray());
|
|
}
|
|
|
|
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 Membro_con_notazione_istanza_generico_diretta_nel_nome_usa_il_generico()
|
|
{
|
|
// Regressione: 9244248577a_c01.asm - alcuni membri di family table non-UNI non
|
|
// passano da ConfigName, il campo "name"/"family_name" del record members porta gia'
|
|
// la forma "<istanza><generico>" (stessa notazione di §4.1). Senza lo split, il codice
|
|
// cercato nell'indice sarebbe "V6-12--_-_U5931_GC<UNI5931>" per intero, mai trovato -
|
|
// va cercato invece il generico "UNI5931".
|
|
byte[] dati = Asm(1, Membro(1, 2, "V6-12--_-_U5931_GC<UNI5931>"));
|
|
|
|
var membri = AsmRecordReader.LeggiMembri(dati);
|
|
|
|
Assert.Single(membri);
|
|
Assert.Equal("V6-12--_-_U5931_GC", membri[0].Nome);
|
|
Assert.Equal("UNI5931", membri[0].Generico);
|
|
}
|
|
|
|
[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: un file che e' esso stesso un'istanza di family table (il suo nome
|
|
// proprio "2546220101" piu' il suffisso "_C01" in model_name) descrive in "members"
|
|
// solo quella rappresentazione, non le istanze sorelle. Va segnalato invece di
|
|
// restituire silenziosamente il contenuto sbagliato (docs/formato-asm-record.md §7).
|
|
byte[] dati = Asm(1, Membro(1, 2, "9244206098"),
|
|
extra: Bytes(ObjName("2546220101"), ModelName("2546220101_C01")));
|
|
|
|
Assert.Throws<InvalidDataException>(() => AsmRecordReader.LeggiMembri(dati));
|
|
}
|
|
|
|
[Fact]
|
|
public void Model_name_di_un_altro_modello_non_lancia_eccezione()
|
|
{
|
|
// Regressione: sui file reali della share il record "model_name" nomina spesso un
|
|
// modello diverso dal file (tipicamente un suo membro), non l'identita' del file. Il
|
|
// nome proprio (objname) non conferma, quindi non e' il sintomo della family table:
|
|
// "members" e' corretto e va usato. Verificato contro BOM Creo reali - senza questo
|
|
// controllo il file cadeva sull'euristica testuale, che produceva codici inesistenti
|
|
// segnalati poi come "componenti mancanti".
|
|
byte[] dati = Asm(1, Membro(1, 2, "9044207999A_C01"),
|
|
extra: Bytes(ObjName("9244207999A"), ModelName("9044207999A_C01")));
|
|
|
|
var membri = AsmRecordReader.LeggiMembri(dati);
|
|
|
|
Assert.Equal("9044207999A_C01", membri.Single().Nome);
|
|
}
|
|
|
|
[Fact]
|
|
public void Model_name_senza_nome_proprio_non_lancia_eccezione()
|
|
{
|
|
// Stesso caso del test sopra ma senza record "objname" (formato osservato su tutti i
|
|
// file reali che facevano scattare il falso positivo): senza conferma del nome proprio
|
|
// il record non e' un'identita', quindi "members" va letto normalmente.
|
|
byte[] dati = Asm(1, Membro(1, 2, "9244206098"), extra: ModelName("2546220101_C01"));
|
|
|
|
Assert.Single(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(ObjName("9244230117"), ModelName("9244230117")));
|
|
|
|
var membri = AsmRecordReader.LeggiMembri(dati);
|
|
|
|
Assert.Single(membri);
|
|
}
|
|
|
|
[Fact]
|
|
public void Componente_soppresso_non_e_fra_i_membri_ma_e_segnalato_a_parte()
|
|
{
|
|
// Un componente soppresso sparisce da "members" (il conteggio scende) ma resta citato
|
|
// dalla feature che lo posiziona. Non va nella BOM, però senza il suo file Creo non
|
|
// apre l'assieme, quindi va copiato lo stesso.
|
|
byte[] dati = Asm(1, Membro(1, 2, "9244206098"),
|
|
extra: Bytes("Componente ID 52 (9230100043.PRT)", 0));
|
|
|
|
Assert.Equal(new[] { "9244206098" }, AsmRecordReader.LeggiMembri(dati).Select(m => m.Nome));
|
|
|
|
string percorso = ScriviFile(dati);
|
|
try
|
|
{
|
|
Assert.Equal(new[] { "9230100043" }, AsmRecordReader.EstraiCodiciSoppressi(percorso));
|
|
Assert.DoesNotContain("9230100043", AsmRecordReader.EstraiCodiciReferenziati(percorso));
|
|
}
|
|
finally { File.Delete(percorso); }
|
|
}
|
|
|
|
[Fact]
|
|
public void Componente_gia_fra_i_membri_non_e_segnalato_come_soppresso()
|
|
{
|
|
byte[] dati = Asm(1, Membro(1, 2, "9244206098"),
|
|
extra: Bytes("Componente ID 7 (9244206098.PRT)", 0));
|
|
string percorso = ScriviFile(dati);
|
|
try
|
|
{
|
|
Assert.Empty(AsmRecordReader.EstraiCodiciSoppressi(percorso));
|
|
}
|
|
finally { File.Delete(percorso); }
|
|
}
|
|
|
|
[Fact]
|
|
public void Riferimenti_esterni_non_sono_fra_i_membri_e_si_risolvono_sul_generico()
|
|
{
|
|
// Un modello legato da riferimento esterno (assembly feature "#AFn", datum copiati) non
|
|
// compare ne' in "members" ne' fra i piazzamenti, ma senza il suo file Creo non apre
|
|
// l'assieme: e' il caso che faceva fallire 9255728001_200 in Distributed Batch. Per le
|
|
// istanze di family table il file su disco e' il generico fra parentesi angolari.
|
|
byte[] dati = Asm(1, Membro(1, 2, "9244206098"),
|
|
extra: Bytes(Riferimenti("9255708017#AF2<9255708017>", "9255708018"),
|
|
Riferimenti("V6-12--_-_U5933_C<UNI5933>")));
|
|
|
|
Assert.Equal(new[] { "9244206098" }, AsmRecordReader.LeggiMembri(dati).Select(m => m.Nome));
|
|
|
|
string percorso = ScriviFile(dati);
|
|
try
|
|
{
|
|
Assert.Equal(new[] { "9255708017", "9255708018", "UNI5933" },
|
|
AsmRecordReader.EstraiCodiciRiferimentiEsterni(percorso).OrderBy(c => c, StringComparer.Ordinal));
|
|
}
|
|
finally { File.Delete(percorso); }
|
|
}
|
|
|
|
[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"));
|
|
}
|