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 \0 \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 \xe0\x0a name\0 \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 \xe0\x0a model_name\0 \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 \xf3, ogni elemento // \xe3\xe2\0 - vedi docs/formato-asm-record.md §7.5 private static byte[] Riferimenti(params string[] nomi) { var parti = new List { 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(), "\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" 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 "" (stessa notazione di §4.1). Senza lo split, il codice // cercato nell'indice sarebbe "V6-12--_-_U5931_GC" per intero, mai trovato - // va cercato invece il generico "UNI5931". byte[] dati = Asm(1, Membro(1, 2, "V6-12--_-_U5931_GC")); 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(() => 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(() => 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"))); 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(() => 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(() => AsmRecordReader.EstraiCodiciReferenziati(@"C:\non\esiste\pippo.asm.1")); }