From ec9f2363a2684bb4761992113b581e7cd746e83b Mon Sep 17 00:00:00 2001 From: Temo Nikolaishvili Date: Wed, 8 Apr 2026 18:04:42 +0400 Subject: [PATCH 1/4] Added comprehensive null-safety tests for dictionary and property access. Updated SimpraParserVisitor for null-safe dictionary indexing and revised test data for substring edge cases. Updated NuGet package versions. --- Directory.Packages.props | 8 +- .../Visitor/SimpraParserVisitor.cs | 11 +- .../Models/DebtorAccount.cs | 17 + .../AltaSoft.Simpra.tests/NullSafetyTests.cs | 949 ++++++++++++++++++ .../SimpraExpressionTests.cs | 42 +- 5 files changed, 1008 insertions(+), 19 deletions(-) create mode 100644 tests/AltaSoft.Simpra.tests/Models/DebtorAccount.cs create mode 100644 tests/AltaSoft.Simpra.tests/NullSafetyTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index b78452f..357e003 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,14 +4,14 @@ - - + + - + - + \ No newline at end of file diff --git a/src/AltaSoft.Simpra/Visitor/SimpraParserVisitor.cs b/src/AltaSoft.Simpra/Visitor/SimpraParserVisitor.cs index 316616c..433a79b 100644 --- a/src/AltaSoft.Simpra/Visitor/SimpraParserVisitor.cs +++ b/src/AltaSoft.Simpra/Visitor/SimpraParserVisitor.cs @@ -569,11 +569,18 @@ private static Expression HandleDictionaryIndexAccess(SimpraParser.IndexAccessCo var keyExpr = index.Type != keyType ? Expression.Convert(index, keyType) : index; var valueVar = Expression.Variable(valueType, "dictValue"); + var dictVar = Expression.Variable(objectType, "dictRef"); + var assignDict = Expression.Assign(dictVar, left); - var tryGetValueCall = Expression.Call(left, tryGetValueMethod, keyExpr, valueVar); + var tryGetValueCall = Expression.Call(dictVar, tryGetValueMethod, keyExpr, valueVar); + var notNull = Expression.NotEqual(dictVar, Expression.Constant(null, objectType)); var defaultValue = Expression.Default(valueType); - var block = Expression.Block([valueVar], Expression.Condition(tryGetValueCall, valueVar, defaultValue)); + var block = Expression.Block( + [dictVar, valueVar], + assignDict, + Expression.Condition(Expression.AndAlso(notNull, tryGetValueCall), valueVar, defaultValue) + ); return ConvertToSimpraType(block, false, context); } diff --git a/tests/AltaSoft.Simpra.tests/Models/DebtorAccount.cs b/tests/AltaSoft.Simpra.tests/Models/DebtorAccount.cs new file mode 100644 index 0000000..ba6af27 --- /dev/null +++ b/tests/AltaSoft.Simpra.tests/Models/DebtorAccount.cs @@ -0,0 +1,17 @@ +namespace AltaSoft.Simpra.Tests.Models; + +public sealed class DebtorAccountModel +{ + public required DebtorAccount DebtorAccount { get; set; } +} + +public sealed class DebtorAccount +{ + public Dictionary? Properties { get; set; } + public Dictionary? Attributes { get; set; } + public Dictionary? IntKeyedProperties { get; set; } + public Dictionary? CustomerMap { get; set; } + public DebtorAccount? Nested { get; set; } + public string? Name { get; set; } + public List? Tags { get; set; } +} diff --git a/tests/AltaSoft.Simpra.tests/NullSafetyTests.cs b/tests/AltaSoft.Simpra.tests/NullSafetyTests.cs new file mode 100644 index 0000000..caa5f1a --- /dev/null +++ b/tests/AltaSoft.Simpra.tests/NullSafetyTests.cs @@ -0,0 +1,949 @@ +using AltaSoft.Simpra.Tests.Models; + +namespace AltaSoft.Simpra.Tests; + +public class NullSafetyTests +{ + + [Fact] + public void NullDictionary_SingleKeyLookup_IsComparison_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] is \"200\""); + + Assert.False(result); + } + + [Fact] + public void NullDictionary_MultipleKeyLookups_AndOperator_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Properties[\"AccSubType\"] is \"8\""); + + Assert.False(result); + } + + [Fact] + public void NullDictionary_MultipleKeyLookups_OrOperator_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Properties[\"AccSubType\"] is \"8\""); + + Assert.False(result); + } + + [Fact] + public void NullDictionary_ReturnValue_ShouldReturnNull() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "return DebtorAccount.Properties[\"AccType\"]"); + + Assert.Null(result); + } + + [Fact] + public void NullDictionary_HasValue_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] has value"); + + Assert.False(result); + } + + // ── Non-null dictionary: missing key should return default ── + + [Fact] + public void NonNullDictionary_MissingKey_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + Properties = new Dictionary { { "Other", "value" } } + } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] is \"200\""); + + Assert.False(result); + } + + [Fact] + public void NonNullDictionary_ExistingKey_ShouldReturnTrue() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + Properties = new Dictionary { { "AccType", "200" } } + } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] is \"200\""); + + Assert.True(result); + } + + [Fact] + public void NonNullDictionary_ExistingKey_ReturnValue() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + Properties = new Dictionary { { "AccType", "200" } } + } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "return DebtorAccount.Properties[\"AccType\"]"); + + Assert.Equal("200", result); + } + + [Fact] + public void NonNullDictionary_KeyWithNullValue_HasValue_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + Properties = new Dictionary { { "AccType", null } } + } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] has value"); + + Assert.False(result); + } + + // ── Null dictionary with complex value type ── + + [Fact] + public void NullDictionary_ComplexValueType_PropertyAccess_ShouldReturnDefault() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "return DebtorAccount.CustomerMap[\"key\"].Id"); + + Assert.Equal(0, result); + } + + [Fact] + public void NullDictionary_ComplexValueType_Comparison_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.CustomerMap[\"key\"].Id is 1"); + + Assert.False(result); + } + + // ── Non-null dict with complex values – existing vs missing key ── + + [Fact] + public void NonNullDictionary_ComplexValueType_ExistingKey_ShouldReturnCorrectValue() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + CustomerMap = new Dictionary { { "vip", new Customer { Id = 42, Status = 10 } } } + } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "return DebtorAccount.CustomerMap[\"vip\"].Id"); + + Assert.Equal(42, result); + } + + [Fact] + public void NonNullDictionary_ComplexValueType_MissingKey_ShouldReturnDefault() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + CustomerMap = new Dictionary { { "vip", new Customer { Id = 42, Status = 10 } } } + } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "return DebtorAccount.CustomerMap[\"unknown\"].Id"); + + Assert.Equal(0, result); + } + + // ── Null parent object in chain before dictionary ── + + [Fact] + public void NullParentObject_DictionaryAccess_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount { Nested = null } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Nested.Properties[\"Key\"] is \"value\""); + + Assert.False(result); + } + + [Fact] + public void NullParentObject_DictionaryAccess_ReturnValue_ShouldReturnNull() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount { Nested = null } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "return DebtorAccount.Nested.Properties[\"Key\"]"); + + Assert.Null(result); + } + + // ── Nested dictionary on non-null parent ── + + [Fact] + public void NestedObject_NullDictionary_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + Nested = new DebtorAccount() // Nested.Properties is null + } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Nested.Properties[\"Key\"] is \"value\""); + + Assert.False(result); + } + + [Fact] + public void NestedObject_NonNullDictionary_ExistingKey_ShouldReturnTrue() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + Nested = new DebtorAccount + { + Properties = new Dictionary { { "Key", "value" } } + } + } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Nested.Properties[\"Key\"] is \"value\""); + + Assert.True(result); + } + + // ── Null list access (non-dictionary indexable) ── + + [Fact] + public void NullList_IndexAccess_ReturnValue_ShouldReturnNull() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount { Tags = null } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "return DebtorAccount.Tags[1]"); + + Assert.Null(result); + } + + [Fact] + public void NullList_IndexAccess_Comparison_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount { Tags = null } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Tags[1] is \"hello\""); + + Assert.False(result); + } + + // ── Null property on parent (non-dictionary, non-list) ── + + [Fact] + public void NullProperty_MemberAccess_Comparison_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount { Name = null } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Name is \"Test\""); + + Assert.False(result); + } + + [Fact] + public void NullProperty_MemberAccess_HasValue_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount { Name = null } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Name has value"); + + Assert.False(result); + } + + [Fact] + public void NonNullProperty_MemberAccess_HasValue_ShouldReturnTrue() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount { Name = "Test" } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Name has value"); + + Assert.True(result); + } + + // ── Mixed: null dict combined with non-null property in same expression ── + + [Fact] + public void NullDictionary_And_NonNullProperty_MixedExpression_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount { Name = "Test" } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Name is \"Test\""); + + Assert.False(result); + } + + [Fact] + public void NullDictionary_Or_NonNullProperty_MixedExpression_ShouldReturnTrue() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount { Name = "Test" } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Name is \"Test\""); + + Assert.True(result); + } + + // ── Null dictionary with "is not" operator ── + + [Fact] + public void NullDictionary_IsNotComparison_ShouldReturnTrue() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] is not \"200\""); + + Assert.True(result); + } + + // ── Null dictionary with "in" operator ── + + [Fact] + public void NullDictionary_InOperator_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] in [\"200\", \"300\"]"); + + Assert.False(result); + } + + // ── Null dictionary in when/conditional expressions ── + + [Fact] + public void NullDictionary_WhenExpression_ShouldEvaluateElseBranch() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "return when DebtorAccount.Properties[\"AccType\"] is \"200\" then \"match\" else \"no match\""); + + Assert.Equal("no match", result); + } + + // ── Existing dictionary tests (TestModel.Countries) with null dict ── + + [Fact] + public void TestModel_NullCountriesDictionary_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new TestModel + { + Transfer = new Transfer { Amount = 100, Currency = "USD" }, + Customer = new Customer { Id = 1, Status = 1 }, + Remittance = "Test", + Countries = null + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "Countries[\"Georgia\"] is \"Test\""); + + Assert.False(result); + } + + [Fact] + public void TestModel_NullCountriesDictionary_ReturnValue_ShouldReturnNull() + { + var simpra = new Simpra(); + var model = new TestModel + { + Transfer = new Transfer { Amount = 100, Currency = "USD" }, + Customer = new Customer { Id = 1, Status = 1 }, + Remittance = "Test", + Countries = null + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "return Countries[\"Georgia\"]"); + + Assert.Null(result); + } + + [Fact] + public void TestModel_NullDictionaryOfObjects_PropertyAccess_ShouldReturnDefault() + { + var simpra = new Simpra(); + var model = new TestModel + { + Transfer = new Transfer { Amount = 100, Currency = "USD" }, + Customer = new Customer { Id = 1, Status = 1 }, + Remittance = "Test", + DictionaryOfObjects = null + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "return DictionaryOfObjects[\"test\"].Id"); + + Assert.Equal(0, result); + } + + [Fact] + public void TestModel_NullDictionaryOfObjects_Comparison_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new TestModel + { + Transfer = new Transfer { Amount = 100, Currency = "USD" }, + Customer = new Customer { Id = 1, Status = 1 }, + Remittance = "Test", + DictionaryOfObjects = null + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DictionaryOfObjects[\"test\"].Id is 1"); + + Assert.False(result); + } + + // ── Null Transfer (parent object) with sub-property access ── + + [Fact] + public void NullTransfer_PropertyAccess_ShouldReturnDefault() + { + var simpra = new Simpra(); + var model = new TestModel + { + Transfer = null, + Customer = new Customer { Id = 1, Status = 1 }, + Remittance = "Test" + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "return Transfer.Amount"); + + Assert.Equal(0m, result); + } + + [Fact] + public void NullTransfer_CurrencyComparison_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new TestModel + { + Transfer = null, + Customer = new Customer { Id = 1, Status = 1 }, + Remittance = "Test" + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "Transfer.Currency is \"USD\""); + + Assert.False(result); + } + + [Fact] + public void NullTransfer_HasValue_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new TestModel + { + Transfer = null, + Customer = new Customer { Id = 1, Status = 1 }, + Remittance = "Test" + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "Transfer has value"); + + Assert.False(result); + } + + // ── Cross-dictionary and/or logic: Properties + Attributes ── + + [Fact] + public void BothDictsPopulated_And_BothMatch_ShouldReturnTrue() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + Properties = new Dictionary { { "AccType", "200" } }, + Attributes = new Dictionary { { "Region", "EU" } } + } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Attributes[\"Region\"] is \"EU\""); + + Assert.True(result); + } + + [Fact] + public void BothDictsPopulated_And_OneMismatch_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + Properties = new Dictionary { { "AccType", "200" } }, + Attributes = new Dictionary { { "Region", "US" } } + } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Attributes[\"Region\"] is \"EU\""); + + Assert.False(result); + } + + [Fact] + public void BothDictsPopulated_Or_OneMismatch_ShouldReturnTrue() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + Properties = new Dictionary { { "AccType", "200" } }, + Attributes = new Dictionary { { "Region", "US" } } + } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Attributes[\"Region\"] is \"EU\""); + + Assert.True(result); + } + + [Fact] + public void BothDictsPopulated_Or_BothMismatch_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + Properties = new Dictionary { { "AccType", "100" } }, + Attributes = new Dictionary { { "Region", "US" } } + } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Attributes[\"Region\"] is \"EU\""); + + Assert.False(result); + } + + [Fact] + public void PropertiesNull_AttributesPopulated_And_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + Properties = null, + Attributes = new Dictionary { { "Region", "EU" } } + } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Attributes[\"Region\"] is \"EU\""); + + Assert.False(result); + } + + [Fact] + public void PropertiesNull_AttributesPopulated_Or_ShouldReturnTrue() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + Properties = null, + Attributes = new Dictionary { { "Region", "EU" } } + } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Attributes[\"Region\"] is \"EU\""); + + Assert.True(result); + } + + [Fact] + public void PropertiesPopulated_AttributesNull_And_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + Properties = new Dictionary { { "AccType", "200" } }, + Attributes = null + } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Attributes[\"Region\"] is \"EU\""); + + Assert.False(result); + } + + [Fact] + public void PropertiesPopulated_AttributesNull_Or_ShouldReturnTrue() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + Properties = new Dictionary { { "AccType", "200" } }, + Attributes = null + } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Attributes[\"Region\"] is \"EU\""); + + Assert.True(result); + } + + [Fact] + public void BothDictsNull_And_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Attributes[\"Region\"] is \"EU\""); + + Assert.False(result); + } + + [Fact] + public void BothDictsNull_Or_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Attributes[\"Region\"] is \"EU\""); + + Assert.False(result); + } + + [Fact] + public void BothDictsPopulated_MixedMissingKeys_And_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + Properties = new Dictionary { { "AccType", "200" } }, + Attributes = new Dictionary { { "Other", "X" } } + } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Attributes[\"Region\"] is \"EU\""); + + Assert.False(result); + } + + [Fact] + public void BothDictsPopulated_MixedMissingKeys_Or_ShouldReturnTrue() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + Properties = new Dictionary { { "AccType", "200" } }, + Attributes = new Dictionary { { "Other", "X" } } + } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Attributes[\"Region\"] is \"EU\""); + + Assert.True(result); + } + + [Fact] + public void ThreeConditions_And_AllMatch_ShouldReturnTrue() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + Properties = new Dictionary { { "AccType", "200" } }, + Attributes = new Dictionary { { "Region", "EU" } }, + Name = "VIP" + } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Attributes[\"Region\"] is \"EU\" and DebtorAccount.Name is \"VIP\""); + + Assert.True(result); + } + + [Fact] + public void ThreeConditions_And_MiddleNull_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + Properties = new Dictionary { { "AccType", "200" } }, + Attributes = null, + Name = "VIP" + } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Attributes[\"Region\"] is \"EU\" and DebtorAccount.Name is \"VIP\""); + + Assert.False(result); + } + + [Fact] + public void ThreeConditions_Or_OnlyLastMatches_ShouldReturnTrue() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + Properties = null, + Attributes = null, + Name = "VIP" + } + }; + + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Attributes[\"Region\"] is \"EU\" or DebtorAccount.Name is \"VIP\""); + + Assert.True(result); + } + + [Fact] + public void MixedAndOr_NullDict_And_PopulatedDict_Or_Property_ShouldEvaluateCorrectly() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + Properties = null, + Attributes = new Dictionary { { "Region", "EU" } }, + Name = "VIP" + } + }; + + // (null_dict is "200" and attr is "EU") or name is "VIP" → (false and true) or true → true + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "(DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Attributes[\"Region\"] is \"EU\") or DebtorAccount.Name is \"VIP\""); + + Assert.True(result); + } + + [Fact] + public void MixedAndOr_NullDict_Or_PopulatedDict_And_Property_ShouldEvaluateCorrectly() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + Properties = null, + Attributes = new Dictionary { { "Region", "EU" } }, + Name = "VIP" + } + }; + + // (null_dict is "200" or attr is "EU") and name is "VIP" → (false or true) and true → true + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "(DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Attributes[\"Region\"] is \"EU\") and DebtorAccount.Name is \"VIP\""); + + Assert.True(result); + } + + [Fact] + public void MixedAndOr_AllNull_And_Property_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new DebtorAccountModel + { + DebtorAccount = new DebtorAccount + { + Properties = null, + Attributes = null, + Name = null + } + }; + + // (null is "200" or null is "EU") and null_name is "VIP" → (false or false) and false → false + var result = simpra.Execute( + model, new SimpraExpressionTests.TestFunctions(), + "(DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Attributes[\"Region\"] is \"EU\") and DebtorAccount.Name is \"VIP\""); + + Assert.False(result); + } +} diff --git a/tests/AltaSoft.Simpra.tests/SimpraExpressionTests.cs b/tests/AltaSoft.Simpra.tests/SimpraExpressionTests.cs index 2e8a1f5..013e122 100644 --- a/tests/AltaSoft.Simpra.tests/SimpraExpressionTests.cs +++ b/tests/AltaSoft.Simpra.tests/SimpraExpressionTests.cs @@ -1,4 +1,4 @@ -using AltaSoft.Simpra.Tests.Models; +using AltaSoft.Simpra.Tests.Models; namespace AltaSoft.Simpra.Tests; @@ -28,12 +28,12 @@ public class SimpraExpressionTests [InlineData("USD", 2, 0, "")] [InlineData("USD", 3, 0, "")] - // negative/zero start → clamp to 1 + // negative/zero start ? clamp to 1 [InlineData("USD", 0, 1, "U")] [InlineData("USD", -5, 2, "US")] [InlineData("USD", -2, 100, "USD")] - // negative length → empty + // negative length ? empty [InlineData("USD", 1, -1, "")] [InlineData("USD", 3, -10, "")] [InlineData("USD", -2, -10, "")] @@ -50,10 +50,10 @@ public class SimpraExpressionTests [InlineData(" ", 2, 100, " ")] // non-ASCII (safe checks with multi-byte chars) - [InlineData("თბილისი", 1, 2, "თბ")] - [InlineData("თბილისი", 3, 3, "ილი")] - [InlineData("თბილისი", 11, 5, "")] - [InlineData("თბილისი", -3, 100, "თბილისი")] + [InlineData("???????", 1, 2, "??")] + [InlineData("???????", 3, 3, "???")] + [InlineData("???????", 11, 5, "")] + [InlineData("???????", -3, 100, "???????")] public void BuiltInFunction_Substring_EdgeCases(string input, int start, int length, string expected) { @@ -101,7 +101,7 @@ public void BuiltInFunction_Substring_ShouldMatchExamplesFromPrompt() [InlineData("", 1, "")] [InlineData("", 5, "")] - // zero/negative start → clamp to 1 + // zero/negative start ? clamp to 1 [InlineData("USD", 0, "U")] [InlineData("USD", -1, "U")] [InlineData("USD", -5, "U")] @@ -113,11 +113,11 @@ public void BuiltInFunction_Substring_ShouldMatchExamplesFromPrompt() [InlineData(" X ", 3, " ")] // non-ASCII (single UTF-16 code units) - [InlineData("თბილისი", 1, "თ")] - [InlineData("თბილისი", 3, "ი")] - [InlineData("თბილისი", 6, "ს")] - [InlineData("თბილისი", 7, "ი")] - [InlineData("თბილისი", 8, "")] + [InlineData("???????", 1, "?")] + [InlineData("???????", 3, "?")] + [InlineData("???????", 6, "?")] + [InlineData("???????", 7, "?")] + [InlineData("???????", 8, "")] // very large index [InlineData("USD", int.MaxValue, "")] @@ -156,6 +156,22 @@ public void BuiltInFunction_Substring_NullSource_ShouldBeNull() Assert.Null(result); } + [Fact] + public void BuiltInFunc2tion_Substring_NullSource_ShouldBeNull() + { + // If your DSL defines a behavior for nulls, keep this. + // If it should throw instead, change to Assert.Throws. + var simpra = new Simpra(); + var model = new DebtorAccountModel() + { + DebtorAccount = new DebtorAccount() + }; + + var result = simpra.Execute(model, new TestFunctions(), "DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Properties[\"AccSubType\"] is \"8\""); + Assert.False(result); + } + + [Fact] public void BuiltInFunction_Substring_ShouldReturnCorrectSubstringOfLength1() { From 2c68f099228ef97c027c315cfa949b24f399bd44 Mon Sep 17 00:00:00 2001 From: Temo Nikolaishvili Date: Wed, 8 Apr 2026 18:05:20 +0400 Subject: [PATCH 2/4] VC --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 9d26598..ceb7f23 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -11,7 +11,7 @@ Simpra ALTA Software llc. Copyright © 2024 ALTA Software llc. - 2.0.1 + 2.0.2 From a05429c31c695df2ae924704718d48f2ef22dd52 Mon Sep 17 00:00:00 2001 From: Teimuraz Nikolaishvili <47372530+temonk@users.noreply.github.com> Date: Wed, 8 Apr 2026 18:15:59 +0400 Subject: [PATCH 3/4] Update tests/AltaSoft.Simpra.tests/SimpraExpressionTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/AltaSoft.Simpra.tests/SimpraExpressionTests.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/AltaSoft.Simpra.tests/SimpraExpressionTests.cs b/tests/AltaSoft.Simpra.tests/SimpraExpressionTests.cs index 013e122..826a87c 100644 --- a/tests/AltaSoft.Simpra.tests/SimpraExpressionTests.cs +++ b/tests/AltaSoft.Simpra.tests/SimpraExpressionTests.cs @@ -50,10 +50,10 @@ public class SimpraExpressionTests [InlineData(" ", 2, 100, " ")] // non-ASCII (safe checks with multi-byte chars) - [InlineData("???????", 1, 2, "??")] - [InlineData("???????", 3, 3, "???")] - [InlineData("???????", 11, 5, "")] - [InlineData("???????", -3, 100, "???????")] + [InlineData("\u0410\u0411\u0412\u0413\u0414\u0415\u0416", 1, 2, "\u0410\u0411")] + [InlineData("\u0410\u0411\u0412\u0413\u0414\u0415\u0416", 3, 3, "\u0412\u0413\u0414")] + [InlineData("\u0410\u0411\u0412\u0413\u0414\u0415\u0416", 11, 5, "")] + [InlineData("\u0410\u0411\u0412\u0413\u0414\u0415\u0416", -3, 100, "\u0410\u0411\u0412\u0413\u0414\u0415\u0416")] public void BuiltInFunction_Substring_EdgeCases(string input, int start, int length, string expected) { From 03613023da1c3fe77ee3b69751ef648284517afe Mon Sep 17 00:00:00 2001 From: Teimuraz Nikolaishvili <47372530+temonk@users.noreply.github.com> Date: Wed, 8 Apr 2026 18:16:14 +0400 Subject: [PATCH 4/4] Update tests/AltaSoft.Simpra.tests/SimpraExpressionTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../SimpraExpressionTests.cs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/tests/AltaSoft.Simpra.tests/SimpraExpressionTests.cs b/tests/AltaSoft.Simpra.tests/SimpraExpressionTests.cs index 826a87c..d21aad2 100644 --- a/tests/AltaSoft.Simpra.tests/SimpraExpressionTests.cs +++ b/tests/AltaSoft.Simpra.tests/SimpraExpressionTests.cs @@ -156,22 +156,6 @@ public void BuiltInFunction_Substring_NullSource_ShouldBeNull() Assert.Null(result); } - [Fact] - public void BuiltInFunc2tion_Substring_NullSource_ShouldBeNull() - { - // If your DSL defines a behavior for nulls, keep this. - // If it should throw instead, change to Assert.Throws. - var simpra = new Simpra(); - var model = new DebtorAccountModel() - { - DebtorAccount = new DebtorAccount() - }; - - var result = simpra.Execute(model, new TestFunctions(), "DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Properties[\"AccSubType\"] is \"8\""); - Assert.False(result); - } - - [Fact] public void BuiltInFunction_Substring_ShouldReturnCorrectSubstringOfLength1() {