| // | |
| // Licensed to the Apache Software Foundation (ASF) under one | |
| // or more contributor license agreements. See the NOTICE file | |
| // distributed with this work for additional information | |
| // regarding copyright ownership. The ASF licenses this file | |
| // to you under the Apache License, Version 2.0 (the | |
| // "License"); you may not use this file except in compliance | |
| // with the License. You may obtain a copy of the License at | |
| // | |
| // https://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, | |
| // software distributed under the License is distributed on an | |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| // KIND, either express or implied. See the License for the | |
| // specific language governing permissions and limitations | |
| // under the License. | |
| // | |
| using System; | |
| using System.Collections.Generic; | |
| using System.IO; | |
| using Antlr4.Runtime; | |
| using org.apache.plc4net.tools.codegen; | |
| using org.apache.plc4net.tools.codegen.grammar; | |
| using org.apache.plc4net.tools.codegen.model.terms; | |
| using Xunit; | |
| namespace org.apache.plc4net.test.codegen; | |
| public class ParserSmokeTests | |
| { | |
| public static TheoryData<string> MSpecFixtures => new() | |
| { | |
| "mspec.example", | |
| "mspec.example2", | |
| "mspec.example3" | |
| }; | |
| public static TheoryData<string> Expressions => new() | |
| { | |
| "payload.lengthInBytes+4", | |
| "CAST(parameter,S7ParameterUserData).items(hurz)[0]", | |
| "enabled && (length >= 4 ? true : false)", | |
| "0x2a << 1" | |
| }; | |
| [Theory] | |
| [MemberData(nameof(MSpecFixtures))] | |
| public void ParsesExistingMSpecFixture(string fixture) | |
| { | |
| string input = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "Fixtures", fixture)); | |
| AssertMSpecParses(input); | |
| } | |
| [Theory] | |
| [MemberData(nameof(Expressions))] | |
| public void ParsesRepresentativeExpression(string input) | |
| { | |
| var lexer = new ExpressionLexer(CharStreams.fromString(input)); | |
| var lexerErrors = new CollectingErrorListener<int>(); | |
| lexer.RemoveErrorListeners(); | |
| lexer.AddErrorListener(lexerErrors); | |
| var tokens = new CommonTokenStream(lexer); | |
| var parser = new ExpressionParser(tokens); | |
| var parserErrors = new CollectingErrorListener<IToken>(); | |
| parser.RemoveErrorListeners(); | |
| parser.AddErrorListener(parserErrors); | |
| parser.expressionString(); | |
| Assert.Empty(lexerErrors.Errors); | |
| Assert.Empty(parserErrors.Errors); | |
| Assert.Equal(TokenConstants.EOF, parser.CurrentToken.Type); | |
| } | |
| [Fact] | |
| public void ParsesIndentedEmptyLineThroughCompatibilityPredicate() | |
| { | |
| const string input = "[type Sample\n [simple uint 8 value]\n \n]\n"; | |
| var lexer = new MSpecLexer(CharStreams.fromString(input)); | |
| var tokens = new CommonTokenStream(lexer); | |
| tokens.Fill(); | |
| Assert.Contains(tokens.GetTokens(), token => | |
| token.Type == MSpecLexer.EmptyLine && token.Text == " \n"); | |
| AssertMSpecParses(input); | |
| } | |
| [Theory] | |
| [InlineData("42", "42")] | |
| [InlineData("0x0E", "0x0E")] | |
| [InlineData("true", "true")] | |
| [InlineData("false", "false")] | |
| [InlineData("null", "null")] | |
| [InlineData("1.5", "1.5")] | |
| [InlineData("1 + 2 * 3", "(1 + (2 * 3))")] | |
| [InlineData("(1 + 2) * 3", "((1 + 2) * 3)")] | |
| [InlineData("8 / 2 % 3", "((8 / 2) % 3)")] | |
| [InlineData("2 ^ 3 ^ 2", "(2 ^ (3 ^ 2))")] | |
| [InlineData("0x2a << 1", "(0x2a << 1)")] | |
| [InlineData("a >= b && c != d", "((a >= b) && (c != d))")] | |
| [InlineData("a & b | c", "((a & b) | c)")] | |
| [InlineData("a || b", "(a || b)")] | |
| [InlineData("!flag", "!flag")] | |
| [InlineData("-1", "-1")] | |
| [InlineData("a ? b : c", "(a ? b : c)")] | |
| public void BuildsExpressionTree(string input, string expected) | |
| { | |
| Assert.Equal(expected, MspecExpressionParser.Parse(input).ToString()); | |
| } | |
| [Theory] | |
| [InlineData("0x0E")] | |
| [InlineData("0xFFFFFFFFFFFFFFFF")] | |
| [InlineData("0x10000000000000000")] | |
| public void PreservesHexadecimalLiteralTextWithoutSignedConversion(string input) | |
| { | |
| var literal = Assert.IsType<HexadecimalLiteral>(MspecExpressionParser.Parse(input)); | |
| Assert.Equal(input, literal.Text); | |
| } | |
| [Fact] | |
| public void BuildsCallsIndexesAndMemberChains() | |
| { | |
| var call = Assert.IsType<VariableLiteral>( | |
| MspecExpressionParser.Parse("CAST(parameter, Type).items[0].value")); | |
| Assert.Equal("CAST", call.Name); | |
| Assert.NotNull(call.Args); | |
| Assert.Equal(2, call.Args.Count); | |
| Assert.Equal("items", call.Child?.Name); | |
| Assert.Equal("0", Assert.Single(call.Child!.Index).ToString()); | |
| Assert.Equal("value", call.Child.Child?.Name); | |
| } | |
| [Theory] | |
| [InlineData("\"a\\\"b\"", "a\"b", "\"a\\\"b\"")] | |
| [InlineData("'a\\'b'", "a'b", "\"a'b\"")] | |
| [InlineData("\"C:\\\\temp\"", "C:\\temp", "\"C:\\\\temp\"")] | |
| public void UnquotesAndRendersStringLiterals(string input, string value, string rendered) | |
| { | |
| var literal = Assert.IsType<StringLiteral>(MspecExpressionParser.Parse(input)); | |
| Assert.Equal(value, literal.Value); | |
| Assert.Equal(rendered, literal.ToString()); | |
| } | |
| [Fact] | |
| public void DistinguishesEmptyCallsAndMultipleIndexes() | |
| { | |
| var value = Assert.IsType<VariableLiteral>(MspecExpressionParser.Parse("values()[row][column]")); | |
| Assert.NotNull(value.Args); | |
| Assert.Empty(value.Args); | |
| Assert.Equal(2, value.Index.Count); | |
| } | |
| [Theory] | |
| [InlineData("\"abc\"[0]", "\"abc\"[0]")] | |
| [InlineData("(left + right)[offset]", "(left + right)[offset]")] | |
| [InlineData("(-value)[0]", "(-value)[0]")] | |
| [InlineData("(!flag)[0]", "(!flag)[0]")] | |
| public void PreservesIndexesOnAnyExpression(string input, string expected) | |
| { | |
| Assert.Equal(expected, MspecExpressionParser.Parse(input).ToString()); | |
| } | |
| [Theory] | |
| [InlineData("pdu.lengthInBytes + 1")] | |
| [InlineData("COUNT(events) + 6")] | |
| [InlineData("(COUNT(fifoValue) * 2) / 2")] | |
| [InlineData("ARRAY_SIZE_IN_BYTES(items)")] | |
| [InlineData("STATIC_CALL(\"rtuCrcCheck\", address, pdu)")] | |
| [InlineData("STATIC_CALL(\"asciiLrcCheck\", address, pdu)")] | |
| public void BuildsExpressionsUsedByModbus(string input) | |
| { | |
| Assert.NotNull(MspecExpressionParser.Parse(input)); | |
| } | |
| [Fact] | |
| public void RejectsInvalidExpressionWithSourceLocation() | |
| { | |
| MspecParseException error = | |
| Assert.Throws<MspecParseException>(() => MspecExpressionParser.Parse("1 +")); | |
| Assert.Contains("'1 +'", error.Message); | |
| Assert.Contains("line 1:", error.Message); | |
| } | |
| [Theory] | |
| [InlineData(false)] | |
| [InlineData(true)] | |
| public void RejectsOutOfRangeNumbersAsParseErrors(bool floatingPoint) | |
| { | |
| string input = floatingPoint ? new string('9', 400) + ".0" : new string('9', 30); | |
| MspecParseException error = | |
| Assert.Throws<MspecParseException>(() => MspecExpressionParser.Parse(input)); | |
| Assert.Contains(input, error.Message); | |
| Assert.IsType<OverflowException>(error.InnerException); | |
| } | |
| private static void AssertMSpecParses(string input) | |
| { | |
| var lexer = new MSpecLexer(CharStreams.fromString(input)); | |
| var lexerErrors = new CollectingErrorListener<int>(); | |
| lexer.RemoveErrorListeners(); | |
| lexer.AddErrorListener(lexerErrors); | |
| var tokens = new CommonTokenStream(lexer); | |
| var parser = new MSpecParser(tokens); | |
| var parserErrors = new CollectingErrorListener<IToken>(); | |
| parser.RemoveErrorListeners(); | |
| parser.AddErrorListener(parserErrors); | |
| parser.file(); | |
| Assert.Empty(lexerErrors.Errors); | |
| Assert.Empty(parserErrors.Errors); | |
| Assert.Equal(TokenConstants.EOF, parser.CurrentToken.Type); | |
| } | |
| private sealed class CollectingErrorListener<TSymbol> : IAntlrErrorListener<TSymbol> | |
| { | |
| public List<string> Errors { get; } = []; | |
| public void SyntaxError( | |
| TextWriter output, | |
| IRecognizer recognizer, | |
| TSymbol offendingSymbol, | |
| int line, | |
| int charPositionInLine, | |
| string msg, | |
| RecognitionException e) | |
| { | |
| Errors.Add($"{line}:{charPositionInLine}: {msg}"); | |
| } | |
| } | |
| } |