From ac3bfecc051f2eeb3eb6b2c9b377225f6f00cef9 Mon Sep 17 00:00:00 2001 From: Rajesh Jinaga Date: Thu, 20 Oct 2022 12:54:39 +0530 Subject: [PATCH] Add Simpleflow runtime exception information, include line number and statement --- .../SimpleflowCodeVisitor.VisitFunction.cs | 16 +++-- ...leflowCodeVisitor.VisitObjectIdentifier.cs | 2 +- .../SimpleflowCodeVisitor.VisitRule.cs | 1 + .../CodeGenerator/SimpleflowCodeVisitor.cs | 16 +++++ .../InvalidFunctionParameterNameException.cs | 13 +++- .../Exceptions/InvalidPropertyException.cs | 4 +- .../Exceptions/SimpleflowException.cs | 10 +++ .../Exceptions/SimpleflowRuntimeException.cs | 36 ++++++++++ src/Simpleflow/RuntimeContext.cs | 2 + src/Simpleflow/Services/ExecutionService.cs | 17 ++++- src/Simpleflow/Simpleflow.csproj | 2 +- src/Simpleflow/Simpleflow.xml | 42 +++++++++++- test/Simpleflow.Tests/Helpers/TestsHelper.cs | 16 +++++ .../RuntimeErrorExceptionLineAndCodeTest.cs | 68 +++++++++++++++++++ .../Scripting/PredicateStatementsTest.cs | 26 +++++++ 15 files changed, 255 insertions(+), 16 deletions(-) create mode 100644 src/Simpleflow/Exceptions/SimpleflowRuntimeException.cs create mode 100644 test/Simpleflow.Tests/Infrastructure/RuntimeErrorExceptionLineAndCodeTest.cs diff --git a/src/Simpleflow/CodeGenerator/SimpleflowCodeVisitor.VisitFunction.cs b/src/Simpleflow/CodeGenerator/SimpleflowCodeVisitor.VisitFunction.cs index 25641a7..ca61873 100644 --- a/src/Simpleflow/CodeGenerator/SimpleflowCodeVisitor.VisitFunction.cs +++ b/src/Simpleflow/CodeGenerator/SimpleflowCodeVisitor.VisitFunction.cs @@ -54,13 +54,13 @@ private List GetArgumentExpressions(SimpleflowParser.FunctionContext var actualMethodParameters = methodInfo.GetParameters(); var arguments = context.functionArguments().functionArgument(); var argumentsExpressions = new List(); - + if (arguments == null) { return argumentsExpressions; } - CheckInvalidParameters(actualMethodParameters, arguments); + CheckInvalidParameters(actualMethodParameters, arguments, context.FunctionName().GetText()); CheckRepeatedParameters(arguments); foreach (var methodParameter in actualMethodParameters) @@ -113,14 +113,16 @@ where g.Count() > 1 } } - private void CheckInvalidParameters(ParameterInfo[] actualMethodParameters, SimpleflowParser.FunctionArgumentContext[] parameters) + private void CheckInvalidParameters(ParameterInfo[] actualMethodParameters, + SimpleflowParser.FunctionArgumentContext[] parameters, + string functionName) { foreach (var parameter in parameters) { var paramterName = parameter.Identifier().GetText(); if (!actualMethodParameters.Any(p => string.Equals(p.Name, paramterName, System.StringComparison.OrdinalIgnoreCase))) { - throw new InvalidFunctionParameterNameException(paramterName); + throw new InvalidFunctionParameterNameException(paramterName, functionName); } } } @@ -141,12 +143,14 @@ private Expression VisitObjectIdentiferAsPerTargetType(SimpleflowParser.ObjectId private Expression CreateSmartVariableIfObjectIdentiferNotDefined(Type targetType, string name) { - // Variable names are not case sensitive + // Variable name is not case sensitive var smartVar = GetSmartVariable(name); if (smartVar == null) { - throw new InvalidFunctionParameterNameException(name); + // Since smart variable (JSON) can only be used with function argument, + // so here we need to throw function related exception + throw new InvalidFunctionParameterNameException($"Invalid parameter or variable '{name}'"); } // Return if already created diff --git a/src/Simpleflow/CodeGenerator/SimpleflowCodeVisitor.VisitObjectIdentifier.cs b/src/Simpleflow/CodeGenerator/SimpleflowCodeVisitor.VisitObjectIdentifier.cs index a5b9206..a29c583 100644 --- a/src/Simpleflow/CodeGenerator/SimpleflowCodeVisitor.VisitObjectIdentifier.cs +++ b/src/Simpleflow/CodeGenerator/SimpleflowCodeVisitor.VisitObjectIdentifier.cs @@ -78,7 +78,7 @@ private Expression GetFinalPropertyValue(Expression propExp, SimpleflowParser.Id if (field == null) { - throw new InvalidPropertyException($"Invalid property or field '{propName}'"); + throw new InvalidPropertyException(propName); } } diff --git a/src/Simpleflow/CodeGenerator/SimpleflowCodeVisitor.VisitRule.cs b/src/Simpleflow/CodeGenerator/SimpleflowCodeVisitor.VisitRule.cs index 3024712..948d2bd 100644 --- a/src/Simpleflow/CodeGenerator/SimpleflowCodeVisitor.VisitRule.cs +++ b/src/Simpleflow/CodeGenerator/SimpleflowCodeVisitor.VisitRule.cs @@ -27,6 +27,7 @@ public override Expression VisitRuleStmt(SimpleflowParser.RuleStmtContext contex if (childResult != null) { + statements.Add(SetRuntimeState(c)); statements.Add(childResult); } } diff --git a/src/Simpleflow/CodeGenerator/SimpleflowCodeVisitor.cs b/src/Simpleflow/CodeGenerator/SimpleflowCodeVisitor.cs index bc77aa1..9c373ae 100644 --- a/src/Simpleflow/CodeGenerator/SimpleflowCodeVisitor.cs +++ b/src/Simpleflow/CodeGenerator/SimpleflowCodeVisitor.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Collections.Generic; using System.Linq.Expressions; +using Antlr4.Runtime.Tree; using Simpleflow.Parser; using Simpleflow.Exceptions; @@ -85,6 +86,9 @@ private void ProcessEachStatement(SimpleflowParser.ProgramContext context, List< if (childResult != null) { + // Set runtime state for debugging Simpleflow code + statementExpressions.Add( SetRuntimeState(c) ); + /* if current rule is variable statement then store the left expression as variable identifier in variable collection */ if (c.GetType() == typeof(SimpleflowParser.LetStmtContext)) @@ -230,6 +234,7 @@ private void ReplaceVirtualSmartVariablesWithReal(List statementExpr } } + /* Create basic set of variables to access in script */ private List CreateDefaultVariablesAndAssign() { @@ -246,5 +251,16 @@ private List CreateDefaultVariablesAndAssign() }; } + private Expression SetRuntimeState(IParseTree node) + { + var codeLineNumber = ((Antlr4.Runtime.CommonToken)((Antlr4.Runtime.ParserRuleContext)node).Start).Line; + + var property = typeof(RuntimeContext) + .GetProperty("LineNumber", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + + var propertyExpression = Expression.Property(ScriptHelperContextParam, property); + return Expression.Assign(propertyExpression, Expression.Constant(codeLineNumber)); + } + } } diff --git a/src/Simpleflow/Exceptions/InvalidFunctionParameterNameException.cs b/src/Simpleflow/Exceptions/InvalidFunctionParameterNameException.cs index 4ca0909..1244a7f 100644 --- a/src/Simpleflow/Exceptions/InvalidFunctionParameterNameException.cs +++ b/src/Simpleflow/Exceptions/InvalidFunctionParameterNameException.cs @@ -9,11 +9,20 @@ namespace Simpleflow.Exceptions /// public class InvalidFunctionParameterNameException : SimpleflowException { + + /// + /// + /// + /// + public InvalidFunctionParameterNameException(string message) : base(message) { } + /// /// Initializes a new instance of the class with /// a specified variable name. /// - /// The message that describes the error. - public InvalidFunctionParameterNameException(string message) : base(message) { } + /// The message that describes the error. + /// + public InvalidFunctionParameterNameException(string parameterName, string functionName) + : base($"Function '{functionName}' does not have parameter '{parameterName}'") { } } } diff --git a/src/Simpleflow/Exceptions/InvalidPropertyException.cs b/src/Simpleflow/Exceptions/InvalidPropertyException.cs index 6e68714..fe04d47 100644 --- a/src/Simpleflow/Exceptions/InvalidPropertyException.cs +++ b/src/Simpleflow/Exceptions/InvalidPropertyException.cs @@ -12,8 +12,8 @@ public class InvalidPropertyException : SimpleflowException /// Initializes a new instance of the class with /// a specified variable name. /// - /// The message that describes the error. - public InvalidPropertyException(string message): base(message) + /// The message that describes the error. + public InvalidPropertyException(string propertyName): base($"Invalid property or field '{propertyName}'") { } diff --git a/src/Simpleflow/Exceptions/SimpleflowException.cs b/src/Simpleflow/Exceptions/SimpleflowException.cs index 39b4d3e..61bb206 100644 --- a/src/Simpleflow/Exceptions/SimpleflowException.cs +++ b/src/Simpleflow/Exceptions/SimpleflowException.cs @@ -20,5 +20,15 @@ public SimpleflowException(string message): base(message) { } + + /// + /// + /// + /// + /// + public SimpleflowException(string message, Exception innerException) : base(message, innerException) + { + + } } } diff --git a/src/Simpleflow/Exceptions/SimpleflowRuntimeException.cs b/src/Simpleflow/Exceptions/SimpleflowRuntimeException.cs new file mode 100644 index 0000000..7762b5b --- /dev/null +++ b/src/Simpleflow/Exceptions/SimpleflowRuntimeException.cs @@ -0,0 +1,36 @@ +// Copyright (c) navtech.io. All rights reserved. +// See License in the project root for license information. + +using System; + +namespace Simpleflow.Exceptions +{ + /// + /// + /// + public class SimpleflowRuntimeException : SimpleflowException + { + /// + /// + /// + /// + /// + /// + /// + public SimpleflowRuntimeException(string message, int lineNumber, string statement, Exception innerException) : base(message, innerException) + { + LineNumber = lineNumber; + Statement = statement; + } + + /// + /// Gets line number where error has occurred + /// + public int LineNumber { get; } + + /// + /// Gets statement where error has occurred + /// + public string Statement { get; } + } +} diff --git a/src/Simpleflow/RuntimeContext.cs b/src/Simpleflow/RuntimeContext.cs index 078b884..94a8bbe 100644 --- a/src/Simpleflow/RuntimeContext.cs +++ b/src/Simpleflow/RuntimeContext.cs @@ -37,6 +37,8 @@ internal RuntimeContext(FlowOutput flowOutput, CancellationToken token) /// Gets cancellation token /// public CancellationToken CancellationToken => _token; + + internal int LineNumber { get;set; } } } diff --git a/src/Simpleflow/Services/ExecutionService.cs b/src/Simpleflow/Services/ExecutionService.cs index 37df893..e1c7084 100644 --- a/src/Simpleflow/Services/ExecutionService.cs +++ b/src/Simpleflow/Services/ExecutionService.cs @@ -2,6 +2,7 @@ // See License in the project root for license information. using System; +using Simpleflow.Exceptions; namespace Simpleflow.Services { @@ -35,10 +36,22 @@ public void Run(FlowContext context, NextPipelineService next) // Add trace for debugging context.Trace?.CreateNewTracePoint(nameof(ExecutionService)); - var scriptHelperContext = new RuntimeContext(context.Output, + var scriptContext = new RuntimeContext(context.Output, context.Options?.CancellationToken ?? default); - context.Internals.CompiledScript?.Invoke(context.Argument, context.Output, scriptHelperContext); + try + { + context.Internals.CompiledScript?.Invoke(context.Argument, context.Output, scriptContext); + } + catch(Exception ex) + { + // Get statement where error has occurred + var lines = context.Script?.Split('\n') ?? new string[] { }; + var code = lines.Length >= scriptContext.LineNumber && scriptContext.LineNumber > 0 ? lines[scriptContext.LineNumber-1] : context.Script; + + // throw + throw new SimpleflowRuntimeException(ex.Message, scriptContext.LineNumber, code, ex); + } next?.Invoke(context); } diff --git a/src/Simpleflow/Simpleflow.csproj b/src/Simpleflow/Simpleflow.csproj index 1e88c1b..fc492fe 100644 --- a/src/Simpleflow/Simpleflow.csproj +++ b/src/Simpleflow/Simpleflow.csproj @@ -3,7 +3,7 @@ net48;netcoreapp3.1;net6.0; PackageIcon.png 1.0.12 - beta2 + README.md diff --git a/src/Simpleflow/Simpleflow.xml b/src/Simpleflow/Simpleflow.xml index 27776c2..8ba211b 100644 --- a/src/Simpleflow/Simpleflow.xml +++ b/src/Simpleflow/Simpleflow.xml @@ -146,10 +146,17 @@ + + + + + + Initializes a new instance of the class with a specified variable name. - The message that describes the error. + The message that describes the error. + @@ -161,7 +168,7 @@ Initializes a new instance of the class with a specified variable name. - The message that describes the error. + The message that describes the error. @@ -175,6 +182,37 @@ The message that describes the error. + + + + + + + + + + + + + + + + + + + + + + + + Gets line number where error has occurred + + + + + Gets statement where error has occurred + + The exception is thrown when any syntax errors are present in the Simpleflow script. diff --git a/test/Simpleflow.Tests/Helpers/TestsHelper.cs b/test/Simpleflow.Tests/Helpers/TestsHelper.cs index 3af63af..509420b 100644 --- a/test/Simpleflow.Tests/Helpers/TestsHelper.cs +++ b/test/Simpleflow.Tests/Helpers/TestsHelper.cs @@ -1,4 +1,6 @@  +using System; + namespace Simpleflow.Tests.Helpers { internal static class TestsHelper @@ -6,5 +8,19 @@ internal static class TestsHelper public static bool IsWindows => System.Runtime.InteropServices.RuntimeInformation.OSDescription.Contains("Windows"); public static string Timezone => IsWindows ? "Eastern Standard Time" : "America/New_York"; + + public static Exception Try(Action callback) + { + try + { + callback(); + } + catch(Exception ex) + { + return ex; + } + return null; + } + } } diff --git a/test/Simpleflow.Tests/Infrastructure/RuntimeErrorExceptionLineAndCodeTest.cs b/test/Simpleflow.Tests/Infrastructure/RuntimeErrorExceptionLineAndCodeTest.cs new file mode 100644 index 0000000..eb266be --- /dev/null +++ b/test/Simpleflow.Tests/Infrastructure/RuntimeErrorExceptionLineAndCodeTest.cs @@ -0,0 +1,68 @@ +// Copyright (c) navtech.io. All rights reserved. +// See License in the project root for license information. + +using Simpleflow.Exceptions; +using Xunit; + +using static Simpleflow.Tests.Helpers.TestsHelper; + +namespace Simpleflow.Tests.Infrastructure +{ + public class RuntimeErrorExceptionLineAndCodeTest + { + [Fact] + public void ErrorLineNumber() + { + // Arrange + + var script = + @" + let x = 10 + error ""test"" + + message 2 / 0 + + output x + "; + + // Act + var exception = Try(() => SimpleflowEngine.Run(script, new object())); + + // Assert + Assert.NotNull(exception); + Assert.IsType(exception); + Assert.Equal(5, ((SimpleflowRuntimeException)exception).LineNumber); + + } + + [Fact] + public void ErrorLineNumberAtRuleStatement() + { + // Arrange + + var script = + @" + let x = 10 + error ""test"" + + rule when 1 == 1 then + + message 2 / 0 + + output x + "; + + // Act + var exception = Try(() => SimpleflowEngine.Run(script, new object())); + + // Assert + Assert.NotNull(exception); + Assert.IsType(exception); + + var sre = ((SimpleflowRuntimeException)exception); + Assert.Equal(7, sre.LineNumber); + Assert.Equal(" message 2 / 0\r", sre.Statement); + + } + } +} diff --git a/test/Simpleflow.Tests/Scripting/PredicateStatementsTest.cs b/test/Simpleflow.Tests/Scripting/PredicateStatementsTest.cs index 2845582..71ae029 100644 --- a/test/Simpleflow.Tests/Scripting/PredicateStatementsTest.cs +++ b/test/Simpleflow.Tests/Scripting/PredicateStatementsTest.cs @@ -199,10 +199,36 @@ public void CheckShortCircuitingAndOperatorWithFunctions() Assert.Equal("got it", output.Messages[0]); } + [Fact] + public void CheckShortCircuitingAndOperatorWithFunctions2() + { + // Arrange + var script = + @" + rule when $exists(dict: arg.data, key: 'ContentType', fallbackdict: arg.data2) and $str(value: arg.data['ContentType']) in ['test'] then + message 'got it' + "; + + FlowOutput output = SimpleflowEngine.Run(script, + new { + Data = new Dictionary { { "ContentType", "test" } }, + Data2 = new Dictionary { { "ContentType", "test" } } + } + , + new FunctionRegister().Add("exists", (System.Func, IDictionary, string, bool>)Exists)); + + Assert.Equal("got it", output.Messages[0]); + } + public static bool Exists(IDictionary dict, string key) { return dict.ContainsKey(key); } + public static bool Exists(IDictionary dict, IDictionary fallbackDict, string key) + { + return dict.ContainsKey(key); + } + } }