diff --git a/Directory.Build.props b/Directory.Build.props index ce33460..7bba322 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -11,7 +11,7 @@ Simpra ALTA Software llc. Copyright © 2024 ALTA Software llc. - 2.0.3 + 2.1.4 diff --git a/src/AltaSoft.Simpra/InternalLanguageFunctions.cs b/src/AltaSoft.Simpra/InternalLanguageFunctions.cs index bcdef88..3afa5f0 100644 --- a/src/AltaSoft.Simpra/InternalLanguageFunctions.cs +++ b/src/AltaSoft.Simpra/InternalLanguageFunctions.cs @@ -31,10 +31,10 @@ internal static class InternalLanguageFunctions public static SimpraString @string(SimpraBool input) => new(input.ToString()); public static SimpraString @string(SimpraDate input) => new(input.ToString()); - public static SimpraNumber number(SimpraString input) => new(input.Value); - public static SimpraNumber number(SimpraBool input) => new(input.Value); + public static SimpraNumber number(SimpraString input) => input.HasValue ? new SimpraNumber(input.Value) : (SimpraNumber)SimpraNumber.NoValue; + public static SimpraNumber number(SimpraBool input) => input.HasValue ? new SimpraNumber(input.Value) : (SimpraNumber)SimpraNumber.NoValue; - public static SimpraDate date(SimpraString input) => new(input.Value); + public static SimpraDate date(SimpraString input) => input.HasValue ? new SimpraDate(input.Value) : (SimpraDate)SimpraDate.NoValue; #pragma warning restore IDE1006 // Naming Styles // ReSharper enable InconsistentNaming diff --git a/src/AltaSoft.Simpra/Types/SimpraNumber.cs b/src/AltaSoft.Simpra/Types/SimpraNumber.cs index a71d6d0..a3e1245 100644 --- a/src/AltaSoft.Simpra/Types/SimpraNumber.cs +++ b/src/AltaSoft.Simpra/Types/SimpraNumber.cs @@ -40,17 +40,17 @@ private void SetValue(decimal? value) public static implicit operator decimal(SimpraNumber value) => value.Value; public static implicit operator bool(SimpraNumber value) => value.Value != 0m; - public static implicit operator sbyte?(SimpraNumber value) => (sbyte?)value.Value; - public static implicit operator byte?(SimpraNumber value) => (byte?)value.Value; - public static implicit operator short?(SimpraNumber value) => (short?)value.Value; - public static implicit operator ushort?(SimpraNumber value) => (ushort?)value.Value; - public static implicit operator int?(SimpraNumber value) => (int?)value.Value; - public static implicit operator uint?(SimpraNumber value) => (uint?)value.Value; - public static implicit operator long?(SimpraNumber value) => (long?)value.Value; - public static implicit operator ulong?(SimpraNumber value) => (ulong?)value.Value; - public static implicit operator float?(SimpraNumber value) => (float?)value.Value; - public static implicit operator double?(SimpraNumber value) => (double?)value.Value; - public static implicit operator decimal?(SimpraNumber value) => value.Value; + public static implicit operator sbyte?(SimpraNumber value) => value.HasValue ? (sbyte?)value.Value : null; + public static implicit operator byte?(SimpraNumber value) => value.HasValue ? (byte?)value.Value : null; + public static implicit operator short?(SimpraNumber value) => value.HasValue ? (short?)value.Value : null; + public static implicit operator ushort?(SimpraNumber value) => value.HasValue ? (ushort?)value.Value : null; + public static implicit operator int?(SimpraNumber value) => value.HasValue ? (int?)value.Value : null; + public static implicit operator uint?(SimpraNumber value) => value.HasValue ? (uint?)value.Value : null; + public static implicit operator long?(SimpraNumber value) => value.HasValue ? (long?)value.Value : null; + public static implicit operator ulong?(SimpraNumber value) => value.HasValue ? (ulong?)value.Value : null; + public static implicit operator float?(SimpraNumber value) => value.HasValue ? (float?)value.Value : null; + public static implicit operator double?(SimpraNumber value) => value.HasValue ? (double?)value.Value : null; + public static implicit operator decimal?(SimpraNumber value) => value.HasValue ? value.Value : null; public static implicit operator bool?(SimpraNumber value) => value.HasValue ? value.Value != 0m : null; public static implicit operator SimpraNumber(sbyte value) => new(value); @@ -113,7 +113,7 @@ public override readonly bool Equals(object? obj) { if (obj is null) return !HasValue; - if (obj is SimpraDate simpraVar) + if (obj is SimpraNumber simpraVar) return Equals(simpraVar); return obj.GetType().IsNumber() && Value == Convert.ToDecimal(obj, CultureInfo.InvariantCulture); diff --git a/src/AltaSoft.Simpra/Visitor/SimpraParserVisitor.cs b/src/AltaSoft.Simpra/Visitor/SimpraParserVisitor.cs index 433a79b..50b4051 100644 --- a/src/AltaSoft.Simpra/Visitor/SimpraParserVisitor.cs +++ b/src/AltaSoft.Simpra/Visitor/SimpraParserVisitor.cs @@ -3,6 +3,7 @@ using System.ComponentModel; using System.Linq; using System.Linq.Expressions; +using AltaSoft.DomainPrimitives; using AltaSoft.Simpra.Types; using Antlr4.Runtime.Tree; @@ -229,9 +230,23 @@ public Expression VisitMemberAccess(SimpraParser.MemberAccessContext context) var exprProperty2 = Expression.Property(variable, propertyName); var notNull = Expression.NotEqual(variable, Expression.Constant(null, exprBaseObject.Type)); - var defaultValue = Expression.Default(exprProperty2.Type); + // Domain primitives throw when their uninitialized default value is read, so if the property + // is a domain primitive, unwrap it to its underlying type before building the null-fallback + // default. Otherwise the fallback would be an uninitialized domain primitive that throws as + // soon as ConvertToSimpraType tries to convert it. + Expression truePart = exprProperty2; + Expression defaultValue; + if (exprProperty2.Type.TryGetUnderlyingDomainPrimitiveType(out var domainType)) + { + truePart = Expression.Convert(exprProperty2, domainType); + defaultValue = Expression.Default(domainType); + } + else + { + defaultValue = Expression.Default(exprProperty2.Type); + } - var expr = Expression.Condition(notNull, exprProperty2, defaultValue); + var expr = Expression.Condition(notNull, truePart, defaultValue); var block = Expression.Block([variable], assignExpr, expr); diff --git a/src/AltaSoft.Simpra/Visitor/SimpraParserVisitor.tools.cs b/src/AltaSoft.Simpra/Visitor/SimpraParserVisitor.tools.cs index 5b336d9..2133c29 100644 --- a/src/AltaSoft.Simpra/Visitor/SimpraParserVisitor.tools.cs +++ b/src/AltaSoft.Simpra/Visitor/SimpraParserVisitor.tools.cs @@ -205,12 +205,13 @@ private Expression CallFunction(string methodName, Expression[] arguments, Parse if (method is null) throw new SimpraException(context, $"Function '{methodName}({string.Join(',', paramTypes.Select(x => x.Name))})' not found"); + var methodParameters = method.GetParameters(); var convertedArguments = new Expression[paramTypes.Length + (matchWithToken ? 1 : 0)]; for (var i = 0; i < paramTypes.Length; i++) { var expr = arguments[i]; - convertedArguments[i] = ConvertToType(expr, paramTypes[i], context); + convertedArguments[i] = ConvertToType(expr, methodParameters[i].ParameterType, context); } if (matchWithToken) diff --git a/tests/AltaSoft.Simpra.tests/AsyncExecutionTests.cs b/tests/AltaSoft.Simpra.tests/AsyncExecutionTests.cs new file mode 100644 index 0000000..4b956fa --- /dev/null +++ b/tests/AltaSoft.Simpra.tests/AsyncExecutionTests.cs @@ -0,0 +1,94 @@ +using AltaSoft.Simpra.Tests.Models; +using static AltaSoft.Simpra.Tests.Models.TestModelFactory; + +namespace AltaSoft.Simpra.Tests; + +public class AsyncExecutionTests +{ + [Fact] + public async Task ExecuteExpression_Should_ReturnTrue_When_CcyIsInListOfCurrencyCodes() + { + var simpra = new Simpra(); + var model = GetTestModel(); + + const string expressionCode = "return Ccy in (ListOfCurrencyCodes('VisaB2B') + 'USD')"; + var result = await simpra.ExecuteAsync(model, new TestFunctions(), expressionCode, null, CancellationToken.None); + Assert.True(result); + } + + [Fact] + public async Task Expression_Should_EvaluateSanctionedCountriesExpression_Correctly() + { + const string expressionCode = + """ + let russianCountries = BigList('sanctioned_countries') + let amount = Transfer.Amount + let ccy = Transfer.Currency + + let isValidCcy = (ccy is '' or ccy not in ['USD', 'EUR']) and (ccy like 'I%' or ccy matches '[a-zA-Z_][a-zA-Z_0-9]') + let isValidAmount = amount > 1000 and amount < 2000 + let isValidAmount2 = amount > 1000 and < 2000 + let isValidRemittance = length(Remittance) > 4 + + return isValidCcy and isValidAmount and isValidRemittance + """; + + var simpra = new Simpra(); + var model = GetTestModel(); // Model is irrelevant here + var result = await simpra.ExecuteAsync(model, new TestFunctions(), expressionCode, + new SimpraCompilerOptions { MutabilityOption = MutabilityOption.Immutable, StringComparisonOption = StringComparisonOption.IgnoreCase }, + CancellationToken.None); + Assert.False(result); + } + + [Fact] + public async Task ExecuteExpression_Should_EvaluateAsyncCallDirectlyWithinArithmeticBinary() + { + const string expressionCode = "return Compute(3, 4) + 1"; + + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = await simpra.ExecuteAsync(model, new TestFunctions(), expressionCode, null, CancellationToken.None); + Assert.Equal(8m, result); + } + + [Theory] + [InlineData("Compute(1, 1) > 1 and Compute(3, 3) > 5", true)] // left true -> right (async) must be evaluated + [InlineData("Compute(1, 1) > 5 and Compute(3, 3) > 5", false)] // left false -> AND short-circuits, right (async) never evaluated + [InlineData("Compute(1, 1) > 5 or Compute(3, 3) > 5", true)] // left false -> OR must evaluate right (async) + [InlineData("Compute(1, 1) > 1 or Compute(3, 3) > 100", true)] // left true -> OR short-circuits, right (async) never evaluated + public async Task ExecuteExpression_Should_ShortCircuitCorrectly_When_AsyncCallIsOnRightOfAndOr(string condition, bool expected) + { + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = await simpra.ExecuteAsync(model, new TestFunctions(), $"return {condition}", null, CancellationToken.None); + Assert.Equal(expected, result); + } + + [Fact] + public async Task ExecuteExpression_Should_EvaluateAsyncCallWithinConditional() + { + const string expressionCode = + "return when Compute(1, 1) > 1 then 'yes' else 'no' end"; + + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = await simpra.ExecuteAsync(model, new TestFunctions(), expressionCode, null, CancellationToken.None); + Assert.Equal("yes", result); + } + + [Fact] + public async Task ExecuteExpression_Should_EvaluateMultipleAsyncCallsCombinedInSingleBinaryExpression() + { + const string expressionCode = "return Compute(1, 2) + Compute(3, 4)"; + + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = await simpra.ExecuteAsync(model, new TestFunctions(), expressionCode, null, CancellationToken.None); + Assert.Equal(10m, result); + } +} diff --git a/tests/AltaSoft.Simpra.tests/BuiltInFunctionsTests.cs b/tests/AltaSoft.Simpra.tests/BuiltInFunctionsTests.cs new file mode 100644 index 0000000..0ce9963 --- /dev/null +++ b/tests/AltaSoft.Simpra.tests/BuiltInFunctionsTests.cs @@ -0,0 +1,315 @@ +using AltaSoft.Simpra.Tests.Models; +using static AltaSoft.Simpra.Tests.Models.TestModelFactory; + +namespace AltaSoft.Simpra.Tests; + +public class BuiltInFunctionsTests +{ + [Theory] + // basic / within bounds + [InlineData("USD", 1, 3, "USD")] // from U + [InlineData("USD", 1, 2, "US")] + [InlineData("USD", 2, 2, "SD")] // from S + [InlineData("USD", 3, 1, "D")] // from D + + // length clamping at the end + [InlineData("USD", 1, 10003, "USD")] + [InlineData("USD", 3, 10, "D")] + + // start beyond end + [InlineData("USD", 1000, 1, "")] + [InlineData("USD", 4, 1, "")] // beyond "USD" + + // start exactly at end + [InlineData("USD", 4, 0, "")] + [InlineData("USD", 4, 10, "")] + + // zero length + [InlineData("USD", 1, 0, "")] + [InlineData("USD", 2, 0, "")] + [InlineData("USD", 3, 0, "")] + + // negative/zero start ? clamp to 1 + [InlineData("USD", 0, 1, "U")] + [InlineData("USD", -5, 2, "US")] + [InlineData("USD", -2, 100, "USD")] + + // negative length ? empty + [InlineData("USD", 1, -1, "")] + [InlineData("USD", 3, -10, "")] + [InlineData("USD", -2, -10, "")] + + // empty input + [InlineData("", 1, 5, "")] + [InlineData("", 10, 1, "")] + [InlineData("", -3, 2, "")] + [InlineData("", 1, 0, "")] + + // whitespace + [InlineData(" ", 1, 1, " ")] + [InlineData(" ", 2, 2, " ")] + [InlineData(" ", 2, 100, " ")] + + // non-ASCII (safe checks with multi-byte chars) + [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) + { + var simpra = new Simpra(); + var model = GetTestModel(); + model.Ccy = input; + var expressionCode = $"return substring(Ccy,{start},{length})"; + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.Equal(expected, result); + } + + [Fact] + public void BuiltInFunction_Substring_ShouldMatchExamplesFromPrompt() + { + var simpra = new Simpra(); + var model = GetTestModel(); + model.Ccy = "USD"; + + var result = simpra.Execute(model, new TestFunctions(), "return substring(Ccy,0,3)"); + Assert.Equal("USD", result); + + result = simpra.Execute(model, new TestFunctions(), "return substring(Ccy,0,10003)"); + Assert.Equal("USD", result); + + result = simpra.Execute(model, new TestFunctions(), "return substring(Ccy,1000,1)"); + Assert.Equal("", result); + + result = simpra.Execute(model, new TestFunctions(), "return substring(Ccy,1000,0)"); + Assert.Equal("", result); + + result = simpra.Execute(model, new TestFunctions(), "return substring(Ccy,1,0)"); + Assert.Equal("", result); + } + [Theory] + // basic in-bounds + [InlineData("USD", 1, "U")] + [InlineData("USD", 2, "S")] + [InlineData("USD", 3, "D")] + + // at/after end => empty + [InlineData("USD", 4, "")] + [InlineData("USD", 1000, "")] + [InlineData("U", 2, "")] + [InlineData("", 1, "")] + [InlineData("", 5, "")] + + // zero/negative start ? clamp to 1 + [InlineData("USD", 0, "U")] + [InlineData("USD", -1, "U")] + [InlineData("USD", -5, "U")] + [InlineData("", -3, "")] + + // whitespace + [InlineData(" X ", 1, " ")] + [InlineData(" X ", 2, "X")] + [InlineData(" X ", 3, " ")] + + // non-ASCII (single UTF-16 code units) + [InlineData("???????", 1, "?")] + [InlineData("???????", 3, "?")] + [InlineData("???????", 6, "?")] + [InlineData("???????", 7, "?")] + [InlineData("???????", 8, "")] + + // very large index + [InlineData("USD", int.MaxValue, "")] + public void BuiltInFunction_Substring_StartOnly_EdgeCases(string input, int start, string expected) + { + var simpra = new Simpra(); + var model = GetTestModel(); + model.Ccy = input; + var expressionCode = $"return substring(Ccy,{start})"; + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.Equal(expected, result); + } + + [Fact] + public void BuiltInFunction_Substring_StartOnly_NullSource_ShouldBeEmpty() + { + var simpra = new Simpra(); + var model = GetTestModel(); + model.Ccy = null; + + var result = simpra.Execute(model, new TestFunctions(), "return substring(Ccy,1)"); + Assert.Null(result); + } + + [Fact] + public void BuiltInFunction_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 = GetTestModel(); + model.Ccy = null!; + + var result = simpra.Execute(model, new TestFunctions(), "return substring(Ccy,0,3)"); + Assert.Null(result); + } + + [Fact] + public void BuiltInFunction_Substring_ShouldReturnCorrectSubstringOfLength1() + { + const string expressionCode = "return substring(Ccy,1)"; + + var simpra = new Simpra(); + var model = GetTestModel(); + model.Ccy = "USD"; + + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + Assert.Equal("U", result); + } + + [Fact] + public void BuiltInFunction_Round_WithDecimalsArgument_ShouldRoundToGivenPrecision() + { + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = simpra.Execute(model, new TestFunctions(), "return round(3.14159, 2)"); + Assert.Equal(3.14m, result); + + result = simpra.Execute(model, new TestFunctions(), "return round(3.14159, 3)"); + Assert.Equal(3.142m, result); + + result = simpra.Execute(model, new TestFunctions(), "return round(-3.14159, 2)"); + Assert.Equal(-3.14m, result); + } + + [Fact] + public void BuiltInFunction_Round_AtMidpoint_ShouldRoundToNearestEvenInteger() + { + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = simpra.Execute(model, new TestFunctions(), "return round(2.5)"); + Assert.Equal(2m, result); + + result = simpra.Execute(model, new TestFunctions(), "return round(3.5)"); + Assert.Equal(4m, result); + } + + [Theory] + [InlineData(-5, 5)] + [InlineData(5, 5)] + [InlineData(0, 0)] + public void BuiltInFunction_Abs_ShouldReturnAbsoluteValue(int input, int expected) + { + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = simpra.Execute(model, new TestFunctions(), $"return abs({input})"); + Assert.Equal(expected, result); + } + + [Fact] + public void BuiltInFunction_Abs_WithFractionalValue_ShouldReturnAbsoluteValue() + { + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = simpra.Execute(model, new TestFunctions(), "return abs(-3.5)"); + Assert.Equal(3.5m, result); + } + + [Fact] + public void BuiltInFunction_String_FromNumber_ShouldReturnDecimalStringRepresentation() + { + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = simpra.Execute(model, new TestFunctions(), "return string(42.5)"); + Assert.Equal("42.5", result); + } + + [Fact] + public void BuiltInFunction_String_FromBool_ShouldReturnCapitalizedBooleanText() + { + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = simpra.Execute(model, new TestFunctions(), "return string(true)"); + Assert.Equal("True", result); + } + + [Fact] + public void BuiltInFunction_Number_FromString_ShouldParseDecimalValue() + { + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = simpra.Execute(model, new TestFunctions(), "return number('123.45')"); + Assert.Equal(123.45m, result); + } + + [Fact] + public void BuiltInFunction_Number_FromBool_ShouldReturnOneForTrueAndZeroForFalse() + { + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = simpra.Execute(model, new TestFunctions(), "return number(true)"); + Assert.Equal(1m, result); + + result = simpra.Execute(model, new TestFunctions(), "return number(false)"); + Assert.Equal(0m, result); + } + + [Fact] + public void BuiltInFunction_Number_FromNullString_ShouldReturnNullInsteadOfThrowing() + { + var simpra = new Simpra(); + var model = GetTestModel(); + model.Ccy = null; + + var result = simpra.Execute(model, new TestFunctions(), "return number(Ccy)"); + Assert.Null(result); + } + + [Fact] + public void BuiltInFunction_Date_FromString_ShouldParseCorrectDate() + { + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = simpra.Execute(model, new TestFunctions(), "return date('2024-03-15')"); + Assert.Equal(new DateTime(2024, 3, 15), result); + } + + [Fact] + public void BuiltInFunction_Length_OfBooleanListLiteral_ShouldReturnElementCount() + { + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = simpra.Execute(model, new TestFunctions(), "return length([true, false, true])"); + Assert.Equal(3m, result); + } + + [Fact] + public void Expression_Should_ReturnTrue_When_XIsNotFive_And_LengthIsTwo() + { + const string expressionCode = + """ + let X = '50' + return X is not '5' + and length(X) is 2; + """; + + var simpra = new Simpra(); + var model = GetTestModel(); // Model is irrelevant here + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.True(result); + } +} diff --git a/tests/AltaSoft.Simpra.tests/ComplexObjectAccessTests.cs b/tests/AltaSoft.Simpra.tests/ComplexObjectAccessTests.cs new file mode 100644 index 0000000..65d748d --- /dev/null +++ b/tests/AltaSoft.Simpra.tests/ComplexObjectAccessTests.cs @@ -0,0 +1,490 @@ +using AltaSoft.Simpra.Tests.Models; +using static AltaSoft.Simpra.Tests.Models.TestModelFactory; + +namespace AltaSoft.Simpra.Tests; + +public class ComplexObjectAccessTests +{ + [Fact] + public void DictionaryIndexer_MissingKey_ReturnsDefaultValueForProperty() + { + const string expressionCode = + "return DictionaryOfObjects['test'].Id"; + + var simpra = new Simpra(); + var model = GetTestModel(); + model.DictionaryOfObjects = new Dictionary { { "Georgia", new Customer { Id = 1, Status = 10 } } }; + + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + Assert.Equal(0, result); + } + + [Fact] + public void CallGetValueFromDictionaryWhenKeyDoesNotExist_ShouldReturnDefault() + { + const string expressionCode = + "return Countries['test'] is 'Test'"; + + var simpra = new Simpra(); + var model = GetTestModel(); + model.Countries = new Dictionary { { "Georgia", "Test" } }; + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + Assert.False(result); + } + + [Fact] + public void CallGetValueFromDictionaryWhenKeyExist_ShouldReturnValue() + { + const string expressionCode = + "return Countries['Georgia'] is 'Test'"; + + var simpra = new Simpra(); + var model = GetTestModel(); + model.Countries = new Dictionary { { "Georgia", "Test" } }; + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + Assert.True(result); + } + + [Fact] + public void ModelWithInheritedClassProperties_ShouldFindPropertyCorrectly() + { + const string expressionCode = + "return Color"; + + var simpra = new Simpra(); + var model = GetTestModel(); + model.Color = Color.Blue; + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + Assert.Equal(Color.Blue, result); + } + + [Fact] + public void ModelWithInheritedInterfaceProperties_ShouldFindPropertyCorrectly() + { + const string expressionCode = + "return Customer.Id"; + + var simpra = new Simpra(); + var model = GetTestModel(); + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + Assert.Equal(1, result); + } + + [Fact] + public void Expression_ReturnNullableEnum_ReturnValueMustBeCorrect() + { + const string expressionCode = + "return NullableEnum"; + + var simpra = new Simpra(); + var model = GetTestModel(); + model.NullableEnum = Color.Green; + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + Assert.NotNull(result); + + model.NullableEnum = null; + result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.Null(result); + } + + [Fact] + public void Expression_ShouldCompareNullableEnum_ReturnValueMustBeCorrect() + { + const string expressionCode = + "return NullableEnum is 'Green'"; + + var simpra = new Simpra(); + var model = GetTestModel(); + model.NullableEnum = Color.Green; + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + Assert.True(result); + } + + [Fact] + public void Expression_NestedDomainPrimitiveType_ShouldReturnCorrectly() + { + const string expressionCode = + "return Transfer.RegulatoryReporting[1].Details[1].Information[1]"; + + var simpra = new Simpra(); + var model = Iso20022TransferModel.CreateForInformation(); + + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.NotNull(result); + } + + [Fact] + public void Expression_NestedDomainPrimitiveType_ShouldCompareCorrectly() + { + const string expressionCode = + "return Transfer.RegulatoryReporting[1].Details[1].Information[1] is 'Information1'"; + + var simpra = new Simpra(); + var model = Iso20022TransferModel.CreateForInformation(); + + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.True(result); + } + + [Fact] + public void ExpressionListOfList_Comparison_ShouldReturnCorrectly() + { + const string expressionCode = + "return ListOfList[2][2] is 4"; + + var simpra = new Simpra(); + var model = new ListModel { EnumList = [Color.Blue, Color.Green], IntegerList = [1, 2, 3], StringList = ["test", "test2"], ListOfList = [[1, 2], [3, 4]] }; + + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.True(result); + } + + [Fact] + public void ListOfList_ShouldReturnCorrectly() + { + const string expressionCode = + "return ListOfList"; + + var simpra = new Simpra(); + var model = new ListModel { EnumList = [Color.Blue, Color.Green], IntegerList = [1, 2, 3], StringList = ["test", "test2"], ListOfList = [[1, 2], [3, 4]] }; + + var result = simpra.Execute>, ListModel, TestFunctions>(model, new TestFunctions(), expressionCode); + + Assert.Equal(4, result[1][1]); + } + + [Fact] + public void GetComplexObject_ShouldReturnCorrectly() + { + const string expressionCode = "return Customer"; + + var simpra = new Simpra(); + var model = GetTestModel(); // Model is irrelevant here + + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.Equal(1, result.Id); + } + + [Fact] + public void EnumerableOfIntegers_ShouldReturnCorrectly() + { + const string expressionCode = + "return IntegerEnumerable"; + + var simpra = new Simpra(); + var model = new ListModel { EnumList = [Color.Blue, Color.Green], IntegerList = [1, 2, 3], StringList = ["test", "test2"], IntegerEnumerable = [1, 2, 3] }; + + var result = simpra.Execute, ListModel, TestFunctions>(model, new TestFunctions(), expressionCode); + + Assert.Equal(2, result.ToList()[1]); + } + + [Fact] + public void ArrayOfIntegers_ShouldReturnCorrectly() + { + const string expressionCode = + "return IntegerArray"; + + var simpra = new Simpra(); + var model = new ListModel { EnumList = [Color.Blue, Color.Green], IntegerList = [1, 2, 3], StringList = ["test", "test2"], IntegerArray = [1, 2, 3] }; + + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.Equal(2, result[1]); + } + + [Fact] + public void ListOfComplexObjects_ShouldReturnCorrectly() + { + const string expressionCode = + "return ComplexList"; + + var simpra = new Simpra(); + var model = new ListModel + { + EnumList = [Color.Blue, Color.Green], + IntegerList = [1, 2, 3], + StringList = ["test", "test2"], + ComplexList = + [ + new Customer { Id = 1, Status = 1 }, + new Customer { Id = 2, Status = 2 } + ] + }; + + var result = simpra.Execute, ListModel, TestFunctions>(model, new TestFunctions(), expressionCode); + + Assert.Equal(2, result[1].Id); + } + + [Fact] + public void ExpressionStringListEqualsValue_ShouldReturnCorrectValues() + { + const string expressionCode = + "return StringList[1] is 'test'"; + + var simpra = new Simpra(); + var model = new ListModel { EnumList = [Color.Blue, Color.Green], IntegerList = [1, 2, 3], StringList = ["test", "test2"] }; + + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.True(result); + } + + [Fact] + public void StringList_ShouldReturnCorrectValues() + { + const string expressionCode = + "return StringList"; + + var simpra = new Simpra(); + var model = new ListModel { EnumList = [Color.Blue, Color.Green], IntegerList = [1, 2, 3], StringList = ["test", "test2"] }; + + var result = simpra.Execute, ListModel, TestFunctions>(model, new TestFunctions(), expressionCode); + + Assert.Equal("test2", result[1]); + } + + [Fact] + public void EnumList_ShouldReturnCorrectValues() + { + const string expressionCode = + "return EnumList"; + + var simpra = new Simpra(); + var model = new ListModel { EnumList = [Color.Blue, Color.Green], IntegerList = [1, 2, 3], StringList = ["test", "test2"] }; + + var result = simpra.Execute, ListModel, TestFunctions>(model, new TestFunctions(), expressionCode); + + Assert.Equal(Color.Green, result[1]); + } + + [Fact] + public void IntegerList_ShouldReturnCorrectValues() + { + const string expressionCode = + "return IntegerList"; + + var simpra = new Simpra(); + var model = new ListModel { EnumList = [Color.Blue, Color.Green], IntegerList = [1, 2, 3], StringList = ["test", "test2"] }; + + var result = simpra.Execute, ListModel, TestFunctions>(model, new TestFunctions(), expressionCode); + Assert.Equal(2, result[1]); + } + + [Fact] + public void Execute_ShouldReturnListOfComplexObject() + { + const string expressionCode = + "return Transfer.RegulatoryReporting"; + + var simpra = new Simpra(); + var model = Iso20022TransferModel.CreateForCountry("FR"); + + var result = simpra.Execute, Iso20022TransferModel, TestFunctions>(model, new TestFunctions(), expressionCode); + Assert.Equal("FR", result[0].Authority.Country); + } + + [Fact] + public void Execute_ShouldReturnComplexObject_WhenAccessedViaIndex() + { + const string expressionCode = + "return Transfer.RegulatoryReporting[1]"; + + var simpra = new Simpra(); + var model = Iso20022TransferModel.CreateForCountry("FR"); + + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + Assert.Equal("FR", result.Authority.Country); + } + + [Fact] + public void Execute_ShouldReturnTrue_WhenAuthorityCountryIsFR() + { + const string expressionCode = + "return Transfer.RegulatoryReporting[1].Authority.Country is 'FR'"; + + var simpra = new Simpra(); + var model = Iso20022TransferModel.CreateForCountry("FR"); + + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + Assert.True(result); + } + + [Fact] + public void Expression_Should_ReturnFalse_When_IndexIsOutOfRangeAndValueCompared() + { + const string expressionCode = + "return Transfer.A[10] is 1"; + + var simpra = new Simpra(); + var model = GetTestModel(); + model.Transfer!.A = [1, 2, 3]; + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + Assert.False(result); + } + + [Fact] + public void Expression_Should_ReturnFalse_When_NestedListIsUsed() + { + const string expressionCode = + "return Transfer.OuterList[10].InnerList[1] is 1"; + + var simpra = new Simpra(); + var model = GetTestModel(); + model.Transfer!.A = [1, 2, 3]; + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + Assert.False(result); + } + + [Fact] + public void Expression_Should_ReturnFalse_When_NestedListIsUsedX() + { + const string expressionCode = + "return CustomerList[1] has value"; + + var simpra = new Simpra(); + var model = GetTestModel(); + model.CustomerList = new List(); + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + Assert.False(result); + } + + [Fact] + public void Expression_Should_ReturnFalse_When_ArrayIsNullAndValueCompared() + { + const string expressionCode = + "return Transfer.A[1] is 1"; + + var simpra = new Simpra(); + var model = GetTestModel(); + model.Transfer = null; + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + Assert.False(result); + } + + [Fact] + public void Expression_Should_ReturnDefault_When_ArrayIsNull() + { + const string expressionCode = + "return Transfer.A[10]"; + + var simpra = new Simpra(); + var model = GetTestModel(); + model.Transfer = null; + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + Assert.Equal(0, result); + } + + [Fact] + public void Expression_Should_ReturnFalse_When_TheValueIsNull() + { + const string expressionCode = + "return Transfer.Customer.Id is 1"; + + var simpra = new Simpra(); + var model = GetTestModel(); + model.Transfer = null; + + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.False(result); + } + + [Fact] + public void ExecuteExpression_Should_ReturnFalse_When_Nint1HasNoValue() + { + const string expressionCode = + """ + let X = CustomerId + let Y = Nint1 + return Y has value + """; + + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.False(result); + } + + [Fact] + public void ExecuteExpression_WithNullCheck_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new TestModelMain { Test = null }; + const string expression = "return Test has value"; + + var result = simpra.Execute(model, new TestFunctions(), expression); + + Assert.False(result); + } + + [Fact] + public void ExecuteExpression_Should_ReturnTrue_When_PropertyIsEnum() + { + const string expression = " return Test.Test is 'Test1';"; + var simpra = new Simpra(); + var model = new TestModelMain { Test = new TestModel1 { Test = TestModel2.Test1 } }; + var result = simpra.Execute(model, new TestFunctions(), expression); + + Assert.True(result); + } + + [Fact] + public void ExecuteWitNullableEnum_ShouldReturnValue() + { + const string expressionCode = "return Nint1"; + + var simpra = new Simpra(); + var model = GetTestModel(); // Model is irrelevant here + model.Nint1 = 1; + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + Assert.Equal(1, result); + } + + [Fact] + public void Execute_WithValidColorReference_ShouldReturnGreen() + { + const string expressionCode = "return Color"; + + var simpra = new Simpra(); + var model = GetTestModel(); // Model is irrelevant here + + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.Equal("Green", result); + } + + [Fact] + public void Execute_WithUnknownColorReference_ShouldReturnGreen() + { + const string expressionCode = "return ColorX"; + + var simpra = new Simpra(); + var model = GetTestModel(); // Model is irrelevant here + + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.Equal("Green", result); + } + + [Fact] + public void ExecuteSyntax_WithArithmeticOperations_ShouldReturnCorrectValue() + { + const string expressionCode = "return ColorM"; + + var simpra = new Simpra(); + var model = GetTestModel(); // Model is irrelevant here + + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.Equal("Green", result); + } +} diff --git a/tests/AltaSoft.Simpra.tests/CompositeExpressionTests.cs b/tests/AltaSoft.Simpra.tests/CompositeExpressionTests.cs new file mode 100644 index 0000000..a546642 --- /dev/null +++ b/tests/AltaSoft.Simpra.tests/CompositeExpressionTests.cs @@ -0,0 +1,263 @@ +using AltaSoft.Simpra.Tests.Models; +using static AltaSoft.Simpra.Tests.Models.TestModelFactory; + +namespace AltaSoft.Simpra.Tests; + +public class CompositeExpressionTests +{ + [Fact] + public void Expression_Should_ReturnMultipleValues_When_AggregateListValues() + { + const string expressionCode = + """ + let values = [1, 2, 3, 4, 5] + let sum = sum(values) + let avg = sum(values) / length(values) + return sum + avg + """; + + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.Equal(18m, result); + } + + [Fact] + public void Expression_Should_ReturnTrue_When_AmountIsGreaterThanMaxOfList() + { + const string expressionCode = + """ + let amounts = [10, 20, 30, 40] + let maxAmount = amounts[3] + return Transfer.Amount > maxAmount + """; + + var simpra = new Simpra(); + var model = GetTestModel(); + model.Transfer!.Amount = 50; + + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.True(result); + } + + [Fact] + public void Expression_Should_ReturnTrue_When_CheckingForNullValue() + { + const string expressionCode = + """ + let transfer = Transfer + return transfer.Currency has value + """; + + var simpra = new Simpra(); + var model = GetTestModel(); + model.Transfer!.Currency = "USD"; + + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.True(result); + } + + [Fact] + public void Expression_Should_ReturnTrue_When_AmountInCurrencyIsGreaterThanThresholdAndMatchesPattern() + { + const string expressionCode = + """ + let transfer = Transfer + let amount = transfer.Amount + let ccy = transfer.Currency + return amount > 100 and (ccy matches '^[A-Z]{3}$') and ccy is 'USD' + """; + + var simpra = new Simpra(); + var model = GetTestModel(); + model.Transfer!.Amount = 150; + model.Transfer.Currency = "USD"; + + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.True(result); + } + + [Fact] + public void Expression_Should_ReturnFalse_When_DividingByZeroHandledProperly() + { + const string expressionCode = + """ + let transfer = Transfer + let amount = transfer.Amount + let divisor = 0 + let safeDivision = when divisor is not 0 then amount / divisor else 0 end + return safeDivision is 0 + """; + + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.True(result); + } + + [Fact] + public void Expression_Should_ReturnTrue_When_DynamicStringListContainsMatchingItem() + { + const string expressionCode = + """ + let dynamicList = ListSomeCountries('countries') + return 'RU' in dynamicList or 'US' in dynamicList + """; + + var simpra = new Simpra(); + var model = GetTestModel(); + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.True(result); + } + + [Fact] + public void Expression_Should_ReturnTrue_When_DynamicIntListContainsMatchingItem() + { + const string expressionCode = + """ + let dynamicList = ListOfCustomerIds('Good') + return 1 in dynamicList + """; + + var simpra = new Simpra(); + var model = GetTestModel(); + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.True(result); + } + + [Fact] + public void Expression_Should_ReturnFalse_When_AmountIsNegativeAndCurrencyIsEUR() + { + const string expressionCode = + """ + let transfer = Transfer + let amount = transfer.Amount + let ccy = transfer.Currency + return amount < 0 and ccy is 'EUR' + """; + + var simpra = new Simpra(); + var model = GetTestModel(); + model.Transfer!.Amount = -50; + model.Transfer.Currency = "EUR"; + + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.True(result); + } + + [Fact] + public void Expression_Should_ReturnTrue_When_SomeComplexConditionIsMet() + { + const string expressionCode = + """ + let x = 100 + let y = 'USD' + let transfer = Transfer + let amount = transfer.Amount + return amount < 500 and y is 'USD' + """; + + var simpra = new Simpra(); + var model = GetTestModel(); + model.Transfer!.Amount = 100; + + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.True(result); + } + + [Fact] + public void Expression_Should_ReturnComplexCalculation_When_CombinedWithArithmetic() + { + const string expressionCode = + """ + let amount = Transfer.Amount + let fee = amount * 0.05 + let totalAmount = amount - fee + return totalAmount + """; + + var simpra = new Simpra(); + var model = GetTestModel(); + model.Transfer!.Amount = 200; + + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.Equal(190, result); + } + + [Fact] + public void Expression_Should_ReturnTrue_When_AmountIsWithinRange_And_CurrencyIsValid_And_AmountIncreasedByCustomLogic() + { + const string expressionCode = + """ + let transfer = Transfer + let amount = transfer.Amount + let currency = transfer.Currency + let threshold = 500 + let validCurrencies = ['USD', 'EUR', 'GBP'] + let increaseAmountByPercentage = amount + (amount * (5 / 100)) + let increasedAmount = when currency in validCurrencies then increaseAmountByPercentage else amount end + return increasedAmount > 180 and increasedAmount < threshold + """; + + var simpra = new Simpra(); + var model = GetTestModel(); + model.Transfer!.Amount = 180; + model.Transfer.Currency = "USD"; + + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.True(result); + } + + [Fact] + public void Expression_Should_ReturnCorrectResult_When_NestedListOperationsAndStringManipulationsAreUsed() + { + const string expressionCode = + """ + let items = ['USD', 'EUR', 'LIRA'] + let priceList = [2.8, 3.0, 0.75] + let targetItem = 'USD' + let targetPrice = when targetItem in items then 3 else 2 end + let discountedPrice = when targetPrice > 1.0 then targetPrice * 0.9 else targetPrice end + let result = targetItem + ' costs ' + discountedPrice + return result + """; + + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.Equal("USD costs 2.7", result); + } + + [Fact] + public void ExecuteExpression_Should_ReturnSumOfXAndY_When_XyPropertiesAreUsed() + { + const string expressionCode = + """ + let X = Xy.X + let Y = Xy.Y + return X + Y + """; + + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.Equal(3, result); + } +} diff --git a/tests/AltaSoft.Simpra.tests/ControlFlowTests.cs b/tests/AltaSoft.Simpra.tests/ControlFlowTests.cs new file mode 100644 index 0000000..65b1388 --- /dev/null +++ b/tests/AltaSoft.Simpra.tests/ControlFlowTests.cs @@ -0,0 +1,224 @@ +using AltaSoft.Simpra.Tests.Models; +using static AltaSoft.Simpra.Tests.Models.TestModelFactory; + +namespace AltaSoft.Simpra.Tests; + +public class ControlFlowTests +{ + [Fact] + public void Expression_Should_EvaluateNestedWhenConditionAndReturnCorrectResult() + { + const string expressionCode = + """ + let amount = Transfer.Amount + let result = when amount < 50 then 'Low' + when amount >= 50 and amount < 200 then 'Medium' + else 'High' + return result + """; + + var simpra = new Simpra(); + var model = GetTestModel(); + model.Transfer!.Amount = 150; + + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.Equal("Medium", result); + } + + [Fact] + public void Expression_Should_ReturnEmptyString_When_StringContainsNoMatch() + { + const string expressionCode = + """ + let str = 'foobar' + return when str like 'f%' then str else '' + """; + + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.Equal("foobar", result); + } + + [Fact] + public void Expression_Should_HandleLargeNestedCondition() + { + const string expressionCode = + """ + let x = Transfer.Amount + let y = Transfer.Currency + return when x < 50 then 'Low' + when x >= 50 and x <= 150 then 'Medium' + when x > 150 and y is 'USD' then 'High - USD' + when x > 150 and y is 'EUR' then 'High - EUR' + else 'Unknown' + """; + + var simpra = new Simpra(); + var model = GetTestModel(); + model.Transfer!.Amount = 200; + model.Transfer.Currency = "USD"; + + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.Equal("High - USD", result); + } + + [Fact] + public void ExecuteExpression_Should_ReturnCorrectValue_FromNestedIfWithElse() + { + const string expressionCode = + """ + let x = Transfer.Amount + if x > 7 then + return 10 + else if x > 5 then + return 6 + else if x > 1 then + return 2 + else + return round(1000) + end + """; + + var simpra = new Simpra(); + var model = GetTestModel(); + + model.Transfer!.Amount = 8; + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + Assert.Equal(10, result); + + model.Transfer!.Amount = 6; + result = simpra.Execute(model, new TestFunctions(), expressionCode); + Assert.Equal(6, result); + + model.Transfer!.Amount = 2; + result = simpra.Execute(model, new TestFunctions(), expressionCode); + Assert.Equal(2, result); + + model.Transfer!.Amount = -1; + result = simpra.Execute(model, new TestFunctions(), expressionCode); + Assert.Equal(1000, result); + } + + [Fact] + public void ExecuteExpression_Should_ReturnCorrectValue_FromNestedIfWithoutElse() + { + const string expressionCode = + """ + let x = Transfer.Amount + if x > 7 then + return 10 + if x > 5 then + return 6 + if x > 1 then + return 2 + else + return 1000 + end + """; + + var simpra = new Simpra(); + var model = GetTestModel(); + model.Transfer!.Amount = 8; + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + Assert.Equal(10, result); + + model.Transfer!.Amount = 6; + result = simpra.Execute(model, new TestFunctions(), expressionCode); + Assert.Equal(6, result); + + model.Transfer!.Amount = 2; + result = simpra.Execute(model, new TestFunctions(), expressionCode); + Assert.Equal(2, result); + + model.Transfer!.Amount = -1; + result = simpra.Execute(model, new TestFunctions(), expressionCode); + Assert.Equal(1000, result); + } + + [Fact] + public void ExecuteExpression_Should_ReturnFirstReturnValue_When_MultipleReturnStatements() + { + var simpra = new Simpra(); + var model = GetTestModel(); + + const string expressionCode = + """ + return 10 + return 20 + """; + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + Assert.Equal(10, result); + } + + [Fact] + public void ExecuteExpression_WithMultipleReturnStatements_ShouldReturnLastValue() + { + var simpra = new Simpra(); + var model = new TestModelMain { Test = null }; + const string expression = """ + return 10 + return true + """; + + var result = simpra.Execute(model, new TestFunctions(), expression); + + Assert.True(result); + } + + [Fact] + public void Expression_Should_ReturnTwo_When_XIsSixty() + { + const string expressionCode = + """ + let X = 50 + 10.0 + let Y = when X > 100 then 1 when X > 10 then 2 else 0 + return Y + """; + + var simpra = new Simpra(); + var model = GetTestModel(); // Model is irrelevant here + + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.Equal(2, result); + } + + [Fact] + public void Expression_Should_ReturnThree_When_bIsThirty() + { + const string expressionCode = + """ + let b = 30; + let x = 1; + return when b is 1 then 1 else 3 end + """; + + var simpra = new Simpra(); + var model = GetTestModel(); // Model is irrelevant here + + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.Equal(3, result); + } + + [Fact] + public void Expression_Should_ReturnOne_When_XIsNotFive() + { + const string expressionCode = + """ + let X = '50' + return when X is not '5' then 1 else 0 end + """; + + var simpra = new Simpra(); + var model = GetTestModel(); // Model is irrelevant here + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.Equal(1, result); + } +} diff --git a/tests/AltaSoft.Simpra.tests/ErrorHandlingTests.cs b/tests/AltaSoft.Simpra.tests/ErrorHandlingTests.cs new file mode 100644 index 0000000..c39a448 --- /dev/null +++ b/tests/AltaSoft.Simpra.tests/ErrorHandlingTests.cs @@ -0,0 +1,102 @@ +using AltaSoft.Simpra.Tests.Models; +using static AltaSoft.Simpra.Tests.Models.TestModelFactory; + +namespace AltaSoft.Simpra.Tests; + +public class ErrorHandlingTests +{ + [Fact] + public void InvalidSimpraSyntax_ShouldThrowException_WhenIncorrectAndSignIsUsedAndReturnStatement() + { + const string expressionCode = + " return Amount is 100 && Amount is 200"; + + var simpra = new Simpra(); + var model = GetTestModel(); + + // ReSharper disable ConvertToLocalFunction + var f = () => simpra.Execute(model, new TestFunctions(), expressionCode); + // ReSharper restore ConvertToLocalFunction + Assert.Throws(() => f()); + } + + [Fact] + public void InvalidSimpraSyntax_ShouldThrowException_WhenIncorrectAndSignIsUsed() + { + const string expressionCode = + " Amount is 100 && Amount is 200"; + + var simpra = new Simpra(); + var model = GetTestModel(); + + // ReSharper disable ConvertToLocalFunction + var f = () => simpra.Execute(model, new TestFunctions(), expressionCode); + // ReSharper restore ConvertToLocalFunction + Assert.Throws(() => f()); + } + + [Fact] + public void InvalidSimpraSyntax_ShouldThrowException_WithoutReturn() + { + const string expressionCode = + " 111 Amount is 100"; + + var simpra = new Simpra(); + var model = GetTestModel(); + + // ReSharper disable ConvertToLocalFunction + var f = () => simpra.Execute(model, new TestFunctions(), expressionCode); + // ReSharper restore ConvertToLocalFunction + Assert.Throws(() => f()); + } + + [Fact] + public void InvalidSimpraSyntax_ShouldThrowException_WithReturn() + { + const string expressionCode = + "return 111 Amount is 100"; + + var simpra = new Simpra(); + var model = GetTestModel(); + + // ReSharper disable ConvertToLocalFunction + var f = () => simpra.Execute(model, new TestFunctions(), expressionCode); + // ReSharper restore ConvertToLocalFunction + Assert.Throws(() => f()); + } + + [Fact] + public void Division_ByZero_ShouldThrowDivideByZeroException() + { + var simpra = new Simpra(); + var model = GetTestModel(); + + var f = () => simpra.Execute(model, new TestFunctions(), "return 5 / 0"); + + Assert.Throws(() => f()); + } + + [Fact] + public void CallingUndefinedFunction_ShouldThrowSimpraException() + { + const string expressionCode = "return NonExistentFunction()"; + + var simpra = new Simpra(); + var model = GetTestModel(); + + var f = () => simpra.Execute(model, new TestFunctions(), expressionCode); + Assert.Throws(() => f()); + } + + [Fact] + public void AccessingUndefinedProperty_ShouldThrowSimpraException() + { + const string expressionCode = "return NonExistentProperty"; + + var simpra = new Simpra(); + var model = GetTestModel(); + + var f = () => simpra.Execute(model, new TestFunctions(), expressionCode); + Assert.Throws(() => f()); + } +} diff --git a/tests/AltaSoft.Simpra.tests/FunctionCallTests.cs b/tests/AltaSoft.Simpra.tests/FunctionCallTests.cs new file mode 100644 index 0000000..5fbbd76 --- /dev/null +++ b/tests/AltaSoft.Simpra.tests/FunctionCallTests.cs @@ -0,0 +1,97 @@ +using AltaSoft.Simpra.Tests.Models; +using static AltaSoft.Simpra.Tests.Models.TestModelFactory; + +namespace AltaSoft.Simpra.Tests; + +public class FunctionCallTests +{ + [Fact] + public void CallInterfaceFunctionFromSimpra_ShouldReturnCorrectValue() + { + const string expressionCode = + "return Upper('test')"; + + var simpra = new Simpra(); + var model = GetTestModel(); + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + Assert.Equal("TEST", result); + } + + [Fact] + public void CallBaseInterfaceFunctionFromSimpra_ShouldReturnCorrectValue() + { + const string expressionCode = + "return Lower('TEST')"; + + var simpra = new Simpra(); + var model = GetTestModel(); + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + Assert.Equal("test", result); + } + + [Fact] + public void CallBaseStaticFunctionFromSimpra_ShouldReturnCorrectValue() + { + const string expressionCode = + "return CallBaseStaticMethod()"; + + var simpra = new Simpra(); + var model = GetTestModel(); + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + Assert.Equal("BaseStaticMethod", result); + } + + [Fact] + public void CallBaseFunctionFromSimpra_ShouldReturnCorrectValue() + { + const string expressionCode = + "return CallBaseMethod()"; + + var simpra = new Simpra(); + var model = GetTestModel(); + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + Assert.Equal("BaseMethod", result); + } + + [Fact] + public void CallFunctionFromSimpra() + { + const string expressionCode = + "return ListSomeCountries('GE')"; + + var simpra = new Simpra(); + var model = GetTestModel(); + var result = simpra.Execute, TestModel, TestFunctions>(model, new TestFunctions(), expressionCode); + Assert.True(result.SequenceEqual(["RU", "BE", "GE"])); + } + + [Fact] + public void Expression_Should_HandleNestedFunctionCallsCorrectly() + { + const string expressionCode = + """ + let str = 'hello world' + let upperStr = Upper(str) + return Lower(upperStr) is str + """; + + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.True(result); + } + + [Fact] + public void CallExternalFunction_WithNonDecimalNumericParameter_ShouldSelectOverloadAndConvertArgument() + { + const string expressionCode = "return DescribeAsInt(7)"; + + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + Assert.Equal("int:7", result); + } +} diff --git a/tests/AltaSoft.Simpra.tests/Models/TestFunctions.cs b/tests/AltaSoft.Simpra.tests/Models/TestFunctions.cs new file mode 100644 index 0000000..09bf8ad --- /dev/null +++ b/tests/AltaSoft.Simpra.tests/Models/TestFunctions.cs @@ -0,0 +1,56 @@ +namespace AltaSoft.Simpra.Tests.Models; + +public class BaseFunctions +{ + public static string CallBaseStaticMethod() => "BaseStaticMethod"; + + public static string CallBaseMethod() => "BaseMethod"; +} + +public interface IBaseFunctions +{ + string Lower(string str); +} + +public interface IFunctions : IBaseFunctions +{ + string Upper(string str); +} + +public class TestFunctions : BaseFunctions, IFunctions +{ + // ReSharper disable UnusedMember.Global + public static string[] ListSomeCountries(string key) + { + return ["RU", "BE", key]; + } + + public static int[] ListOfCustomerIds(string key) + { + return [1, 2]; + } + +#pragma warning disable S2325 + public string Upper(string str) => str.ToUpper(); + + public string Lower(string str) => str.ToLower(); + + public ValueTask> ListOfCurrencyCodes(string name) => ValueTask.FromResult(new List() { "EUR", "GEL" }); + + public ValueTask ComputeAsync(decimal a, decimal b) => ValueTask.FromResult(a + b); + + public static string DescribeAsInt(int value) => $"int:{value}"; + + public string[] List(string key) + { + return ["RU", "BE"]; + } + + public ValueTask BigListAsync(string key, CancellationToken cancellationToken) +#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously + { + return ValueTask.FromResult(new[] { "RU", "BE" }); + } +#pragma warning restore S2325 + // ReSharper restore UnusedMember.Global +} diff --git a/tests/AltaSoft.Simpra.tests/Models/TestModelFactory.cs b/tests/AltaSoft.Simpra.tests/Models/TestModelFactory.cs new file mode 100644 index 0000000..cf802cc --- /dev/null +++ b/tests/AltaSoft.Simpra.tests/Models/TestModelFactory.cs @@ -0,0 +1,9 @@ +namespace AltaSoft.Simpra.Tests.Models; + +internal static class TestModelFactory +{ + public static TestModel GetTestModel() + { + return new TestModel { Transfer = new Transfer { Amount = 100, Currency = "USD" }, Customer = new Customer { Id = 1, Status = 1 }, Remittance = "Test" }; + } +} diff --git a/tests/AltaSoft.Simpra.tests/NullSafetyTests.cs b/tests/AltaSoft.Simpra.tests/NullSafetyTests.cs index caa5f1a..cf4d398 100644 --- a/tests/AltaSoft.Simpra.tests/NullSafetyTests.cs +++ b/tests/AltaSoft.Simpra.tests/NullSafetyTests.cs @@ -11,8 +11,8 @@ 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(), + var result = simpra.Execute( + model, new TestFunctions(), "DebtorAccount.Properties[\"AccType\"] is \"200\""); Assert.False(result); @@ -24,8 +24,8 @@ 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(), + var result = simpra.Execute( + model, new TestFunctions(), "DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Properties[\"AccSubType\"] is \"8\""); Assert.False(result); @@ -37,8 +37,8 @@ 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(), + var result = simpra.Execute( + model, new TestFunctions(), "DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Properties[\"AccSubType\"] is \"8\""); Assert.False(result); @@ -50,8 +50,8 @@ public void NullDictionary_ReturnValue_ShouldReturnNull() var simpra = new Simpra(); var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() }; - var result = simpra.Execute( - model, new SimpraExpressionTests.TestFunctions(), + var result = simpra.Execute( + model, new TestFunctions(), "return DebtorAccount.Properties[\"AccType\"]"); Assert.Null(result); @@ -63,8 +63,8 @@ public void NullDictionary_HasValue_ShouldReturnFalse() var simpra = new Simpra(); var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() }; - var result = simpra.Execute( - model, new SimpraExpressionTests.TestFunctions(), + var result = simpra.Execute( + model, new TestFunctions(), "DebtorAccount.Properties[\"AccType\"] has value"); Assert.False(result); @@ -84,8 +84,8 @@ public void NonNullDictionary_MissingKey_ShouldReturnFalse() } }; - var result = simpra.Execute( - model, new SimpraExpressionTests.TestFunctions(), + var result = simpra.Execute( + model, new TestFunctions(), "DebtorAccount.Properties[\"AccType\"] is \"200\""); Assert.False(result); @@ -103,8 +103,8 @@ public void NonNullDictionary_ExistingKey_ShouldReturnTrue() } }; - var result = simpra.Execute( - model, new SimpraExpressionTests.TestFunctions(), + var result = simpra.Execute( + model, new TestFunctions(), "DebtorAccount.Properties[\"AccType\"] is \"200\""); Assert.True(result); @@ -122,8 +122,8 @@ public void NonNullDictionary_ExistingKey_ReturnValue() } }; - var result = simpra.Execute( - model, new SimpraExpressionTests.TestFunctions(), + var result = simpra.Execute( + model, new TestFunctions(), "return DebtorAccount.Properties[\"AccType\"]"); Assert.Equal("200", result); @@ -141,8 +141,8 @@ public void NonNullDictionary_KeyWithNullValue_HasValue_ShouldReturnFalse() } }; - var result = simpra.Execute( - model, new SimpraExpressionTests.TestFunctions(), + var result = simpra.Execute( + model, new TestFunctions(), "DebtorAccount.Properties[\"AccType\"] has value"); Assert.False(result); @@ -156,8 +156,8 @@ 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(), + var result = simpra.Execute( + model, new TestFunctions(), "return DebtorAccount.CustomerMap[\"key\"].Id"); Assert.Equal(0, result); @@ -169,8 +169,8 @@ 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(), + var result = simpra.Execute( + model, new TestFunctions(), "DebtorAccount.CustomerMap[\"key\"].Id is 1"); Assert.False(result); @@ -190,8 +190,8 @@ public void NonNullDictionary_ComplexValueType_ExistingKey_ShouldReturnCorrectVa } }; - var result = simpra.Execute( - model, new SimpraExpressionTests.TestFunctions(), + var result = simpra.Execute( + model, new TestFunctions(), "return DebtorAccount.CustomerMap[\"vip\"].Id"); Assert.Equal(42, result); @@ -209,8 +209,8 @@ public void NonNullDictionary_ComplexValueType_MissingKey_ShouldReturnDefault() } }; - var result = simpra.Execute( - model, new SimpraExpressionTests.TestFunctions(), + var result = simpra.Execute( + model, new TestFunctions(), "return DebtorAccount.CustomerMap[\"unknown\"].Id"); Assert.Equal(0, result); @@ -227,8 +227,8 @@ public void NullParentObject_DictionaryAccess_ShouldReturnFalse() DebtorAccount = new DebtorAccount { Nested = null } }; - var result = simpra.Execute( - model, new SimpraExpressionTests.TestFunctions(), + var result = simpra.Execute( + model, new TestFunctions(), "DebtorAccount.Nested.Properties[\"Key\"] is \"value\""); Assert.False(result); @@ -243,8 +243,8 @@ public void NullParentObject_DictionaryAccess_ReturnValue_ShouldReturnNull() DebtorAccount = new DebtorAccount { Nested = null } }; - var result = simpra.Execute( - model, new SimpraExpressionTests.TestFunctions(), + var result = simpra.Execute( + model, new TestFunctions(), "return DebtorAccount.Nested.Properties[\"Key\"]"); Assert.Null(result); @@ -264,8 +264,8 @@ public void NestedObject_NullDictionary_ShouldReturnFalse() } }; - var result = simpra.Execute( - model, new SimpraExpressionTests.TestFunctions(), + var result = simpra.Execute( + model, new TestFunctions(), "DebtorAccount.Nested.Properties[\"Key\"] is \"value\""); Assert.False(result); @@ -286,8 +286,8 @@ public void NestedObject_NonNullDictionary_ExistingKey_ShouldReturnTrue() } }; - var result = simpra.Execute( - model, new SimpraExpressionTests.TestFunctions(), + var result = simpra.Execute( + model, new TestFunctions(), "DebtorAccount.Nested.Properties[\"Key\"] is \"value\""); Assert.True(result); @@ -304,8 +304,8 @@ public void NullList_IndexAccess_ReturnValue_ShouldReturnNull() DebtorAccount = new DebtorAccount { Tags = null } }; - var result = simpra.Execute( - model, new SimpraExpressionTests.TestFunctions(), + var result = simpra.Execute( + model, new TestFunctions(), "return DebtorAccount.Tags[1]"); Assert.Null(result); @@ -320,8 +320,8 @@ public void NullList_IndexAccess_Comparison_ShouldReturnFalse() DebtorAccount = new DebtorAccount { Tags = null } }; - var result = simpra.Execute( - model, new SimpraExpressionTests.TestFunctions(), + var result = simpra.Execute( + model, new TestFunctions(), "DebtorAccount.Tags[1] is \"hello\""); Assert.False(result); @@ -338,8 +338,8 @@ public void NullProperty_MemberAccess_Comparison_ShouldReturnFalse() DebtorAccount = new DebtorAccount { Name = null } }; - var result = simpra.Execute( - model, new SimpraExpressionTests.TestFunctions(), + var result = simpra.Execute( + model, new TestFunctions(), "DebtorAccount.Name is \"Test\""); Assert.False(result); @@ -354,8 +354,8 @@ public void NullProperty_MemberAccess_HasValue_ShouldReturnFalse() DebtorAccount = new DebtorAccount { Name = null } }; - var result = simpra.Execute( - model, new SimpraExpressionTests.TestFunctions(), + var result = simpra.Execute( + model, new TestFunctions(), "DebtorAccount.Name has value"); Assert.False(result); @@ -370,8 +370,8 @@ public void NonNullProperty_MemberAccess_HasValue_ShouldReturnTrue() DebtorAccount = new DebtorAccount { Name = "Test" } }; - var result = simpra.Execute( - model, new SimpraExpressionTests.TestFunctions(), + var result = simpra.Execute( + model, new TestFunctions(), "DebtorAccount.Name has value"); Assert.True(result); @@ -388,8 +388,8 @@ public void NullDictionary_And_NonNullProperty_MixedExpression_ShouldReturnFalse DebtorAccount = new DebtorAccount { Name = "Test" } }; - var result = simpra.Execute( - model, new SimpraExpressionTests.TestFunctions(), + var result = simpra.Execute( + model, new TestFunctions(), "DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Name is \"Test\""); Assert.False(result); @@ -404,8 +404,8 @@ public void NullDictionary_Or_NonNullProperty_MixedExpression_ShouldReturnTrue() DebtorAccount = new DebtorAccount { Name = "Test" } }; - var result = simpra.Execute( - model, new SimpraExpressionTests.TestFunctions(), + var result = simpra.Execute( + model, new TestFunctions(), "DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Name is \"Test\""); Assert.True(result); @@ -419,8 +419,8 @@ public void NullDictionary_IsNotComparison_ShouldReturnTrue() var simpra = new Simpra(); var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() }; - var result = simpra.Execute( - model, new SimpraExpressionTests.TestFunctions(), + var result = simpra.Execute( + model, new TestFunctions(), "DebtorAccount.Properties[\"AccType\"] is not \"200\""); Assert.True(result); @@ -434,8 +434,8 @@ public void NullDictionary_InOperator_ShouldReturnFalse() var simpra = new Simpra(); var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() }; - var result = simpra.Execute( - model, new SimpraExpressionTests.TestFunctions(), + var result = simpra.Execute( + model, new TestFunctions(), "DebtorAccount.Properties[\"AccType\"] in [\"200\", \"300\"]"); Assert.False(result); @@ -449,8 +449,8 @@ public void NullDictionary_WhenExpression_ShouldEvaluateElseBranch() var simpra = new Simpra(); var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() }; - var result = simpra.Execute( - model, new SimpraExpressionTests.TestFunctions(), + var result = simpra.Execute( + model, new TestFunctions(), "return when DebtorAccount.Properties[\"AccType\"] is \"200\" then \"match\" else \"no match\""); Assert.Equal("no match", result); @@ -470,8 +470,8 @@ public void TestModel_NullCountriesDictionary_ShouldReturnFalse() Countries = null }; - var result = simpra.Execute( - model, new SimpraExpressionTests.TestFunctions(), + var result = simpra.Execute( + model, new TestFunctions(), "Countries[\"Georgia\"] is \"Test\""); Assert.False(result); @@ -489,8 +489,8 @@ public void TestModel_NullCountriesDictionary_ReturnValue_ShouldReturnNull() Countries = null }; - var result = simpra.Execute( - model, new SimpraExpressionTests.TestFunctions(), + var result = simpra.Execute( + model, new TestFunctions(), "return Countries[\"Georgia\"]"); Assert.Null(result); @@ -508,8 +508,8 @@ public void TestModel_NullDictionaryOfObjects_PropertyAccess_ShouldReturnDefault DictionaryOfObjects = null }; - var result = simpra.Execute( - model, new SimpraExpressionTests.TestFunctions(), + var result = simpra.Execute( + model, new TestFunctions(), "return DictionaryOfObjects[\"test\"].Id"); Assert.Equal(0, result); @@ -527,8 +527,8 @@ public void TestModel_NullDictionaryOfObjects_Comparison_ShouldReturnFalse() DictionaryOfObjects = null }; - var result = simpra.Execute( - model, new SimpraExpressionTests.TestFunctions(), + var result = simpra.Execute( + model, new TestFunctions(), "DictionaryOfObjects[\"test\"].Id is 1"); Assert.False(result); @@ -547,8 +547,8 @@ public void NullTransfer_PropertyAccess_ShouldReturnDefault() Remittance = "Test" }; - var result = simpra.Execute( - model, new SimpraExpressionTests.TestFunctions(), + var result = simpra.Execute( + model, new TestFunctions(), "return Transfer.Amount"); Assert.Equal(0m, result); @@ -565,8 +565,8 @@ public void NullTransfer_CurrencyComparison_ShouldReturnFalse() Remittance = "Test" }; - var result = simpra.Execute( - model, new SimpraExpressionTests.TestFunctions(), + var result = simpra.Execute( + model, new TestFunctions(), "Transfer.Currency is \"USD\""); Assert.False(result); @@ -583,8 +583,8 @@ public void NullTransfer_HasValue_ShouldReturnFalse() Remittance = "Test" }; - var result = simpra.Execute( - model, new SimpraExpressionTests.TestFunctions(), + var result = simpra.Execute( + model, new TestFunctions(), "Transfer has value"); Assert.False(result); @@ -605,8 +605,8 @@ public void BothDictsPopulated_And_BothMatch_ShouldReturnTrue() } }; - var result = simpra.Execute( - model, new SimpraExpressionTests.TestFunctions(), + var result = simpra.Execute( + model, new TestFunctions(), "DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Attributes[\"Region\"] is \"EU\""); Assert.True(result); @@ -625,8 +625,8 @@ public void BothDictsPopulated_And_OneMismatch_ShouldReturnFalse() } }; - var result = simpra.Execute( - model, new SimpraExpressionTests.TestFunctions(), + var result = simpra.Execute( + model, new TestFunctions(), "DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Attributes[\"Region\"] is \"EU\""); Assert.False(result); @@ -645,8 +645,8 @@ public void BothDictsPopulated_Or_OneMismatch_ShouldReturnTrue() } }; - var result = simpra.Execute( - model, new SimpraExpressionTests.TestFunctions(), + var result = simpra.Execute( + model, new TestFunctions(), "DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Attributes[\"Region\"] is \"EU\""); Assert.True(result); @@ -665,8 +665,8 @@ public void BothDictsPopulated_Or_BothMismatch_ShouldReturnFalse() } }; - var result = simpra.Execute( - model, new SimpraExpressionTests.TestFunctions(), + var result = simpra.Execute( + model, new TestFunctions(), "DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Attributes[\"Region\"] is \"EU\""); Assert.False(result); @@ -685,8 +685,8 @@ public void PropertiesNull_AttributesPopulated_And_ShouldReturnFalse() } }; - var result = simpra.Execute( - model, new SimpraExpressionTests.TestFunctions(), + var result = simpra.Execute( + model, new TestFunctions(), "DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Attributes[\"Region\"] is \"EU\""); Assert.False(result); @@ -705,8 +705,8 @@ public void PropertiesNull_AttributesPopulated_Or_ShouldReturnTrue() } }; - var result = simpra.Execute( - model, new SimpraExpressionTests.TestFunctions(), + var result = simpra.Execute( + model, new TestFunctions(), "DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Attributes[\"Region\"] is \"EU\""); Assert.True(result); @@ -725,8 +725,8 @@ public void PropertiesPopulated_AttributesNull_And_ShouldReturnFalse() } }; - var result = simpra.Execute( - model, new SimpraExpressionTests.TestFunctions(), + var result = simpra.Execute( + model, new TestFunctions(), "DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Attributes[\"Region\"] is \"EU\""); Assert.False(result); @@ -745,8 +745,8 @@ public void PropertiesPopulated_AttributesNull_Or_ShouldReturnTrue() } }; - var result = simpra.Execute( - model, new SimpraExpressionTests.TestFunctions(), + var result = simpra.Execute( + model, new TestFunctions(), "DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Attributes[\"Region\"] is \"EU\""); Assert.True(result); @@ -758,8 +758,8 @@ public void BothDictsNull_And_ShouldReturnFalse() var simpra = new Simpra(); var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() }; - var result = simpra.Execute( - model, new SimpraExpressionTests.TestFunctions(), + var result = simpra.Execute( + model, new TestFunctions(), "DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Attributes[\"Region\"] is \"EU\""); Assert.False(result); @@ -771,8 +771,8 @@ public void BothDictsNull_Or_ShouldReturnFalse() var simpra = new Simpra(); var model = new DebtorAccountModel { DebtorAccount = new DebtorAccount() }; - var result = simpra.Execute( - model, new SimpraExpressionTests.TestFunctions(), + var result = simpra.Execute( + model, new TestFunctions(), "DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Attributes[\"Region\"] is \"EU\""); Assert.False(result); @@ -791,8 +791,8 @@ public void BothDictsPopulated_MixedMissingKeys_And_ShouldReturnFalse() } }; - var result = simpra.Execute( - model, new SimpraExpressionTests.TestFunctions(), + var result = simpra.Execute( + model, new TestFunctions(), "DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Attributes[\"Region\"] is \"EU\""); Assert.False(result); @@ -811,8 +811,8 @@ public void BothDictsPopulated_MixedMissingKeys_Or_ShouldReturnTrue() } }; - var result = simpra.Execute( - model, new SimpraExpressionTests.TestFunctions(), + var result = simpra.Execute( + model, new TestFunctions(), "DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Attributes[\"Region\"] is \"EU\""); Assert.True(result); @@ -832,8 +832,8 @@ public void ThreeConditions_And_AllMatch_ShouldReturnTrue() } }; - var result = simpra.Execute( - model, new SimpraExpressionTests.TestFunctions(), + var result = simpra.Execute( + model, new TestFunctions(), "DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Attributes[\"Region\"] is \"EU\" and DebtorAccount.Name is \"VIP\""); Assert.True(result); @@ -853,8 +853,8 @@ public void ThreeConditions_And_MiddleNull_ShouldReturnFalse() } }; - var result = simpra.Execute( - model, new SimpraExpressionTests.TestFunctions(), + var result = simpra.Execute( + model, new TestFunctions(), "DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Attributes[\"Region\"] is \"EU\" and DebtorAccount.Name is \"VIP\""); Assert.False(result); @@ -874,8 +874,8 @@ public void ThreeConditions_Or_OnlyLastMatches_ShouldReturnTrue() } }; - var result = simpra.Execute( - model, new SimpraExpressionTests.TestFunctions(), + var result = simpra.Execute( + model, new TestFunctions(), "DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Attributes[\"Region\"] is \"EU\" or DebtorAccount.Name is \"VIP\""); Assert.True(result); @@ -896,8 +896,8 @@ public void MixedAndOr_NullDict_And_PopulatedDict_Or_Property_ShouldEvaluateCorr }; // (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(), + var result = simpra.Execute( + model, new TestFunctions(), "(DebtorAccount.Properties[\"AccType\"] is \"200\" and DebtorAccount.Attributes[\"Region\"] is \"EU\") or DebtorAccount.Name is \"VIP\""); Assert.True(result); @@ -918,8 +918,8 @@ public void MixedAndOr_NullDict_Or_PopulatedDict_And_Property_ShouldEvaluateCorr }; // (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(), + var result = simpra.Execute( + model, new TestFunctions(), "(DebtorAccount.Properties[\"AccType\"] is \"200\" or DebtorAccount.Attributes[\"Region\"] is \"EU\") and DebtorAccount.Name is \"VIP\""); Assert.True(result); @@ -940,8 +940,8 @@ public void MixedAndOr_AllNull_And_Property_ShouldReturnFalse() }; // (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(), + var result = simpra.Execute( + model, new 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/OperatorTests.cs b/tests/AltaSoft.Simpra.tests/OperatorTests.cs new file mode 100644 index 0000000..7abe8d6 --- /dev/null +++ b/tests/AltaSoft.Simpra.tests/OperatorTests.cs @@ -0,0 +1,698 @@ +using AltaSoft.Simpra.Tests.Models; +using static AltaSoft.Simpra.Tests.Models.TestModelFactory; + +namespace AltaSoft.Simpra.Tests; + +public class OperatorTests +{ + [Fact] + public void TaskExpression_Should_ReturnTrue_When_ValueMatchesRegexPattern() + { + const string expressionCode = + """ + let Value = 'nbas568fq' + return Value matches '[a-zA-Z_][a-zA-Z_0-9]' + """; + + var simpra = new Simpra(); + var model = GetTestModel(); + var result = simpra.Execute(model, new TestFunctions(), expressionCode, + new SimpraCompilerOptions { MutabilityOption = MutabilityOption.Immutable, StringComparisonOption = StringComparisonOption.IgnoreCase }); + Assert.True(result); + } + + [Fact] + public void Execute_NullableAmount_LessThanTen_ShouldReturnTrue() + { + const string expressionCode = "NullableAmount < 10"; + + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = simpra.Execute(model, expressionCode, null); + Assert.True(result); + } + + [Fact] + public void Execute_NullableIntegerProperty_LessThanZero_ShouldReturnFalse() + { + const string expressionCode = "NullableIntegerProperty < 0"; + + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = simpra.Execute(model, expressionCode, null); + Assert.False(result); + } + + [Fact] + public void ExecuteExpression_Should_ReturnFalse_When_TransferAmountIsGreaterThanTen() + { + const string expressionCode = "Transfer.Amount < 10"; + + var simpra = new Simpra(); + var model = GetTestModel(); + model.Transfer!.Amount = 11; + + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + Assert.False(result); + } + + [Fact] + public void ExecuteExpression_WithLogicalOperations_ShouldReturnFalse() + { + var simpra = new Simpra(); + var model = new TestModelMain { Test = null }; + const string expression = "return (false or false) and (true or true)"; + + var result = simpra.Execute(model, new TestFunctions(), expression); + + Assert.False(result); + } + + [Fact] + public void ExecuteExpression_WithArithmeticOperations_ShouldReturnCorrectValue() + { + var simpra = new Simpra(); + var model = new TestModelMain { Test = null }; + const string expression = "return (10 + 10) * 10"; + const int expected = 200; + + var result = simpra.Execute(model, new TestFunctions(), expression); + + Assert.Equal(expected, result); + } + + [Fact] + public void ExecuteExpression_Should_ReturnZero_When_AddingOneAndNegativeOne() + { + const string expression = """ + let x = 1 + let y = -1 + return x + y + """; + var simpra = new Simpra(); + var model = new TestModelMain { Test = new TestModel1 { Test = TestModel2.Test2 } }; + var result = simpra.Execute(model, new TestFunctions(), expression); + Assert.Equal(0, result); + } + + [Fact] + public void Expression_Should_ReturnTrue_When_StrMatchesPattern() + { + const string expressionCode = + """ + let str = 'abc'; + return str like 'a%' + """; + + var simpra = new Simpra(); + var model = GetTestModel(); // Model is irrelevant here + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.True(result); + } + + [Fact] + public void Expression_Should_ReturnTrue_When_CaseInsensitivePatternMatches() + { + const string expressionCode = + """ + let str = 'abc' + $case_sensitive off + return str like 'A%' + """; + + var simpra = new Simpra(); + var model = GetTestModel(); // Model is irrelevant here + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.True(result); + } + + [Fact] + public void Expression_Should_ReturnFalse_When_CaseSensitivePatternDoesNotMatch() + { + const string expressionCode = + """ + let str = 'abc' + $case_sensitive on + return str like 'A%' + """; + + var simpra = new Simpra(); + var model = GetTestModel(); // Model is irrelevant here + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.False(result); + } + + [Fact] + public void Expression_Should_ReturnTrue_When_AmountIsGreaterThanHundred() + { + const string expressionCode = + """ + let transfer = Transfer + let amount = transfer.Amount + return amount > 100 * 2 / 1.1 + """; + + var simpra = new Simpra(); + var model = GetTestModel(); + model.Transfer!.Amount = 200; + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.True(result); + } + + [Fact] + public void Expression_Should_ReturnTrue_When_AmountIsGreaterThanFifty_And_CurrencyIsUSD() + { + const string expressionCode = "return Transfer.Amount > 50 and Transfer.Currency is 'USD'"; + + var simpra = new Simpra(); + var model = GetTestModel(); + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.True(result); + } + + [Fact] + public void Expression_Should_ReturnFalse_When_AmountIsNotGreaterThanFifty_And_CurrencyIsNotUSD() + { + const string expressionCode = "return not (Transfer.Amount > 50 and Transfer.Currency is 'USD')"; + + var simpra = new Simpra(); + var model = GetTestModel(); + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.False(result); + } + + [Fact] + public void Expression_Should_ReturnTrue_When_CurrencyIsInList() + { + const string expressionCode = "return Transfer.Currency in ['USD', 'EUR']"; + + var simpra = new Simpra(); + var model = GetTestModel(); + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.True(result); + } + + [Fact] + public void Expression_Should_ReturnFalse_When_CurrencyIsNotInList() + { + const string expressionCode = "return Transfer.Currency not in ['USD', 'EUR']"; + + var simpra = new Simpra(); + var model = GetTestModel(); + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.False(result); + } + + [Fact] + public void Expression_Should_ReturnTrue_When_CurrencyIsConcatenatedString() + { + const string expressionCode = "return Transfer.Currency is 'US' + 'D'"; + + var simpra = new Simpra(); + var model = GetTestModel(); + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.True(result); + } + + [Fact] + public void Expression_Should_ReturnTrue_When_CurrencyIsInConcatenatedList() + { + const string expressionCode = "return Transfer.Currency in ['USD', 'EUR'] + 'GEL'"; + + var simpra = new Simpra(); + var model = GetTestModel(); + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.True(result); + } + + [Fact] + public void Expression_Should_ReturnTrue_When_AmountIsEqualToFifty() + { + const string expressionCode = "return Transfer.Amount is 50"; + + var simpra = new Simpra(); + var model = GetTestModel(); + model.Transfer!.Amount = 50; + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.True(result); + } + + [Fact] + public void Expression_Should_ReturnTrue_When_NestedConditionsAreMet() + { + const string expressionCode = + "return (Transfer.Amount > 50 and Transfer.Currency is 'USD') or (Transfer.Amount < 20 and Transfer.Currency is 'EUR')"; + + var simpra = new Simpra(); + var model = GetTestModel(); + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.True(result); + } + + [Fact] + public void Expression_Should_ReturnTrue_When_CurrencyIsEmpty() + { + const string expressionCode = "return Transfer.Currency is ''"; + + var simpra = new Simpra(); + var model = GetTestModel(); + model.Transfer!.Currency = ""; + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.True(result); + } + + [Fact] + public void Expression_Should_ReturnHelloWorld_When_StringsAreConcatenated() + { + const string expressionCode = "return 'Hello' + ' ' + 'World!'"; + + var simpra = new Simpra(); + var model = GetTestModel(); + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.Equal("Hello World!", result); + } + + [Fact] + public void Expression_Should_ReturnCurrencyWithString_When_Concatenated() + { + const string expressionCode = "return Transfer.Currency + ' is strong'"; + + var simpra = new Simpra(); + var model = GetTestModel(); + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.Equal("USD is strong", result); + } + + [Fact] + public void Expression_Should_ReturnCurrencyAndAmount_When_Concatenated() + { + const string expressionCode = "return Transfer.Currency + ' - Amount: ' + Transfer.Amount"; + + var simpra = new Simpra(); + var model = GetTestModel(); + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.Equal("USD - Amount: 100", result); + } + + [Fact] + public void Expression_Should_ReturnTrue_When_CurrencyIsInSubtractedList() + { + const string expressionCode = "return Transfer.Currency in ['USD', 'EUR', 'GEL'] - ['GEL']"; + + var simpra = new Simpra(); + var model = GetTestModel(); + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.True(result); + + model.Transfer!.Currency = "GEL"; + result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.False(result); + } + + [Fact] + public void Expression_Should_ReturnEmptyArray_When_SubtractingIdenticalLists() + { + const string expressionCode = "return ['USD', 'EUR'] - ['USD', 'EUR']"; + + var simpra = new Simpra(); + var model = GetTestModel(); + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.Empty(result); + } + + [Fact] + public void Expression_Should_ReturnOriginalArray_When_SubtractingNonOverlappingLists() + { + const string expressionCode = "return ['USD', 'EUR'] - ['GEL']"; + + var simpra = new Simpra(); + var model = GetTestModel(); + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.Contains("USD", result); + Assert.Contains("EUR", result); + Assert.DoesNotContain("GEL", result); + } + + [Theory] + [InlineData("2024-01-01", "2023-01-01", true)] + [InlineData("2023-01-01", "2024-01-01", false)] + [InlineData("2024-01-01", "2024-01-01", false)] + public void SimpraDate_GreaterThanComparison_ShouldCompareChronologically(string left, string right, bool expected) + { + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = simpra.Execute(model, new TestFunctions(), $"return date('{left}') > date('{right}')"); + Assert.Equal(expected, result); + } + + [Fact] + public void SimpraDate_Equality_ShouldCompareByValue() + { + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = simpra.Execute(model, new TestFunctions(), "return date('2024-01-01') is date('2024-01-01')"); + Assert.True(result); + + result = simpra.Execute(model, new TestFunctions(), "return date('2024-01-01') is not date('2024-01-02')"); + Assert.True(result); + } + + [Fact] + public void SimpraDate_Min_ShouldReturnEarlierDate() + { + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = simpra.Execute(model, new TestFunctions(), "return date('2024-01-01') min date('2023-01-01')"); + Assert.Equal(new DateTime(2023, 1, 1), result); + } + + [Fact] + public void SimpraDate_Max_ShouldReturnLaterDate() + { + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = simpra.Execute(model, new TestFunctions(), "return date('2024-01-01') max date('2023-01-01')"); + Assert.Equal(new DateTime(2024, 1, 1), result); + } + + [Fact] + public void SimpraDate_In_ShouldReturnTrue_When_DateExistsInList() + { + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = simpra.Execute(model, new TestFunctions(), + "return date('2024-01-01') in [date('2024-01-01'), date('2024-06-01')]"); + Assert.True(result); + + result = simpra.Execute(model, new TestFunctions(), + "return date('2024-12-25') in [date('2024-01-01'), date('2024-06-01')]"); + Assert.False(result); + } + + [Theory] + [InlineData(true, false, true)] // AND NOT: true && !false = true + [InlineData(true, true, false)] // true && !true = false + [InlineData(false, true, false)] // false && !true = false + [InlineData(false, false, false)] // false && !false = false + public void BooleanOperator_Subtract_ShouldComputeAndNot(bool left, bool right, bool expected) + { + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = simpra.Execute(model, new TestFunctions(), $"return {(left ? "true" : "false")} - {(right ? "true" : "false")}"); + Assert.Equal(expected, result); + } + + [Theory] + [InlineData(true, true, true)] + [InlineData(true, false, false)] + [InlineData(false, false, false)] + public void BooleanOperator_Multiply_ShouldComputeLogicalAnd(bool left, bool right, bool expected) + { + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = simpra.Execute(model, new TestFunctions(), $"return {(left ? "true" : "false")} * {(right ? "true" : "false")}"); + Assert.Equal(expected, result); + } + + [Theory] + [InlineData(true, true, false)] // NAND: not(true && true) = false + [InlineData(true, false, true)] + [InlineData(false, false, true)] + public void BooleanOperator_Divide_ShouldComputeLogicalNand(bool left, bool right, bool expected) + { + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = simpra.Execute(model, new TestFunctions(), $"return {(left ? "true" : "false")} / {(right ? "true" : "false")}"); + Assert.Equal(expected, result); + } + + [Fact] + public void BooleanOperator_MinMax_ShouldComputeLogicalAndOr() + { + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = simpra.Execute(model, new TestFunctions(), "return true min false"); + Assert.False(result); + + result = simpra.Execute(model, new TestFunctions(), "return true max false"); + Assert.True(result); + } + + [Theory] + [InlineData(10, 3, 3)] // 10 / 3 = 3.33.. -> 3 + [InlineData(7, 2, 4)] // 7 / 2 = 3.5 -> rounds to nearest even (4) + [InlineData(9, 2, 4)] // 9 / 2 = 4.5 -> rounds to nearest even (4) + [InlineData(-7, 2, -4)] // -7 / 2 = -3.5 -> rounds to nearest even (-4) + public void BinaryOperator_IntegerDivision_ShouldRoundQuotientToNearestEvenInteger(int left, int right, int expected) + { + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = simpra.Execute(model, new TestFunctions(), $"return {left} // {right}"); + Assert.Equal(expected, result); + } + + [Fact] + public void BinaryOperator_IntegerDivision_ByZero_ShouldThrowDivideByZeroException() + { + var simpra = new Simpra(); + var model = GetTestModel(); + + var f = () => simpra.Execute(model, new TestFunctions(), "return 5 // 0"); + Assert.Throws(() => f()); + } + + [Theory] + [InlineData(10, 3, 3)] + [InlineData(3, 10, 3)] + [InlineData(-5, -2, -5)] + public void BinaryOperator_Min_ShouldReturnSmallerNumber(int left, int right, int expected) + { + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = simpra.Execute(model, new TestFunctions(), $"return {left} min {right}"); + Assert.Equal(expected, result); + } + + [Theory] + [InlineData(10, 3, 10)] + [InlineData(3, 10, 10)] + [InlineData(-5, -2, -2)] + public void BinaryOperator_Max_ShouldReturnLargerNumber(int left, int right, int expected) + { + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = simpra.Execute(model, new TestFunctions(), $"return {left} max {right}"); + Assert.Equal(expected, result); + } + + [Fact] + public void UnaryOperator_Plus_ShouldReturnSameNumericValue() + { + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = simpra.Execute(model, new TestFunctions(), "return +5"); + Assert.Equal(5, result); + } + + [Fact] + public void UnaryOperator_Percent_ShouldConvertToHundredth() + { + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = simpra.Execute(model, new TestFunctions(), "return 50%"); + Assert.Equal(0.5m, result); + } + + [Fact] + public void UnaryOperator_Percent_AppliedWithinArithmetic_ShouldComputePercentageOfAmount() + { + var simpra = new Simpra(); + var model = GetTestModel(); + model.Transfer!.Amount = 200; + + var result = simpra.Execute(model, new TestFunctions(), "return Transfer.Amount * 5%"); + Assert.Equal(10m, result); + } + + [Theory] + [InlineData(150, true)] + [InlineData(1000, false)] + [InlineData(50, false)] + [InlineData(100, false)] // lower bound is exclusive + [InlineData(101, true)] + public void ChainedComparison_WithAndKeyword_ShouldCheckValueIsWithinRange(int amount, bool expected) + { + var simpra = new Simpra(); + var model = GetTestModel(); + model.Transfer!.Amount = amount; + + var result = simpra.Execute(model, new TestFunctions(), "return Transfer.Amount > 100 and < 1000"); + Assert.Equal(expected, result); + } + + [Theory] + [InlineData(150, true)] + [InlineData(1000, false)] + [InlineData(50, false)] + public void ChainedComparison_WithOrKeyword_StillCombinesBothSidesWithAnd(int amount, bool expected) + { + // The grammar accepts 'and'/'or' between the two comparison halves of a chained comparison, + // but the visitor (HandleChainedComparison) always combines them with AndAlso regardless of + // which keyword was written. This test documents that actual (surprising) behavior. + var simpra = new Simpra(); + var model = GetTestModel(); + model.Transfer!.Amount = amount; + + var result = simpra.Execute(model, new TestFunctions(), "return Transfer.Amount > 100 or < 1000"); + Assert.Equal(expected, result); + } + + [Fact] + public void BinaryOperator_AnyIn_ShouldReturnTrue_When_AtLeastOneElementExistsInOtherList() + { + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = simpra.Execute(model, new TestFunctions(), "return [1, 2, 3] any in [5, 6, 3]"); + Assert.True(result); + } + + [Fact] + public void BinaryOperator_AnyIn_ShouldReturnFalse_When_NoElementsExistInOtherList() + { + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = simpra.Execute(model, new TestFunctions(), "return [1, 2, 3] any in [5, 6, 7]"); + Assert.False(result); + } + + [Fact] + public void BinaryOperator_AllIn_ShouldReturnTrue_When_EveryElementExistsInOtherList() + { + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = simpra.Execute(model, new TestFunctions(), "return [1, 2] all in [1, 2, 3]"); + Assert.True(result); + } + + [Fact] + public void BinaryOperator_AllIn_ShouldReturnFalse_When_NotEveryElementExistsInOtherList() + { + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = simpra.Execute(model, new TestFunctions(), "return [1, 2, 4] all in [1, 2, 3]"); + Assert.False(result); + } + + [Fact] + public void BinaryOperator_AnyNotIn_ShouldReturnTrue_When_NoElementsExistInOtherList() + { + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = simpra.Execute(model, new TestFunctions(), "return [1, 2, 3] any not in [5, 6, 7]"); + Assert.True(result); + } + + [Fact] + public void BinaryOperator_AllNotIn_ShouldReturnFalse_When_AllElementsExistInOtherList() + { + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = simpra.Execute(model, new TestFunctions(), "return [1, 2] all not in [1, 2, 3]"); + Assert.False(result); + } + + [Fact] + public void ListOperator_Subtract_OnNumberLists_ShouldRemoveMatchingElements() + { + var simpra = new Simpra(); + var model = GetTestModel(); + var result = simpra.Execute(model, new TestFunctions(), "return [1, 2, 3] - [2, 3]"); + Assert.Equal([1m], result); + } + + [Fact] + public void ListOperator_Multiply_ShouldReturnIntersectionOfLists() + { + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = simpra.Execute(model, new TestFunctions(), "return ['USD', 'EUR', 'GEL'] * ['EUR', 'GEL', 'EUR']"); + + Assert.Equal(2, result.Length); + Assert.Contains("EUR", result); + Assert.Contains("GEL", result); + Assert.DoesNotContain("USD", result); + } + + [Fact] + public void ListOperator_Divide_ShouldThrowInvalidOperationException() + { + var simpra = new Simpra(); + var model = GetTestModel(); + + var f = () => simpra.Execute(model, new TestFunctions(), "return ['USD', 'EUR'] / ['EUR']"); + Assert.Throws(() => f()); + } + + [Fact] + public void StringOperator_Multiply_ShouldRepeatStringGivenNumberOfTimes() + { + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = simpra.Execute(model, new TestFunctions(), "return 'ab' * 3"); + Assert.Equal("ababab", result); + } + + [Fact] + public void StringOperator_Divide_ShouldSplitStringBySeparatorIntoList() + { + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = simpra.Execute(model, new TestFunctions(), "return 'a,b,c' / ','"); + Assert.Equal(["a", "b", "c"], result); + } +} diff --git a/tests/AltaSoft.Simpra.tests/SimpraExpressionTests.cs b/tests/AltaSoft.Simpra.tests/SimpraExpressionTests.cs deleted file mode 100644 index d21aad2..0000000 --- a/tests/AltaSoft.Simpra.tests/SimpraExpressionTests.cs +++ /dev/null @@ -1,1861 +0,0 @@ -using AltaSoft.Simpra.Tests.Models; - -namespace AltaSoft.Simpra.Tests; - -public class SimpraExpressionTests -{ - [Theory] - // basic / within bounds - [InlineData("USD", 1, 3, "USD")] // from U - [InlineData("USD", 1, 2, "US")] - [InlineData("USD", 2, 2, "SD")] // from S - [InlineData("USD", 3, 1, "D")] // from D - - // length clamping at the end - [InlineData("USD", 1, 10003, "USD")] - [InlineData("USD", 3, 10, "D")] - - // start beyond end - [InlineData("USD", 1000, 1, "")] - [InlineData("USD", 4, 1, "")] // beyond "USD" - - // start exactly at end - [InlineData("USD", 4, 0, "")] - [InlineData("USD", 4, 10, "")] - - // zero length - [InlineData("USD", 1, 0, "")] - [InlineData("USD", 2, 0, "")] - [InlineData("USD", 3, 0, "")] - - // negative/zero start ? clamp to 1 - [InlineData("USD", 0, 1, "U")] - [InlineData("USD", -5, 2, "US")] - [InlineData("USD", -2, 100, "USD")] - - // negative length ? empty - [InlineData("USD", 1, -1, "")] - [InlineData("USD", 3, -10, "")] - [InlineData("USD", -2, -10, "")] - - // empty input - [InlineData("", 1, 5, "")] - [InlineData("", 10, 1, "")] - [InlineData("", -3, 2, "")] - [InlineData("", 1, 0, "")] - - // whitespace - [InlineData(" ", 1, 1, " ")] - [InlineData(" ", 2, 2, " ")] - [InlineData(" ", 2, 100, " ")] - - // non-ASCII (safe checks with multi-byte chars) - [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) - { - var simpra = new Simpra(); - var model = GetTestModel(); - model.Ccy = input; - var expressionCode = $"return substring(Ccy,{start},{length})"; - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.Equal(expected, result); - } - - [Fact] - public void BuiltInFunction_Substring_ShouldMatchExamplesFromPrompt() - { - var simpra = new Simpra(); - var model = GetTestModel(); - model.Ccy = "USD"; - - var result = simpra.Execute(model, new TestFunctions(), "return substring(Ccy,0,3)"); - Assert.Equal("USD", result); - - result = simpra.Execute(model, new TestFunctions(), "return substring(Ccy,0,10003)"); - Assert.Equal("USD", result); - - result = simpra.Execute(model, new TestFunctions(), "return substring(Ccy,1000,1)"); - Assert.Equal("", result); - - result = simpra.Execute(model, new TestFunctions(), "return substring(Ccy,1000,0)"); - Assert.Equal("", result); - - result = simpra.Execute(model, new TestFunctions(), "return substring(Ccy,1,0)"); - Assert.Equal("", result); - } - [Theory] - // basic in-bounds - [InlineData("USD", 1, "U")] - [InlineData("USD", 2, "S")] - [InlineData("USD", 3, "D")] - - // at/after end => empty - [InlineData("USD", 4, "")] - [InlineData("USD", 1000, "")] - [InlineData("U", 2, "")] - [InlineData("", 1, "")] - [InlineData("", 5, "")] - - // zero/negative start ? clamp to 1 - [InlineData("USD", 0, "U")] - [InlineData("USD", -1, "U")] - [InlineData("USD", -5, "U")] - [InlineData("", -3, "")] - - // whitespace - [InlineData(" X ", 1, " ")] - [InlineData(" X ", 2, "X")] - [InlineData(" X ", 3, " ")] - - // non-ASCII (single UTF-16 code units) - [InlineData("???????", 1, "?")] - [InlineData("???????", 3, "?")] - [InlineData("???????", 6, "?")] - [InlineData("???????", 7, "?")] - [InlineData("???????", 8, "")] - - // very large index - [InlineData("USD", int.MaxValue, "")] - public void BuiltInFunction_Substring_StartOnly_EdgeCases(string input, int start, string expected) - { - var simpra = new Simpra(); - var model = GetTestModel(); - model.Ccy = input; - var expressionCode = $"return substring(Ccy,{start})"; - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.Equal(expected, result); - } - - [Fact] - public void BuiltInFunction_Substring_StartOnly_NullSource_ShouldBeEmpty() - { - var simpra = new Simpra(); - var model = GetTestModel(); - model.Ccy = null; - - var result = simpra.Execute(model, new TestFunctions(), "return substring(Ccy,1)"); - Assert.Null(result); - } - - [Fact] - public void BuiltInFunction_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 = GetTestModel(); - model.Ccy = null!; - - var result = simpra.Execute(model, new TestFunctions(), "return substring(Ccy,0,3)"); - Assert.Null(result); - } - - [Fact] - public void BuiltInFunction_Substring_ShouldReturnCorrectSubstringOfLength1() - { - const string expressionCode = "return substring(Ccy,1)"; - - var simpra = new Simpra(); - var model = GetTestModel(); - model.Ccy = "USD"; - - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - Assert.Equal("U", result); - } - - [Fact] - public void InvalidSimpraSyntax_ShouldThrowException_WhenIncorrectAndSignIsUsedAndReturnStatement() - { - const string expressionCode = - """ - return Amount is 100 && Amount is 200 - """; - - var simpra = new Simpra(); - var model = GetTestModel(); - - // ReSharper disable ConvertToLocalFunction - var f = () => simpra.Execute(model, new TestFunctions(), expressionCode); - // ReSharper restore ConvertToLocalFunction - Assert.Throws(() => f()); - } - - [Fact] - public void InvalidSimpraSyntax_ShouldThrowException_WhenIncorrectAndSignIsUsed() - { - const string expressionCode = - """ - Amount is 100 && Amount is 200 - """; - - var simpra = new Simpra(); - var model = GetTestModel(); - - // ReSharper disable ConvertToLocalFunction - var f = () => simpra.Execute(model, new TestFunctions(), expressionCode); - // ReSharper restore ConvertToLocalFunction - Assert.Throws(() => f()); - } - - [Fact] - public void InvalidSimpraSyntax_ShouldThrowException_WithoutReturn() - { - const string expressionCode = - """ - 111 Amount is 100 - """; - - var simpra = new Simpra(); - var model = GetTestModel(); - - // ReSharper disable ConvertToLocalFunction - var f = () => simpra.Execute(model, new TestFunctions(), expressionCode); - // ReSharper restore ConvertToLocalFunction - Assert.Throws(() => f()); - } - - [Fact] - public void InvalidSimpraSyntax_ShouldThrowException_WithReturn() - { - const string expressionCode = - """ - return 111 Amount is 100 - """; - - var simpra = new Simpra(); - var model = GetTestModel(); - - // ReSharper disable ConvertToLocalFunction - var f = () => simpra.Execute(model, new TestFunctions(), expressionCode); - // ReSharper restore ConvertToLocalFunction - Assert.Throws(() => f()); - } - - [Fact] - public void DictionaryIndexer_MissingKey_ReturnsDefaultValueForProperty() - { - const string expressionCode = - """ - return DictionaryOfObjects['test'].Id - """; - - var simpra = new Simpra(); - var model = GetTestModel(); - model.DictionaryOfObjects = new Dictionary { { "Georgia", new Customer { Id = 1, Status = 10 } } }; - - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - Assert.Equal(0, result); - } - - [Fact] - public void CallGetValueFromDictionaryWhenKeyDoesNotExist_ShouldReturnDefault() - { - const string expressionCode = - """ - return Countries['test'] is 'Test' - """; - - var simpra = new Simpra(); - var model = GetTestModel(); - model.Countries = new Dictionary { { "Georgia", "Test" } }; - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - Assert.False(result); - } - - [Fact] - public void CallGetValueFromDictionaryWhenKeyExist_ShouldReturnValue() - { - const string expressionCode = - """ - return Countries['Georgia'] is 'Test' - """; - - var simpra = new Simpra(); - var model = GetTestModel(); - model.Countries = new Dictionary { { "Georgia", "Test" } }; - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - Assert.True(result); - } - - [Fact] - public void CallInterfaceFunctionFromSimpra_ShouldReturnCorrectValue() - { - const string expressionCode = - """ - return Upper('test') - """; - - var simpra = new Simpra(); - var model = GetTestModel(); - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - Assert.Equal("TEST", result); - } - - [Fact] - public void CallBaseInterfaceFunctionFromSimpra_ShouldReturnCorrectValue() - { - const string expressionCode = - """ - return Lower('TEST') - """; - - var simpra = new Simpra(); - var model = GetTestModel(); - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - Assert.Equal("test", result); - } - - [Fact] - public void CallBaseStaticFunctionFromSimpra_ShouldReturnCorrectValue() - { - const string expressionCode = - """ - return CallBaseStaticMethod() - """; - - var simpra = new Simpra(); - var model = GetTestModel(); - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - Assert.Equal("BaseStaticMethod", result); - } - - [Fact] - public void CallBaseFunctionFromSimpra_ShouldReturnCorrectValue() - { - const string expressionCode = - """ - return CallBaseMethod() - """; - - var simpra = new Simpra(); - var model = GetTestModel(); - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - Assert.Equal("BaseMethod", result); - } - - [Fact] - public void CallFunctionFromSimpra() - { - const string expressionCode = - """ - return ListSomeCountries('GE') - """; - - var simpra = new Simpra(); - var model = GetTestModel(); - var result = simpra.Execute, TestModel, TestFunctions>(model, new TestFunctions(), expressionCode); - Assert.True(result.SequenceEqual(["RU", "BE", "GE"])); - } - - [Fact] - public void ModelWithInheritedClassProperties_ShouldFindPropertyCorrectly() - { - const string expressionCode = - """ - return Color - """; - - var simpra = new Simpra(); - var model = GetTestModel(); - model.Color = Color.Blue; - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - Assert.Equal(Color.Blue, result); - } - - [Fact] - public void ModelWithInheritedInterfaceProperties_ShouldFindPropertyCorrectly() - { - const string expressionCode = - """ - return Customer.Id - """; - - var simpra = new Simpra(); - var model = GetTestModel(); - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - Assert.Equal(1, result); - } - - [Fact] - public void Expression_ReturnNullableEnum_ReturnValueMustBeCorrect() - { - const string expressionCode = - """ - return NullableEnum - """; - - var simpra = new Simpra(); - var model = GetTestModel(); - model.NullableEnum = Color.Green; - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - Assert.NotNull(result); - - model.NullableEnum = null; - result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.Null(result); - } - - [Fact] - public void Expression_ShouldCompareNullableEnum_ReturnValueMustBeCorrect() - { - const string expressionCode = - """ - return NullableEnum is 'Green' - """; - - var simpra = new Simpra(); - var model = GetTestModel(); - model.NullableEnum = Color.Green; - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - Assert.True(result); - } - - [Fact] - public void Expression_NestedDomainPrimitiveType_ShouldReturnCorrectly() - { - const string expressionCode = - """ - return Transfer.RegulatoryReporting[1].Details[1].Information[1] - """; - - var simpra = new Simpra(); - var model = Iso20022TransferModel.CreateForInformation(); - - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.NotNull(result); - } - - [Fact] - public void Expression_NestedDomainPrimitiveType_ShouldCompareCorrectly() - { - const string expressionCode = - """ - return Transfer.RegulatoryReporting[1].Details[1].Information[1] is 'Information1' - """; - - var simpra = new Simpra(); - var model = Iso20022TransferModel.CreateForInformation(); - - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.True(result); - } - - [Fact] - public void ExpressionListOfList_Comparison_ShouldReturnCorrectly() - { - const string expressionCode = - """ - return ListOfList[2][2] is 4 - """; - - var simpra = new Simpra(); - var model = new ListModel { EnumList = [Color.Blue, Color.Green], IntegerList = [1, 2, 3], StringList = ["test", "test2"], ListOfList = [[1, 2], [3, 4]] }; - - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.True(result); - } - - [Fact] - public void ListOfList_ShouldReturnCorrectly() - { - const string expressionCode = - """ - return ListOfList - """; - - var simpra = new Simpra(); - var model = new ListModel { EnumList = [Color.Blue, Color.Green], IntegerList = [1, 2, 3], StringList = ["test", "test2"], ListOfList = [[1, 2], [3, 4]] }; - - var result = simpra.Execute>, ListModel, TestFunctions>(model, new TestFunctions(), expressionCode); - - Assert.Equal(4, result[1][1]); - } - - [Fact] - public void GetComplexObject_ShouldReturnCorrectly() - { - const string expressionCode = "return Customer"; - - var simpra = new Simpra(); - var model = GetTestModel(); // Model is irrelevant here - - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.Equal(1, result.Id); - } - - [Fact] - public void EnumerableOfIntegers_ShouldReturnCorrectly() - { - const string expressionCode = - """ - return IntegerEnumerable - """; - - var simpra = new Simpra(); - var model = new ListModel { EnumList = [Color.Blue, Color.Green], IntegerList = [1, 2, 3], StringList = ["test", "test2"], IntegerEnumerable = [1, 2, 3] }; - - var result = simpra.Execute, ListModel, TestFunctions>(model, new TestFunctions(), expressionCode); - - Assert.Equal(2, result.ToList()[1]); - } - - [Fact] - public void ArrayOfIntegers_ShouldReturnCorrectly() - { - const string expressionCode = - """ - return IntegerArray - """; - - var simpra = new Simpra(); - var model = new ListModel { EnumList = [Color.Blue, Color.Green], IntegerList = [1, 2, 3], StringList = ["test", "test2"], IntegerArray = [1, 2, 3] }; - - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.Equal(2, result[1]); - } - - [Fact] - public void ListOfComplexObjects_ShouldReturnCorrectly() - { - const string expressionCode = - """ - return ComplexList - """; - - var simpra = new Simpra(); - var model = new ListModel - { - EnumList = [Color.Blue, Color.Green], - IntegerList = [1, 2, 3], - StringList = ["test", "test2"], - ComplexList = - [ - new Customer { Id = 1, Status = 1 }, - new Customer { Id = 2, Status = 2 } - ] - }; - - var result = simpra.Execute, ListModel, TestFunctions>(model, new TestFunctions(), expressionCode); - - Assert.Equal(2, result[1].Id); - } - - [Fact] - public void ExpressionStringListEqualsValue_ShouldReturnCorrectValues() - { - const string expressionCode = - """ - return StringList[1] is 'test' - """; - - var simpra = new Simpra(); - var model = new ListModel { EnumList = [Color.Blue, Color.Green], IntegerList = [1, 2, 3], StringList = ["test", "test2"] }; - - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.True(result); - } - - [Fact] - public void StringList_ShouldReturnCorrectValues() - { - const string expressionCode = - """ - return StringList - """; - - var simpra = new Simpra(); - var model = new ListModel { EnumList = [Color.Blue, Color.Green], IntegerList = [1, 2, 3], StringList = ["test", "test2"] }; - - var result = simpra.Execute, ListModel, TestFunctions>(model, new TestFunctions(), expressionCode); - - Assert.Equal("test2", result[1]); - } - - [Fact] - public void EnumList_ShouldReturnCorrectValues() - { - const string expressionCode = - """ - return EnumList - """; - - var simpra = new Simpra(); - var model = new ListModel { EnumList = [Color.Blue, Color.Green], IntegerList = [1, 2, 3], StringList = ["test", "test2"] }; - - var result = simpra.Execute, ListModel, TestFunctions>(model, new TestFunctions(), expressionCode); - - Assert.Equal(Color.Green, result[1]); - } - - [Fact] - public void IntegerList_ShouldReturnCorrectValues() - { - const string expressionCode = - """ - return IntegerList - """; - - var simpra = new Simpra(); - var model = new ListModel { EnumList = [Color.Blue, Color.Green], IntegerList = [1, 2, 3], StringList = ["test", "test2"] }; - - var result = simpra.Execute, ListModel, TestFunctions>(model, new TestFunctions(), expressionCode); - Assert.Equal(2, result[1]); - } - - [Fact] - public void Execute_ShouldReturnListOfComplexObject() - { - const string expressionCode = - """ - return Transfer.RegulatoryReporting - """; - - var simpra = new Simpra(); - var model = Iso20022TransferModel.CreateForCountry("FR"); - - var result = simpra.Execute, Iso20022TransferModel, TestFunctions>(model, new TestFunctions(), expressionCode); - Assert.Equal("FR", result[0].Authority.Country); - } - - [Fact] - public void Execute_ShouldReturnComplexObject_WhenAccessedViaIndex() - { - const string expressionCode = - """ - return Transfer.RegulatoryReporting[1] - """; - - var simpra = new Simpra(); - var model = Iso20022TransferModel.CreateForCountry("FR"); - - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - Assert.Equal("FR", result.Authority.Country); - } - - [Fact] - public void Execute_ShouldReturnTrue_WhenAuthorityCountryIsFR() - { - const string expressionCode = - """ - return Transfer.RegulatoryReporting[1].Authority.Country is 'FR' - """; - - var simpra = new Simpra(); - var model = Iso20022TransferModel.CreateForCountry("FR"); - - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - Assert.True(result); - } - - [Fact] - public void Expression_Should_ReturnFalse_When_IndexIsOutOfRangeAndValueCompared() - { - const string expressionCode = - """ - return Transfer.A[10] is 1 - """; - - var simpra = new Simpra(); - var model = GetTestModel(); - model.Transfer!.A = [1, 2, 3]; - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - Assert.False(result); - } - - [Fact] - public void Expression_Should_ReturnFalse_When_NestedListIsUsed() - { - const string expressionCode = - """ - return Transfer.OuterList[10].InnerList[1] is 1 - """; - - var simpra = new Simpra(); - var model = GetTestModel(); - model.Transfer!.A = [1, 2, 3]; - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - Assert.False(result); - } - - [Fact] - public void Expression_Should_ReturnFalse_When_NestedListIsUsedX() - { - const string expressionCode = - """ - return CustomerList[1] has value - """; - - var simpra = new Simpra(); - var model = GetTestModel(); - model.CustomerList = new List(); - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - Assert.False(result); - } - - [Fact] - public void Expression_Should_ReturnFalse_When_ArrayIsNullAndValueCompared() - { - const string expressionCode = - """ - return Transfer.A[1] is 1 - """; - - var simpra = new Simpra(); - var model = GetTestModel(); - model.Transfer = null; - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - Assert.False(result); - } - - [Fact] - public void Expression_Should_ReturnDefault_When_ArrayIsNull() - { - const string expressionCode = - """ - return Transfer.A[10] - """; - - var simpra = new Simpra(); - var model = GetTestModel(); - model.Transfer = null; - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - Assert.Equal(0, result); - } - - [Fact] - public void Expression_Should_ReturnFalse_When_TheValueIsNull() - { - const string expressionCode = - """ - return Transfer.Customer.Id is 1 - """; - - var simpra = new Simpra(); - var model = GetTestModel(); - model.Transfer = null; - - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.False(result); - } - - [Fact] - public void Expression_Should_ReturnMultipleValues_When_AggregateListValues() - { - const string expressionCode = - """ - let values = [1, 2, 3, 4, 5] - let sum = sum(values) - let avg = sum(values) / length(values) - return sum + avg - """; - - var simpra = new Simpra(); - var model = GetTestModel(); - - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.Equal(18m, result); - } - - [Fact] - public void Expression_Should_ReturnTrue_When_AmountIsGreaterThanMaxOfList() - { - const string expressionCode = - """ - let amounts = [10, 20, 30, 40] - let maxAmount = amounts[3] - return Transfer.Amount > maxAmount - """; - - var simpra = new Simpra(); - var model = GetTestModel(); - model.Transfer!.Amount = 50; - - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.True(result); - } - - [Fact] - public void Expression_Should_ReturnTrue_When_CheckingForNullValue() - { - const string expressionCode = - """ - let transfer = Transfer - return transfer.Currency has value - """; - - var simpra = new Simpra(); - var model = GetTestModel(); - model.Transfer!.Currency = "USD"; - - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.True(result); - } - - [Fact] - public void Expression_Should_ReturnTrue_When_AmountInCurrencyIsGreaterThanThresholdAndMatchesPattern() - { - const string expressionCode = - """ - let transfer = Transfer - let amount = transfer.Amount - let ccy = transfer.Currency - return amount > 100 and (ccy matches '^[A-Z]{3}$') and ccy is 'USD' - """; - - var simpra = new Simpra(); - var model = GetTestModel(); - model.Transfer!.Amount = 150; - model.Transfer.Currency = "USD"; - - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.True(result); - } - - [Fact] - public void Expression_Should_ReturnFalse_When_DividingByZeroHandledProperly() - { - const string expressionCode = - """ - let transfer = Transfer - let amount = transfer.Amount - let divisor = 0 - let safeDivision = when divisor is not 0 then amount / divisor else 0 end - return safeDivision is 0 - """; - - var simpra = new Simpra(); - var model = GetTestModel(); - - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.True(result); - } - - [Fact] - public void Expression_Should_ReturnTrue_When_DynamicStringListContainsMatchingItem() - { - const string expressionCode = - """ - let dynamicList = ListSomeCountries('countries') - return 'RU' in dynamicList or 'US' in dynamicList - """; - - var simpra = new Simpra(); - var model = GetTestModel(); - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.True(result); - } - - [Fact] - public void Expression_Should_ReturnTrue_When_DynamicIntListContainsMatchingItem() - { - const string expressionCode = - """ - let dynamicList = ListOfCustomerIds('Good') - return 1 in dynamicList - """; - - var simpra = new Simpra(); - var model = GetTestModel(); - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.True(result); - } - - [Fact] - public void Expression_Should_ReturnFalse_When_AmountIsNegativeAndCurrencyIsEUR() - { - const string expressionCode = - """ - let transfer = Transfer - let amount = transfer.Amount - let ccy = transfer.Currency - return amount < 0 and ccy is 'EUR' - """; - - var simpra = new Simpra(); - var model = GetTestModel(); - model.Transfer!.Amount = -50; - model.Transfer.Currency = "EUR"; - - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.True(result); - } - - [Fact] - public void Expression_Should_ReturnTrue_When_SomeComplexConditionIsMet() - { - const string expressionCode = - """ - let x = 100 - let y = 'USD' - let transfer = Transfer - let amount = transfer.Amount - return amount < 500 and y is 'USD' - """; - - var simpra = new Simpra(); - var model = GetTestModel(); - model.Transfer!.Amount = 100; - - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.True(result); - } - - [Fact] - public void Expression_Should_EvaluateNestedWhenConditionAndReturnCorrectResult() - { - const string expressionCode = - """ - let amount = Transfer.Amount - let result = when amount < 50 then 'Low' - when amount >= 50 and amount < 200 then 'Medium' - else 'High' - return result - """; - - var simpra = new Simpra(); - var model = GetTestModel(); - model.Transfer!.Amount = 150; - - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.Equal("Medium", result); - } - - [Fact] - public void Expression_Should_ReturnEmptyString_When_StringContainsNoMatch() - { - const string expressionCode = - """ - let str = 'foobar' - return when str like 'f%' then str else '' - """; - - var simpra = new Simpra(); - var model = GetTestModel(); - - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.Equal("foobar", result); - } - - [Fact] - public void Expression_Should_ReturnComplexCalculation_When_CombinedWithArithmetic() - { - const string expressionCode = - """ - let amount = Transfer.Amount - let fee = amount * 0.05 - let totalAmount = amount - fee - return totalAmount - """; - - var simpra = new Simpra(); - var model = GetTestModel(); - model.Transfer!.Amount = 200; - - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.Equal(190, result); - } - - [Fact] - public void Expression_Should_HandleLargeNestedCondition() - { - const string expressionCode = - """ - let x = Transfer.Amount - let y = Transfer.Currency - return when x < 50 then 'Low' - when x >= 50 and x <= 150 then 'Medium' - when x > 150 and y is 'USD' then 'High - USD' - when x > 150 and y is 'EUR' then 'High - EUR' - else 'Unknown' - """; - - var simpra = new Simpra(); - var model = GetTestModel(); - model.Transfer!.Amount = 200; - model.Transfer.Currency = "USD"; - - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.Equal("High - USD", result); - } - - [Fact] - public void TaskExpression_Should_ReturnTrue_When_ValueMatchesRegexPattern() - { - const string expressionCode = - """ - let Value = 'nbas568fq' - return Value matches '[a-zA-Z_][a-zA-Z_0-9]' - """; - - var simpra = new Simpra(); - var model = GetTestModel(); - var result = simpra.Execute(model, new TestFunctions(), expressionCode, - new SimpraCompilerOptions { MutabilityOption = MutabilityOption.Immutable, StringComparisonOption = StringComparisonOption.IgnoreCase }); - Assert.True(result); - } - - [Fact] - public void Expression_Should_HandleNestedFunctionCallsCorrectly() - { - const string expressionCode = - """ - let str = 'hello world' - let upperStr = Upper(str) - return Lower(upperStr) is str - """; - - var simpra = new Simpra(); - var model = GetTestModel(); - - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.True(result); - } - - [Fact] - public void Expression_Should_ReturnTrue_When_AmountIsWithinRange_And_CurrencyIsValid_And_AmountIncreasedByCustomLogic() - { - const string expressionCode = - """ - let transfer = Transfer - let amount = transfer.Amount - let currency = transfer.Currency - let threshold = 500 - let validCurrencies = ['USD', 'EUR', 'GBP'] - let increaseAmountByPercentage = amount + (amount * (5 / 100)) - let increasedAmount = when currency in validCurrencies then increaseAmountByPercentage else amount end - return increasedAmount > 180 and increasedAmount < threshold - """; - - var simpra = new Simpra(); - var model = GetTestModel(); - model.Transfer!.Amount = 180; - model.Transfer.Currency = "USD"; - - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.True(result); - } - - [Fact] - public void Expression_Should_ReturnCorrectResult_When_NestedListOperationsAndStringManipulationsAreUsed() - { - const string expressionCode = - """ - let items = ['USD', 'EUR', 'LIRA'] - let priceList = [2.8, 3.0, 0.75] - let targetItem = 'USD' - let targetPrice = when targetItem in items then 3 else 2 end - let discountedPrice = when targetPrice > 1.0 then targetPrice * 0.9 else targetPrice end - let result = targetItem + ' costs ' + discountedPrice - return result - """; - - var simpra = new Simpra(); - var model = GetTestModel(); - - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.Equal("USD costs 2.7", result); - } - - [Fact] - public void Execute_NullableAmount_LessThanTen_ShouldReturnTrue() - { - const string expressionCode = "NullableAmount < 10"; - - var simpra = new Simpra(); - var model = GetTestModel(); - - var result = simpra.Execute(model, expressionCode, null); - Assert.True(result); - } - - [Fact] - public void Execute_NullableIntegerProperty_LessThanZero_ShouldReturnFalse() - { - const string expressionCode = "NullableIntegerProperty < 0"; - - var simpra = new Simpra(); - var model = GetTestModel(); - - var result = simpra.Execute(model, expressionCode, null); - Assert.False(result); - } - - [Fact] - public void ExecuteExpression_Should_ReturnFalse_When_TransferAmountIsGreaterThanTen() - { - const string expressionCode = "Transfer.Amount < 10"; - - var simpra = new Simpra(); - var model = GetTestModel(); - model.Transfer!.Amount = 11; - - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - Assert.False(result); - } - - [Fact] - public void ExecuteExpression_Should_ReturnCorrectValue_FromNestedIfWithElse() - { - const string expressionCode = - """ - let x = Transfer.Amount - if x > 7 then - return 10 - else if x > 5 then - return 6 - else if x > 1 then - return 2 - else - return round(1000) - end - """; - - var simpra = new Simpra(); - var model = GetTestModel(); - - model.Transfer!.Amount = 8; - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - Assert.Equal(10, result); - - model.Transfer!.Amount = 6; - result = simpra.Execute(model, new TestFunctions(), expressionCode); - Assert.Equal(6, result); - - model.Transfer!.Amount = 2; - result = simpra.Execute(model, new TestFunctions(), expressionCode); - Assert.Equal(2, result); - - model.Transfer!.Amount = -1; - result = simpra.Execute(model, new TestFunctions(), expressionCode); - Assert.Equal(1000, result); - } - - [Fact] - public void ExecuteExpression_Should_ReturnCorrectValue_FromNestedIfWithoutElse() - { - const string expressionCode = - """ - let x = Transfer.Amount - if x > 7 then - return 10 - if x > 5 then - return 6 - if x > 1 then - return 2 - else - return 1000 - end - """; - - var simpra = new Simpra(); - var model = GetTestModel(); - model.Transfer!.Amount = 8; - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - Assert.Equal(10, result); - - model.Transfer!.Amount = 6; - result = simpra.Execute(model, new TestFunctions(), expressionCode); - Assert.Equal(6, result); - - model.Transfer!.Amount = 2; - result = simpra.Execute(model, new TestFunctions(), expressionCode); - Assert.Equal(2, result); - - model.Transfer!.Amount = -1; - result = simpra.Execute(model, new TestFunctions(), expressionCode); - Assert.Equal(1000, result); - } - - [Fact] - public void ExecuteExpression_Should_UpdateCustomerIdAndReturnSum_When_MutabilityIsOn() - { - const string expressionCode = - """ - $mutable on - let Y = Nint2 - let list = Values - let X = CustomerId - CustomerId = 12 - return X + Y - """; - - var simpra = new Simpra(); - var model = GetTestModel(); - - var result = simpra.Execute(model, new TestFunctions(), expressionCode, - new SimpraCompilerOptions { MutabilityOption = MutabilityOption.DefaultImmutable }); - - Assert.Equal(31, result); - Assert.Equal(12, (int)model.CustomerId); - } - - [Fact] - public void ExecuteExpression_Should_ReturnFalse_When_Nint1HasNoValue() - { - const string expressionCode = - """ - let X = CustomerId - let Y = Nint1 - return Y has value - """; - - var simpra = new Simpra(); - var model = GetTestModel(); - - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.False(result); - } - - [Fact] - public void ExecuteExpression_Should_ReturnSumOfXAndY_When_XyPropertiesAreUsed() - { - const string expressionCode = - """ - let X = Xy.X - let Y = Xy.Y - return X + Y - """; - - var simpra = new Simpra(); - var model = GetTestModel(); - - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.Equal(3, result); - } - - [Fact] - public void ExecuteExpression_Should_ReturnFirstReturnValue_When_MultipleReturnStatements() - { - var simpra = new Simpra(); - var model = GetTestModel(); - - const string expressionCode = - """ - return 10 - return 20 - """; - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - Assert.Equal(10, result); - } - - [Fact] - public async Task ExecuteExpression_Should_ReturnTrue_When_CcyIsInListOfCurrencyCodes() - { - var simpra = new Simpra(); - var model = GetTestModel(); - - const string expressionCode = "return Ccy in (ListOfCurrencyCodes('VisaB2B') + 'USD')"; - var result = await simpra.ExecuteAsync(model, new TestFunctions(), expressionCode, null, CancellationToken.None); - Assert.True(result); - } - - [Fact] - public void ExecuteExpression_WithLogicalOperations_ShouldReturnFalse() - { - var simpra = new Simpra(); - var model = new TestModelMain { Test = null }; - const string expression = "return (false or false) and (true or true)"; - - var result = simpra.Execute(model, new TestFunctions(), expression); - - Assert.False(result); - } - - [Fact] - public void ExecuteExpression_WithMultipleReturnStatements_ShouldReturnLastValue() - { - var simpra = new Simpra(); - var model = new TestModelMain { Test = null }; - const string expression = """ - return 10 - return true - """; - - var result = simpra.Execute(model, new TestFunctions(), expression); - - Assert.True(result); - } - - [Fact] - public void ExecuteExpression_WithArithmeticOperations_ShouldReturnCorrectValue() - { - var simpra = new Simpra(); - var model = new TestModelMain { Test = null }; - const string expression = "return (10 + 10) * 10"; - const int expected = 200; - - var result = simpra.Execute(model, new TestFunctions(), expression); - - Assert.Equal(expected, result); - } - - [Fact] - public void ExecuteExpression_WithNullCheck_ShouldReturnFalse() - { - var simpra = new Simpra(); - var model = new TestModelMain { Test = null }; - const string expression = "return Test has value"; - - var result = simpra.Execute(model, new TestFunctions(), expression); - - Assert.False(result); - } - - [Fact] - public void ExecuteExpression_Should_ReturnTrue_When_PropertyIsEnum() - { - const string expression = """ - return Test.Test is 'Test1'; - """; - var simpra = new Simpra(); - var model = new TestModelMain { Test = new TestModel1 { Test = TestModel2.Test1 } }; - var result = simpra.Execute(model, new TestFunctions(), expression); - - Assert.True(result); - } - - [Fact] - public void ExecuteExpression_Should_ReturnZero_When_AddingOneAndNegativeOne() - { - const string expression = """ - let x = 1 - let y = -1 - return x + y - """; - var simpra = new Simpra(); - var model = new TestModelMain { Test = new TestModel1 { Test = TestModel2.Test2 } }; - var result = simpra.Execute(model, new TestFunctions(), expression); - Assert.Equal(0, result); - } - - [Fact] - public void Expression_Should_ReturnTwo_When_XIsSixty() - { - const string expressionCode = - """ - let X = 50 + 10.0 - let Y = when X > 100 then 1 when X > 10 then 2 else 0 - return Y - """; - - var simpra = new Simpra(); - var model = GetTestModel(); // Model is irrelevant here - - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.Equal(2, result); - } - - [Fact] - public void Expression_Should_ReturnSixty_When_XIsNotFive() - { - const string expressionCode = - """ - let X = '50' - if X is not '5' then - X = '60' - end - return X - """; - - var simpra = new Simpra(); - var model = GetTestModel(); // Model is irrelevant here - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.Equal("60", result); - } - - [Fact] - public void Expression_Should_ReturnThree_When_bIsThirty() - { - const string expressionCode = - """ - let b = 30; - let x = 1; - return when b is 1 then 1 else 3 end - """; - - var simpra = new Simpra(); - var model = GetTestModel(); // Model is irrelevant here - - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.Equal(3, result); - } - - [Fact] - public async Task Expression_Should_EvaluateSanctionedCountriesExpression_Correctly() - { - const string expressionCode = - """ - let russianCountries = BigList('sanctioned_countries') - let amount = Transfer.Amount - let ccy = Transfer.Currency - - let isValidCcy = (ccy is '' or ccy not in ['USD', 'EUR']) and (ccy like 'I%' or ccy matches '[a-zA-Z_][a-zA-Z_0-9]') - let isValidAmount = amount > 1000 and amount < 2000 - let isValidAmount2 = amount > 1000 and < 2000 - let isValidRemittance = length(Remittance) > 4 - - return isValidCcy and isValidAmount and isValidRemittance - """; - - var simpra = new Simpra(); - var model = GetTestModel(); // Model is irrelevant here - var result = await simpra.ExecuteAsync(model, new TestFunctions(), expressionCode, - new SimpraCompilerOptions { MutabilityOption = MutabilityOption.Immutable, StringComparisonOption = StringComparisonOption.IgnoreCase }, - CancellationToken.None); - Assert.False(result); - } - - [Fact] - public void Expression_Should_ReturnOne_When_XIsNotFive() - { - const string expressionCode = - """ - let X = '50' - return when X is not '5' then 1 else 0 end - """; - - var simpra = new Simpra(); - var model = GetTestModel(); // Model is irrelevant here - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.Equal(1, result); - } - - [Fact] - public void Expression_Should_ReturnTrue_When_XIsNotFive_And_LengthIsTwo() - { - const string expressionCode = - """ - let X = '50' - return X is not '5' - and length(X) is 2; - """; - - var simpra = new Simpra(); - var model = GetTestModel(); // Model is irrelevant here - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.True(result); - } - - [Fact] - public void Expression_Should_ReturnTrue_When_StrMatchesPattern() - { - const string expressionCode = - """ - let str = 'abc'; - return str like 'a%' - """; - - var simpra = new Simpra(); - var model = GetTestModel(); // Model is irrelevant here - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.True(result); - } - - [Fact] - public void Expression_Should_ReturnTrue_When_CaseInsensitivePatternMatches() - { - const string expressionCode = - """ - let str = 'abc' - $case_sensitive off - return str like 'A%' - """; - - var simpra = new Simpra(); - var model = GetTestModel(); // Model is irrelevant here - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.True(result); - } - - [Fact] - public void Expression_Should_ReturnFalse_When_CaseSensitivePatternDoesNotMatch() - { - const string expressionCode = - """ - let str = 'abc' - $case_sensitive on - return str like 'A%' - """; - - var simpra = new Simpra(); - var model = GetTestModel(); // Model is irrelevant here - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.False(result); - } - - [Fact] - public void Expression_Should_ReturnTrue_When_AmountIsGreaterThanHundred() - { - const string expressionCode = - """ - let transfer = Transfer - let amount = transfer.Amount - return amount > 100 * 2 / 1.1 - """; - - var simpra = new Simpra(); - var model = GetTestModel(); - model.Transfer!.Amount = 200; - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.True(result); - } - - [Fact] - public void Expression_Should_ReturnTrue_When_AmountIsGreaterThanFifty_And_CurrencyIsUSD() - { - const string expressionCode = "return Transfer.Amount > 50 and Transfer.Currency is 'USD'"; - - var simpra = new Simpra(); - var model = GetTestModel(); - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.True(result); - } - - [Fact] - public void Expression_Should_ReturnFalse_When_AmountIsNotGreaterThanFifty_And_CurrencyIsNotUSD() - { - const string expressionCode = "return not (Transfer.Amount > 50 and Transfer.Currency is 'USD')"; - - var simpra = new Simpra(); - var model = GetTestModel(); - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.False(result); - } - - [Fact] - public void Expression_Should_ReturnTrue_When_CurrencyIsInList() - { - const string expressionCode = "return Transfer.Currency in ['USD', 'EUR']"; - - var simpra = new Simpra(); - var model = GetTestModel(); - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.True(result); - } - - [Fact] - public void Expression_Should_ReturnFalse_When_CurrencyIsNotInList() - { - const string expressionCode = "return Transfer.Currency not in ['USD', 'EUR']"; - - var simpra = new Simpra(); - var model = GetTestModel(); - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.False(result); - } - - [Fact] - public void Expression_Should_ReturnTrue_When_CurrencyIsConcatenatedString() - { - const string expressionCode = "return Transfer.Currency is 'US' + 'D'"; - - var simpra = new Simpra(); - var model = GetTestModel(); - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.True(result); - } - - [Fact] - public void Expression_Should_ReturnTrue_When_CurrencyIsInConcatenatedList() - { - const string expressionCode = "return Transfer.Currency in ['USD', 'EUR'] + 'GEL'"; - - var simpra = new Simpra(); - var model = GetTestModel(); - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.True(result); - } - - [Fact] - public void Expression_Should_ReturnTrue_When_AmountIsEqualToFifty() - { - const string expressionCode = "return Transfer.Amount is 50"; - - var simpra = new Simpra(); - var model = GetTestModel(); - model.Transfer!.Amount = 50; - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.True(result); - } - - [Fact] - public void Expression_Should_ReturnTrue_When_NestedConditionsAreMet() - { - const string expressionCode = - "return (Transfer.Amount > 50 and Transfer.Currency is 'USD') or (Transfer.Amount < 20 and Transfer.Currency is 'EUR')"; - - var simpra = new Simpra(); - var model = GetTestModel(); - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.True(result); - } - - [Fact] - public void Expression_Should_ReturnTrue_When_CurrencyIsEmpty() - { - const string expressionCode = "return Transfer.Currency is ''"; - - var simpra = new Simpra(); - var model = GetTestModel(); - model.Transfer!.Currency = ""; - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.True(result); - } - - [Fact] - public void Expression_Should_ReturnHelloWorld_When_StringsAreConcatenated() - { - const string expressionCode = "return 'Hello' + ' ' + 'World!'"; - - var simpra = new Simpra(); - var model = GetTestModel(); - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.Equal("Hello World!", result); - } - - [Fact] - public void Expression_Should_ReturnCurrencyWithString_When_Concatenated() - { - const string expressionCode = "return Transfer.Currency + ' is strong'"; - - var simpra = new Simpra(); - var model = GetTestModel(); - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.Equal("USD is strong", result); - } - - [Fact] - public void Expression_Should_ReturnCurrencyAndAmount_When_Concatenated() - { - const string expressionCode = "return Transfer.Currency + ' - Amount: ' + Transfer.Amount"; - - var simpra = new Simpra(); - var model = GetTestModel(); - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.Equal("USD - Amount: 100", result); - } - - [Fact] - public void Expression_Should_ReturnTrue_When_CurrencyIsInSubtractedList() - { - const string expressionCode = "return Transfer.Currency in ['USD', 'EUR', 'GEL'] - ['GEL']"; - - var simpra = new Simpra(); - var model = GetTestModel(); - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.True(result); - - model.Transfer!.Currency = "GEL"; - result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.False(result); - } - - [Fact] - public void Expression_Should_ReturnEmptyArray_When_SubtractingIdenticalLists() - { - const string expressionCode = "return ['USD', 'EUR'] - ['USD', 'EUR']"; - - var simpra = new Simpra(); - var model = GetTestModel(); - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.Empty(result); - } - - [Fact] - public void Expression_Should_ReturnOriginalArray_When_SubtractingNonOverlappingLists() - { - const string expressionCode = "return ['USD', 'EUR'] - ['GEL']"; - - var simpra = new Simpra(); - var model = GetTestModel(); - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.Contains("USD", result); - Assert.Contains("EUR", result); - Assert.DoesNotContain("GEL", result); - } - - [Fact] - public void Expression_Should_UpdateObjectProperties_And_ReturnFalse() - { - const string expressionCode = """ - $mutable on - Transfer.Amount = 15 - Transfer.Currency = 'GBP' - return Transfer.Amount > 100 or Transfer.Currency is 'USD'; - """; - - var simpra = new Simpra(); - var model = GetTestModel(); - - var result = simpra.Execute(model, new TestFunctions(), expressionCode, - new SimpraCompilerOptions { MutabilityOption = MutabilityOption.DefaultImmutable }); - - Assert.Equal(15, model.Transfer!.Amount); - Assert.Equal("GBP", model.Transfer.Currency); - Assert.False(result); - } - - [Fact] - public void ExecuteWitNullableEnum_ShouldReturnValue() - { - const string expressionCode = "return Nint1"; - - var simpra = new Simpra(); - var model = GetTestModel(); // Model is irrelevant here - model.Nint1 = 1; - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - Assert.Equal(1, result); - } - - [Fact] - public void Execute_WithValidColorReference_ShouldReturnGreen() - { - const string expressionCode = "return Color"; - - var simpra = new Simpra(); - var model = GetTestModel(); // Model is irrelevant here - - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.Equal("Green", result); - } - - [Fact] - public void Execute_WithUnknownColorReference_ShouldReturnGreen() - { - const string expressionCode = "return ColorX"; - - var simpra = new Simpra(); - var model = GetTestModel(); // Model is irrelevant here - - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.Equal("Green", result); - } - - [Fact] - public void ExecuteSyntax_WithArithmeticOperations_ShouldReturnCorrectValue() - { - const string expressionCode = "return ColorM"; - - var simpra = new Simpra(); - var model = GetTestModel(); // Model is irrelevant here - - var result = simpra.Execute(model, new TestFunctions(), expressionCode); - - Assert.Equal("Green", result); - } - - private static TestModel GetTestModel() - { - return new TestModel { Transfer = new Transfer { Amount = 100, Currency = "USD" }, Customer = new Customer { Id = 1, Status = 1 }, Remittance = "Test" }; - } - - public class BaseFunctions - { - public static string CallBaseStaticMethod() => "BaseStaticMethod"; - - public static string CallBaseMethod() => "BaseMethod"; - } - - public interface IFunctions : IBaseFunctions - { - string Upper(string str); - } - - public interface IBaseFunctions - { - string Lower(string str); - } - - public class TestFunctions : BaseFunctions, IFunctions - { - // ReSharper disable UnusedMember.Global - public static string[] ListSomeCountries(string key) - { - return ["RU", "BE", key]; - } - - public static int[] ListOfCustomerIds(string key) - { - return [1, 2]; - } - -#pragma warning disable S2325 - public string Upper(string str) => str.ToUpper(); - - public string Lower(string str) => str.ToLower(); - - public ValueTask> ListOfCurrencyCodes(string name) => ValueTask.FromResult(new List() { "EUR", "GEL" }); - - public string[] List(string key) - { - return ["RU", "BE"]; - } - - public ValueTask BigListAsync(string key, CancellationToken cancellationToken) -#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously - { - return ValueTask.FromResult(new[] { "RU", "BE" }); - } -#pragma warning restore S2325 - // ReSharper restore UnusedMember.Global - } -} diff --git a/tests/AltaSoft.Simpra.tests/SimpraMetaDataTest.cs b/tests/AltaSoft.Simpra.tests/SimpraMetaDataTest.cs index edd7228..3b8ba98 100644 --- a/tests/AltaSoft.Simpra.tests/SimpraMetaDataTest.cs +++ b/tests/AltaSoft.Simpra.tests/SimpraMetaDataTest.cs @@ -61,10 +61,9 @@ public void GetTypeModel_Should_ReturnCorrectModel_ForBaseClass() var typeModel = metadataService.GetTypeModel("MyClass"); - Assert.NotNull(typeModel); Assert.NotNull(typeModel); Assert.Equal("MyClass", typeModel.Name); - //Assert.Contains(typeModel.Properties, p => p.Name == nameof(MyClass.Base)); + Assert.Contains(typeModel.Properties, p => p.Name == nameof(MyClass.Base)); Assert.Contains(typeModel.Functions, f => f.Name == nameof(MyClass.Father)); } } diff --git a/tests/AltaSoft.Simpra.tests/UnitTestsSmartPurposeModel.cs b/tests/AltaSoft.Simpra.tests/UnitTestsSmartPurposeModel.cs index 3432dbd..9fa1f40 100644 --- a/tests/AltaSoft.Simpra.tests/UnitTestsSmartPurposeModel.cs +++ b/tests/AltaSoft.Simpra.tests/UnitTestsSmartPurposeModel.cs @@ -1,9 +1,11 @@ +using AltaSoft.Simpra.Tests.Models; + namespace AltaSoft.Simpra.Tests; public class UnitTestsSmartPurposeModel { [Fact] - public void ExecuteExpression_ReturnsExpectedResult() + public void DictionaryValue_ConcatenatedWithStringLiteral_ReturnsConcatenatedString() { const string expressionCode = "return P['Key1'] + 'A'"; @@ -14,10 +16,107 @@ public void ExecuteExpression_ReturnsExpectedResult() var result = simpra.Execute(model, expressionCode, null); Assert.Equal("Value1A", result); } + + [Fact] + public void NullCustomer_DomainPrimitiveComparison_ReturnsFalseInsteadOfThrowing() + { + const string expressionCode = "return Customer.CustomerId is 1"; + + var simpra = new Simpra(); + var model = new SmartPurposeModel { Customer = null }; + + var result = simpra.Execute(model, expressionCode, null); + Assert.False(result); + } + + [Fact] + public void NullCustomer_DomainPrimitiveNotEqualComparison_ReturnsTrue() + { + const string expressionCode = "return Customer.CustomerId is not 1"; + + var simpra = new Simpra(); + var model = new SmartPurposeModel { Customer = null }; + + var result = simpra.Execute(model, expressionCode, null); + Assert.True(result); + } + + [Fact] + public void NullCustomer_HasValue_ReturnsFalse() + { + const string expressionCode = "return Customer has value"; + + var simpra = new Simpra(); + var model = new SmartPurposeModel { Customer = null }; + + var result = simpra.Execute(model, expressionCode, null); + Assert.False(result); + } + + [Fact] + public void NullCustomer_DomainPrimitiveValue_ReturnsDefaultInsteadOfThrowing() + { + const string expressionCode = "return Customer.CustomerId"; + + var simpra = new Simpra(); + var model = new SmartPurposeModel { Customer = null }; + + var result = simpra.Execute(model, expressionCode, null); + Assert.Equal(0, result); + } + + [Fact] + public void NonNullCustomer_DomainPrimitiveComparison_ReturnsTrueWhenEqual() + { + const string expressionCode = "return Customer.CustomerId is 1"; + + var simpra = new Simpra(); + var model = new SmartPurposeModel { Customer = new CustomerX { CustomerId = 1 } }; + + var result = simpra.Execute(model, expressionCode, null); + Assert.True(result); + } + + [Fact] + public void NonNullCustomer_DomainPrimitiveComparison_ReturnsFalseWhenNotEqual() + { + const string expressionCode = "return Customer.CustomerId is 1"; + + var simpra = new Simpra(); + var model = new SmartPurposeModel { Customer = new CustomerX { CustomerId = 2 } }; + + var result = simpra.Execute(model, expressionCode, null); + Assert.False(result); + } + + [Fact] + public void NonNullCustomer_HasValue_ReturnsTrue() + { + const string expressionCode = "return Customer has value"; + + var simpra = new Simpra(); + var model = new SmartPurposeModel { Customer = new CustomerX { CustomerId = 1 } }; + + var result = simpra.Execute(model, expressionCode, null); + Assert.True(result); + } + + [Fact] + public void NonNullCustomer_DomainPrimitiveValue_ReturnsActualValue() + { + const string expressionCode = "return Customer.CustomerId"; + + var simpra = new Simpra(); + var model = new SmartPurposeModel { Customer = new CustomerX { CustomerId = 42 } }; + + var result = simpra.Execute(model, expressionCode, null); + Assert.Equal(42, result); + } } public sealed class SmartPurposeModel { + public CustomerX? Customer { get; set; } public Dictionary P { get; } = new(); public void SetValue(string key, string? value) @@ -27,3 +126,8 @@ public void SetValue(string key, string? value) public string GetValue(string key) => P.TryGetValue(key, out var result) ? result : string.Empty; } + +public class CustomerX +{ + public CustomerId CustomerId { get; set; } +} diff --git a/tests/AltaSoft.Simpra.tests/VariableAndMutabilityTests.cs b/tests/AltaSoft.Simpra.tests/VariableAndMutabilityTests.cs new file mode 100644 index 0000000..24c6c3b --- /dev/null +++ b/tests/AltaSoft.Simpra.tests/VariableAndMutabilityTests.cs @@ -0,0 +1,141 @@ +using AltaSoft.Simpra.Tests.Models; +using static AltaSoft.Simpra.Tests.Models.TestModelFactory; + +namespace AltaSoft.Simpra.Tests; + +public class VariableAndMutabilityTests +{ + [Fact] + public void ExecuteExpression_Should_UpdateCustomerIdAndReturnSum_When_MutabilityIsOn() + { + const string expressionCode = + """ + $mutable on + let Y = Nint2 + let list = Values + let X = CustomerId + CustomerId = 12 + return X + Y + """; + + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = simpra.Execute(model, new TestFunctions(), expressionCode, + new SimpraCompilerOptions { MutabilityOption = MutabilityOption.DefaultImmutable }); + + Assert.Equal(31, result); + Assert.Equal(12, (int)model.CustomerId); + } + + [Fact] + public void Expression_Should_ReturnSixty_When_XIsNotFive() + { + const string expressionCode = + """ + let X = '50' + if X is not '5' then + X = '60' + end + return X + """; + + var simpra = new Simpra(); + var model = GetTestModel(); // Model is irrelevant here + var result = simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.Equal("60", result); + } + + [Fact] + public void Expression_Should_UpdateObjectProperties_And_ReturnFalse() + { + const string expressionCode = """ + $mutable on + Transfer.Amount = 15 + Transfer.Currency = 'GBP' + return Transfer.Amount > 100 or Transfer.Currency is 'USD'; + """; + + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = simpra.Execute(model, new TestFunctions(), expressionCode, + new SimpraCompilerOptions { MutabilityOption = MutabilityOption.DefaultImmutable }); + + Assert.Equal(15, model.Transfer!.Amount); + Assert.Equal("GBP", model.Transfer.Currency); + Assert.False(result); + } + + [Fact] + public void CompoundAssignment_PlusEquals_OnLetListVariable_ShouldAppendValue() + { + const string expressionCode = """ + $mutable on + let list = [1, 2, 3] + list += 4 + return list + """; + + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = simpra.Execute, TestModel, TestFunctions>(model, new TestFunctions(), expressionCode, + new SimpraCompilerOptions { MutabilityOption = MutabilityOption.DefaultImmutable }); + + Assert.Equal([1, 2, 3, 4], result); + } + + [Fact] + public void CompoundAssignment_MinusEquals_OnLetListVariable_ShouldRemoveValue() + { + const string expressionCode = """ + $mutable on + let list = [1, 2, 3] + list -= 2 + return list + """; + + var simpra = new Simpra(); + var model = GetTestModel(); + + var result = simpra.Execute, TestModel, TestFunctions>(model, new TestFunctions(), expressionCode, + new SimpraCompilerOptions { MutabilityOption = MutabilityOption.DefaultImmutable }); + + Assert.Equal([1, 3], result); + } + + [Fact] + public void MutableDirective_WhenCompilerOptionsAreImmutable_ShouldThrowSimpraException() + { + const string expressionCode = """ + $mutable on + return 1 + """; + + var simpra = new Simpra(); + var model = GetTestModel(); + + var f = () => simpra.Execute(model, new TestFunctions(), expressionCode, + new SimpraCompilerOptions { MutabilityOption = MutabilityOption.Immutable }); + + Assert.Throws(() => f()); + } + + [Fact] + public void Assignment_WithoutMutableDirective_ShouldThrowSimpraException() + { + const string expressionCode = """ + Transfer.Amount = 500 + return Transfer.Amount + """; + + var simpra = new Simpra(); + var model = GetTestModel(); + + var f = () => simpra.Execute(model, new TestFunctions(), expressionCode); + + Assert.Throws(() => f()); + } +}