blob: 4def8408e6008e4a86ee73a383b794e860b5a95f [file]
//
// 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}");
}
}
}