464 lines
21 KiB
C#
464 lines
21 KiB
C#
using AyCode.Core.Enums;
|
||
using AyCode.Core.Extensions;
|
||
using AyCode.Core.Loggers;
|
||
using AyCode.Core.Serializers.Binaries;
|
||
using AyCode.Core.Serializers.Jsons;
|
||
using AyCode.Core.Serializers.Toons;
|
||
using FruitBank.Common;
|
||
using FruitBank.Common.Dtos;
|
||
using FruitBank.Common.Entities;
|
||
using FruitBank.Common.Loggers;
|
||
using FruitBankHybrid.Shared.Services.SignalRs;
|
||
using Mango.Nop.Core.Entities;
|
||
using Newtonsoft.Json;
|
||
using Nop.Core.Domain.Catalog;
|
||
using Nop.Core.Domain.Common;
|
||
using Nop.Core.Domain.Orders;
|
||
using Nop.Core.Domain.Payments;
|
||
using System.Linq.Expressions;
|
||
using System.Runtime.InteropServices.JavaScript;
|
||
using System.Runtime.Serialization;
|
||
using System.Text;
|
||
|
||
namespace FruitBankHybrid.Shared.Tests;
|
||
|
||
//1. "Headered List" (A biztonságos táblázatosítás)
|
||
|
||
//Az LLM-eknek nem kell minden sorban megismételni a mezőneveket, ha a lista elején egyszer definiálod a sorrendet. Ez nem találgatás, hanem egy lokális "szerződés".
|
||
|
||
//Hagyományos (pazarló):
|
||
//Kódrészlet
|
||
|
||
//OrderItemDtos = [
|
||
// OrderItemDto { Id = 120, Quantity = 10, ProductName = "Áfonya" }
|
||
// OrderItemDto { Id = 121, Quantity = 5, ProductName = "Narancs" }
|
||
//]
|
||
|
||
//Optimalizált (pontos és tömör):
|
||
//Kódrészlet
|
||
|
||
//OrderItemDtos: OrderItemDto[] = [
|
||
// [ Id, Quantity, ProductName ]
|
||
// [ 120, 10, "Áfonya" ]
|
||
// [ 121, 5, "Narancs" ]
|
||
//]
|
||
|
||
// Miért jó ez? Az LLM a fejléc alapján (mint egy CSV-nél) rendeli hozzá az értékeket a típushoz. Mivel a típus (OrderItemDto) ott van a definícióban, a szemantikai kapcsolat nem vész el.
|
||
|
||
//2. Típus-öröklődés a listákban
|
||
|
||
//Ha a @types részben már leírtad, hogy az OrderItemDto.ProductDto mezője egy ProductDto típust vár, akkor a @data részben felesleges kiírni a típusnevet minden egyes elemnél.
|
||
|
||
//Példa:
|
||
//Kódrészlet
|
||
|
||
//// A 'ProductDto' elhagyható az objektum elől, mert a sémából tudja
|
||
//ProductDto = {
|
||
// Id = 1
|
||
// Name = "Áfonya..."
|
||
// GenericAttributes = [
|
||
// { Id = 99, Key = "NetWeight", Value = "178.3" }
|
||
// { Id = 100, Key = "GrossWeight", Value = "19" }
|
||
// ]
|
||
//}
|
||
|
||
//3. Alapértelmezett értékek elhagyása (Implicit Defaults)
|
||
|
||
//Ha egy mező értéke megegyezik a @types-ban definiált default-value-val, vagy null/0/false, akkor azt teljesen hagyd ki a @data részből.
|
||
|
||
// Szabály: Ami nincs ott, az az alapértelmezett.
|
||
|
||
// Token megtakarítás: A FruitBank példádban a GenericAttributes = <GenericAttributeDto[]> (count: 0) [] sorok rengeteg helyet foglalnak. Ha üres, egyszerűen ne küldd el a mezőt.
|
||
|
||
//4. String Table helyett: "Object Anchoring"
|
||
|
||
//használd az objektum-referenciákat (amit a @ProductDto:1 jelöléssel már el is kezdett a rendszered).
|
||
|
||
//Ha ugyanaz a Product szerepel 5 különböző rendelési tételnél, ne írd le ötször.
|
||
|
||
// Első alkalommal: ProductDto { ... }
|
||
|
||
// Minden további alkalommal: ProductDto = @ProductDto:1
|
||
|
||
//[ToonIgnore][ToonDataIgnore]
|
||
[ToonDescription(Purpose = "Container model for Shipping, Order")]
|
||
public class FullProcessModel
|
||
{
|
||
public List<Shipping> Shippings { get; set; }
|
||
public List<OrderDto> Orders { get; set; }
|
||
public List<StockTaking> StockTakings { get; set; }
|
||
}
|
||
|
||
[TestClass]
|
||
public sealed class ToonTests
|
||
{
|
||
private const int CustomerIdAasdDsserverCom = 6; //aasd@dsserver.com
|
||
|
||
private FruitBankSignalRClient _signalRClient = null!;
|
||
|
||
[TestInitialize]
|
||
public void TestInit()
|
||
{
|
||
if (!FruitBankConstClient.BaseUrl.Contains("localhost:")) throw new Exception("NEM LOCALHOST-ON TESZTELÜNK!");
|
||
|
||
_signalRClient = new FruitBankSignalRClient(new List<IAcLogWriterClientBase>
|
||
{
|
||
//new ConsoleLogWriter(AppType.TestUnit, LogLevel.Detail, nameof(FruitBankClientTests)),
|
||
new SignaRClientLogItemWriter(AppType.TestUnit, LogLevel.Detail, nameof(FruitBankClientTests))
|
||
});
|
||
}
|
||
|
||
#if DEBUG
|
||
[TestMethod]
|
||
public async Task GetAnalyzeStringInternCandidatesLog()
|
||
{
|
||
var orders = (await _signalRClient.GetAllOrderDtos())!.Where(x=>x.CreatedOnUtc > DateTime.UtcNow.AddDays(-70)).ToList();
|
||
|
||
var options = AcBinarySerializerOptions.WithoutReferenceHandling;
|
||
//options.SetReferenceHandlingUnsafe(ReferenceHandlingMode.OnlyId);
|
||
var analysisLog = AcBinarySerializer.GetAnalyzeStringInternCandidatesLog(orders, options);
|
||
|
||
Assert.IsNotNull(analysisLog);
|
||
Assert.IsGreaterThan(0, analysisLog.Length);
|
||
|
||
// Print results sorted by occurrence count
|
||
Console.WriteLine(analysisLog.ToString());
|
||
Console.WriteLine();
|
||
}
|
||
#endif
|
||
|
||
[TestMethod]
|
||
public async Task OrderDtoToToon()
|
||
{
|
||
var a = new FullProcessModel();
|
||
a.Orders = (await _signalRClient.GetAllOrderDtos())!.Where(x=>x.CreatedOnUtc > DateTime.UtcNow.AddDays(-70)).ToList();
|
||
a.Shippings = (await _signalRClient.GetShippings())!.Where(x=>x.Created > DateTime.UtcNow.AddDays(-70)).ToList();
|
||
|
||
var toon = AcToonSerializer.SerializeTypeMetadata<FullProcessModel>(FruitBankConstClient.DomainDescription);
|
||
|
||
Console.WriteLine(toon);
|
||
Assert.IsNotEmpty(toon);
|
||
// Note: @ref: only appears when the same object instance is referenced multiple times.
|
||
// Data from separate API calls typically don't share object instances.
|
||
}
|
||
|
||
[TestMethod]
|
||
public void GetMetaInfos()
|
||
{
|
||
var a = new FullProcessModel();
|
||
var toon = AcToonSerializer.SerializeTypeMetadata<FullProcessModel>(FruitBankConstClient.DomainDescription);
|
||
Console.WriteLine(toon);
|
||
}
|
||
|
||
|
||
[TestMethod]
|
||
public void ReferenceHandling_WithSharedReferences_ShouldOutputRefMarkers()
|
||
{
|
||
// Create a simple test container with shared references
|
||
var sharedProduct = new ProductDto { Id = 1, Name = "Shared Product" };
|
||
|
||
// Create a container that references the same ProductDto twice
|
||
var container = new TestContainerWithSharedRefs
|
||
{
|
||
Product1 = sharedProduct,
|
||
Product2 = sharedProduct // Same instance, should create @ref
|
||
};
|
||
|
||
var toon = AcToonSerializer.Serialize(container, FruitBankConstClient.DomainDescription, AcToonSerializerOptions.Default);
|
||
|
||
Console.WriteLine(toon);
|
||
Assert.IsNotEmpty(toon);
|
||
Assert.IsTrue(toon.Contains("@ref:"), "ReferenceHandling should detect shared Product instance");
|
||
}
|
||
|
||
[TestMethod]
|
||
public void ReferenceHandling_WithSharedIIdReferences_ShouldOutputRefMarkers()
|
||
{
|
||
// Create a simple test container with shared references
|
||
var sharedProduct = new ProductDto { Id = 1, Name = "Shared Product" };
|
||
var sharedProduct2 = new ProductDto { Id = 1, Name = "Shared Product" };
|
||
|
||
// Create a container that references the same ProductDto twice
|
||
var container = new TestContainerWithSharedRefs
|
||
{
|
||
Product1 = sharedProduct,
|
||
Product2 = sharedProduct2 // Same instance, should create @ref
|
||
};
|
||
|
||
var toon = AcToonSerializer.Serialize(container, FruitBankConstClient.DomainDescription, AcToonSerializerOptions.Default);
|
||
|
||
Console.WriteLine(toon);
|
||
Assert.IsNotEmpty(toon);
|
||
Assert.IsTrue(toon.Contains("@ref:"), "ReferenceHandling should detect shared Product instance");
|
||
}
|
||
|
||
[TestMethod]
|
||
public void ToonTypes_ShouldNotContainList1OrGenericTypeNames()
|
||
{
|
||
var toon = AcToonSerializer.SerializeTypeMetadata<OrderDto>();
|
||
StringAssert.DoesNotMatch(toon, new System.Text.RegularExpressions.Regex(@"List`?1"), "A @meta.types vagy @types szekcióban nem szerepelhet List`1 vagy generikus típusnév.");
|
||
}
|
||
|
||
[TestMethod]
|
||
public void ToonTypes_ShouldNotContainDuplicateTypeNames()
|
||
{
|
||
var toon = AcToonSerializer.SerializeTypeMetadata<OrderDto>();
|
||
var typesLine = toon.Split('\n').FirstOrDefault(x => x.TrimStart().StartsWith("types = ["));
|
||
Assert.IsNotNull(typesLine, "Nem található types lista a @meta szekcióban.");
|
||
var typeNames = typesLine.Substring(typesLine.IndexOf('[') + 1, typesLine.LastIndexOf(']') - typesLine.IndexOf('[') - 1)
|
||
.Split(',').Select(x => x.Trim(' ', '"')).Where(x => !string.IsNullOrWhiteSpace(x)).ToList();
|
||
var duplicates = typeNames.GroupBy(x => x).Where(g => g.Count() > 1).Select(g => g.Key).ToList();
|
||
Assert.IsTrue(duplicates.Count == 0, $"A types listában duplikált típusnév található: {string.Join(", ", duplicates)}");
|
||
}
|
||
|
||
[TestMethod]
|
||
public void ToonTypes_EachTypeShouldBeDefinedOnceInTypesSection()
|
||
{
|
||
var toon = AcToonSerializer.SerializeTypeMetadata<OrderDto>();
|
||
var typeDefLines = toon.Split('\n').Where(x => x.TrimEnd().EndsWith(": \"Object of type") || x.TrimEnd().EndsWith(": enum") || x.TrimEnd().EndsWith(": \"Object of type "));
|
||
var typeNames = typeDefLines.Select(x => x.Trim().Split(':')[0]).ToList();
|
||
var duplicates = typeNames.GroupBy(x => x).Where(g => g.Count() > 1).Select(g => g.Key).ToList();
|
||
Assert.IsTrue(duplicates.Count == 0, $"A @types szekcióban duplikált típusdefiníció található: {string.Join(", ", duplicates)}");
|
||
}
|
||
|
||
[TestMethod]
|
||
public void ToonTypes_PropertyTypesShouldNotReferenceList1()
|
||
{
|
||
var toon = AcToonSerializer.SerializeTypeMetadata<OrderDto>();
|
||
var lines = toon.Split('\n');
|
||
foreach (var line in lines)
|
||
{
|
||
if (line.Trim().EndsWith(": List`1"))
|
||
{
|
||
Assert.Fail($"Property List`1 típusra hivatkozik: {line.Trim()}");
|
||
}
|
||
}
|
||
}
|
||
|
||
[TestMethod]
|
||
public void ToonTypes_PropertyDescriptions_ShouldNotBeRedundantOrMisleading()
|
||
{
|
||
var toon = AcToonSerializer.SerializeTypeMetadata<OrderDto>(FruitBankConstClient.DomainDescription);
|
||
var lines = toon.Split('\n');
|
||
foreach (var line in lines)
|
||
{
|
||
if (line.Trim().StartsWith("description:") && line.Contains("Collection of Object for"))
|
||
{
|
||
Assert.Fail($"Redundáns vagy félrevezető description: {line.Trim()}");
|
||
}
|
||
}
|
||
}
|
||
|
||
[TestMethod]
|
||
public void ToonTypes_NavigationMetadata_ShouldBeComplete()
|
||
{
|
||
var toon = AcToonSerializer.SerializeMetadata(FruitBankConstClient.DomainDescription, [typeof(Shipping), typeof(OrderDto), typeof(StockTaking), typeof(StockQuantityHistory), typeof(StockQuantityHistoryExt)]);
|
||
Console.WriteLine(toon);
|
||
Console.WriteLine("\n=== NAVIGATION METADATA ELLENŐRZÉS ===\n");
|
||
|
||
var lines = toon.Split('\n').Select(x => x.TrimEnd()).ToList();
|
||
|
||
// Ismerten egyirányú kapcsolatok - ezeknél nincs inverse property a másik oldalon
|
||
// Customer: NopCommerce domain entity, nincs benne Orders kollekció
|
||
// OrderNotes: OrderNote osztályban nincs Order navigation property
|
||
// ProductDto: nincs benne OrderItems kollekció
|
||
// GenericAttributes: polimorf kapcsolat ExpressionPredicate-tel, nincs inverse ÉS nincs egyértelmű FK
|
||
// FONTOS: Csak az inverse-property hiányát engedjük! Az other-key-nek léteznie kell!
|
||
var knownUnidirectionalNavigations = new HashSet<string>
|
||
{
|
||
"Customer",
|
||
"OrderNotes",
|
||
"ProductDto",
|
||
"GenericAttributes",
|
||
"ShippingDocumentFile",
|
||
"Pallet"
|
||
};
|
||
|
||
// GenericAttributes speciális eset - polimorf, nincs other-key sem
|
||
var knownPolymorphicNavigations = new HashSet<string>
|
||
{
|
||
"GenericAttributes"
|
||
};
|
||
|
||
// FK property-k NEM tartalmazhatnak foreign-key attribútumot
|
||
for (int i = 0; i < lines.Count; i++)
|
||
{
|
||
var line = lines[i].Trim();
|
||
if (line.EndsWith("Id: int") || line.EndsWith("Id: int?"))
|
||
{
|
||
int j = i + 1;
|
||
while (j < lines.Count)
|
||
{
|
||
if (lines[j].StartsWith(" ") && !lines[j].StartsWith(" "))
|
||
break;
|
||
|
||
if (lines[j].StartsWith(" "))
|
||
{
|
||
var metaLine = lines[j].Trim();
|
||
if (metaLine.StartsWith("foreign-key:"))
|
||
{
|
||
Assert.Fail($"FK property nem tartalmazhat foreign-key attribútumot: {line} -> {metaLine}");
|
||
}
|
||
}
|
||
j++;
|
||
}
|
||
}
|
||
}
|
||
|
||
Console.WriteLine("✓ FK property-k nem tartalmaznak foreign-key attribútumot\n");
|
||
|
||
// Számoljuk meg a hiányzó navigation metadatokat
|
||
var missingMetadata = new List<string>();
|
||
var skippedUnidirectional = new List<string>();
|
||
|
||
for (int i = 0; i < lines.Count; i++)
|
||
{
|
||
var line = lines[i].Trim();
|
||
|
||
// Navigation property-k keresése
|
||
if (line.Contains(": ") &&
|
||
!line.Contains(": int") && !line.Contains(": string") &&
|
||
!line.Contains(": DateTime") && !line.Contains(": decimal") &&
|
||
!line.Contains(": bool") && !line.Contains(": Guid") &&
|
||
!line.Contains(": double") && !line.Contains(": float") &&
|
||
!line.Contains("description:") && !line.Contains("purpose:") &&
|
||
!line.Contains("navigation:") && !line.Contains("foreign-key:") &&
|
||
!line.Contains("table-name:") && !line.Contains("constraints:") &&
|
||
!line.Contains("inverse-property:") && !line.Contains("other-key:") &&
|
||
!line.Contains("primary-key:") && !line.Contains("examples:") &&
|
||
!line.Contains("@meta") && !line.Contains("@types") &&
|
||
!line.Contains("version") && !line.Contains("format") && !line.Contains("source-code-language") &&
|
||
!line.Contains("underlying-type:") && !line.Contains("default-value:") && !line.Contains("values:"))
|
||
{
|
||
var propName = line.Split(':')[0].Trim();
|
||
if (string.IsNullOrEmpty(propName) || propName == "types") continue;
|
||
|
||
// Következő sorok metadatainak összegyűjtése
|
||
var metadata = new HashSet<string>();
|
||
int j = i + 1;
|
||
while (j < lines.Count && lines[j].StartsWith(" ") && lines[j].Trim().Contains(':'))
|
||
{
|
||
var metaLine = lines[j].Trim();
|
||
if (metaLine.StartsWith("navigation:")) metadata.Add("navigation");
|
||
if (metaLine.StartsWith("foreign-key:")) metadata.Add("foreign-key");
|
||
if (metaLine.StartsWith("inverse-property:")) metadata.Add("inverse-property");
|
||
if (metaLine.StartsWith("other-key:")) metadata.Add("other-key");
|
||
j++;
|
||
}
|
||
|
||
// Ha van navigation attribútum, ellenőrizzük a szükséges metadatokat
|
||
if (metadata.Contains("navigation"))
|
||
{
|
||
var navLine = lines.Skip(i + 1).FirstOrDefault(x => x.Trim().StartsWith("navigation:"));
|
||
if (navLine != null)
|
||
{
|
||
var isUnidirectional = knownUnidirectionalNavigations.Contains(propName);
|
||
var isPolymorphic = knownPolymorphicNavigations.Contains(propName);
|
||
|
||
if (navLine.Contains("many-to-one"))
|
||
{
|
||
if (!metadata.Contains("foreign-key"))
|
||
missingMetadata.Add($"{propName} (ManyToOne): hiányzik foreign-key");
|
||
|
||
if (!metadata.Contains("inverse-property"))
|
||
{
|
||
if (isUnidirectional)
|
||
skippedUnidirectional.Add($"{propName} (ManyToOne): egyirányú kapcsolat, nincs inverse");
|
||
else
|
||
missingMetadata.Add($"{propName} (ManyToOne): hiányzik inverse-property");
|
||
}
|
||
}
|
||
else if (navLine.Contains("one-to-many"))
|
||
{
|
||
// other-key: polimorf kapcsolatoknál nem kötelező
|
||
if (!metadata.Contains("other-key"))
|
||
{
|
||
if (isPolymorphic)
|
||
{
|
||
skippedUnidirectional.Add($"{propName} (OneToMany): polimorf kapcsolat, nincs other-key");
|
||
}
|
||
else
|
||
{
|
||
// DEBUG: részletes info
|
||
Console.WriteLine($"\n[DEBUG] {propName} (OneToMany) - other-key hiányzik!");
|
||
|
||
// Keressük meg a property típusát a Toon outputban
|
||
var propTypePart = line.Split(':').LastOrDefault()?.Trim() ?? "";
|
||
Console.WriteLine($" Property type: {propTypePart}");
|
||
|
||
// Ha List<X> formátum, keressük meg X-et
|
||
if (propTypePart.StartsWith("List<") && propTypePart.EndsWith(">"))
|
||
{
|
||
var elementTypeName = propTypePart.Substring(5, propTypePart.Length - 6);
|
||
Console.WriteLine($" Element type: {elementTypeName}");
|
||
|
||
// Keressük meg az element type definícióját
|
||
var elementTypeDefIndex = lines.FindIndex(l => l.Trim().StartsWith($"{elementTypeName}:"));
|
||
if (elementTypeDefIndex >= 0)
|
||
{
|
||
Console.WriteLine($" Element type definition found at line {elementTypeDefIndex}");
|
||
// Listázzuk ki az element type property-jeit amik "Id"-re végződnek
|
||
for (int k = elementTypeDefIndex + 1; k < lines.Count; k++)
|
||
{
|
||
var propLine = lines[k];
|
||
if (!propLine.StartsWith(" ")) break; // Új típus definíció
|
||
if (propLine.StartsWith(" ")) continue; // Metaadat, skip
|
||
|
||
var trimmed = propLine.Trim();
|
||
if (trimmed.EndsWith(": int") && trimmed.Contains("Id"))
|
||
{
|
||
Console.WriteLine($" FK candidate: {trimmed}");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
missingMetadata.Add($"{propName} (OneToMany): hiányzik other-key");
|
||
}
|
||
}
|
||
|
||
if (!metadata.Contains("inverse-property"))
|
||
{
|
||
if (isUnidirectional)
|
||
skippedUnidirectional.Add($"{propName} (OneToMany): egyirányú kapcsolat, nincs inverse");
|
||
else
|
||
missingMetadata.Add($"{propName} (OneToMany): hiányzik inverse-property");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (skippedUnidirectional.Count > 0)
|
||
{
|
||
Console.WriteLine("EGYIRÁNYÚ/POLIMORF KAPCSOLATOK (nem hiba):");
|
||
foreach (var skipped in skippedUnidirectional)
|
||
{
|
||
Console.WriteLine($" ℹ {skipped}");
|
||
}
|
||
Console.WriteLine();
|
||
}
|
||
|
||
if (missingMetadata.Count > 0)
|
||
{
|
||
Console.WriteLine("HIÁNYZÓ METAADATOK:");
|
||
foreach (var missing in missingMetadata)
|
||
{
|
||
Console.WriteLine($" - {missing}");
|
||
}
|
||
Assert.Fail($"Hiányzó navigation metaadatok: {missingMetadata.Count} db");
|
||
}
|
||
|
||
Console.WriteLine("✓ Minden navigation property tartalmazza a szükséges metadatokat");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Test helper class to verify reference handling with shared object instances.
|
||
/// </summary>
|
||
public class TestContainerWithSharedRefs
|
||
{
|
||
public ProductDto? Product1 { get; set; }
|
||
public ProductDto? Product2 { get; set; }
|
||
} |