| // |
| // 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 org.apache.plc4net.tools.codegen; |
| using org.apache.plc4net.tools.codegen.model.terms; |
| using org.apache.plc4net.tools.codegen.output; |
| using Xunit; |
| |
| namespace org.apache.plc4net.test.codegen |
| { |
| /// <summary> |
| /// The mspec expression engine: <see cref="MspecExpressionParser"/> (text |
| /// -> <see cref="Term"/> tree, via the <c>Expression.g4</c> grammar) and |
| /// <see cref="CSharpExpressionRenderer"/> (tree -> C#). Every field that |
| /// carries a formula - array <c>count</c> / <c>length</c>, <c>implicit</c>, |
| /// <c>virtual</c>, <c>const</c>, discriminator values - depends on both. |
| /// </summary> |
| public class ExpressionTests |
| { |
| // ── literals ───────────────────────────────────────────── |
| |
| [Theory] |
| [InlineData("42", "42")] |
| [InlineData("0", "0")] |
| [InlineData("0x0E", "0x0E")] |
| [InlineData("0xFF", "0xFF")] |
| [InlineData("true", "true")] |
| [InlineData("false", "false")] |
| [InlineData("null", "null")] |
| [InlineData("1.5", "1.5")] |
| public void Literals_round_trip(string mspec, string expected) |
| { |
| Assert.Equal(expected, MspecExpressionParser.Parse(mspec).ToString()); |
| } |
| |
| [Fact] |
| public void String_literal_drops_the_quotes() |
| { |
| var term = Assert.IsType<StringLiteral>(MspecExpressionParser.Parse("\"UTF8\"")); |
| Assert.Equal("UTF8", term.Value); |
| } |
| |
| // ── operators and precedence ───────────────────────────── |
| |
| [Theory] |
| [InlineData("1 + 2", "(1 + 2)")] |
| [InlineData("1 + 2 * 3", "(1 + (2 * 3))")] |
| [InlineData("(1 + 2) * 3", "((1 + 2) * 3)")] |
| [InlineData("byteCount - 6", "(byteCount - 6)")] |
| [InlineData("numberOfValues * 8", "(numberOfValues * 8)")] |
| [InlineData("(COUNT(fifoValue) * 2) + 2", "((COUNT(fifoValue) * 2) + 2)")] |
| [InlineData("a >= b && c != d", "((a >= b) && (c != d))")] |
| [InlineData("!flag", "!flag")] |
| [InlineData("-1", "-1")] |
| [InlineData("a ? b : c", "(a ? b : c)")] |
| public void Operators_parse_with_the_right_shape(string mspec, string expectedTree) |
| { |
| Assert.Equal(expectedTree, MspecExpressionParser.Parse(mspec).ToString()); |
| } |
| |
| // ── identifier segments: name (args)? [index]? (. child)? ─ |
| |
| [Fact] |
| public void Dotted_path_becomes_a_child_chain() |
| { |
| var v = Assert.IsType<VariableLiteral>(MspecExpressionParser.Parse("pdu.lengthInBytes")); |
| Assert.Equal("pdu", v.Name); |
| Assert.Null(v.Args); |
| Assert.NotNull(v.Child); |
| Assert.Equal("lengthInBytes", v.Child.Name); |
| } |
| |
| [Fact] |
| public void Call_captures_arguments() |
| { |
| var v = Assert.IsType<VariableLiteral>(MspecExpressionParser.Parse("COUNT(value)")); |
| Assert.Equal("COUNT", v.Name); |
| Assert.NotNull(v.Args); |
| Assert.Single(v.Args); |
| Assert.Equal("value", v.Args[0].ToString()); |
| } |
| |
| [Fact] |
| public void Static_call_keeps_the_string_target_and_the_rest() |
| { |
| var v = Assert.IsType<VariableLiteral>( |
| MspecExpressionParser.Parse("STATIC_CALL(\"rtuCrcCheck\", address, pdu)")); |
| Assert.Equal("STATIC_CALL", v.Name); |
| Assert.NotNull(v.Args); |
| Assert.Equal(3, v.Args.Count); |
| Assert.IsType<StringLiteral>(v.Args[0]); |
| } |
| |
| [Fact] |
| public void Indexing_is_captured() |
| { |
| var v = Assert.IsType<VariableLiteral>(MspecExpressionParser.Parse("items[0].fileNumber")); |
| Assert.Single(v.Index); |
| Assert.Equal("0", v.Index[0].ToString()); |
| Assert.NotNull(v.Child); |
| Assert.Equal("fileNumber", v.Child.Name); |
| } |
| |
| // ── C# rendering ───────────────────────────────────────── |
| |
| [Theory] |
| [InlineData("pdu.lengthInBytes + 1", "(pdu.GetLengthInBytes() + 1)")] |
| [InlineData("COUNT(value)", "value.Count")] |
| [InlineData("COUNT(value) + 6", "(value.Count + 6)")] |
| [InlineData("byteCount - 6", "(byteCount - 6)")] |
| [InlineData("(COUNT(fifoValue) * 2) / 2", "((fifoValue.Count * 2) / 2)")] |
| [InlineData("ARRAY_SIZE_IN_BYTES(items)", "StaticHelper.ArraySizeInBytes(items)")] |
| [InlineData("STATIC_CALL(\"rtuCrcCheck\", address, pdu)", "StaticHelper.RtuCrcCheck(address, pdu)")] |
| [InlineData("numberOfValues * 8", "(numberOfValues * 8)")] |
| [InlineData("errorFlag ? 1 : 0", "(errorFlag ? 1 : 0)")] |
| public void Renders_to_C_sharp(string mspec, string expectedCSharp) |
| { |
| var term = MspecExpressionParser.Parse(mspec); |
| Assert.Equal(expectedCSharp, new CSharpExpressionRenderer().Render(term)); |
| } |
| |
| [Fact] |
| public void The_scope_rewrites_bare_references() |
| { |
| var term = MspecExpressionParser.Parse("startingAddress + quantity"); |
| var renderer = new CSharpExpressionRenderer(new PrefixScope("_value.")); |
| Assert.Equal("(_value.startingAddress + _value.quantity)", renderer.Render(term)); |
| } |
| |
| [Fact] |
| public void Power_operator_maps_to_Math_Pow_not_xor() |
| { |
| var term = MspecExpressionParser.Parse("2 ^ exponent"); |
| Assert.Equal("System.Math.Pow(2, exponent)", new CSharpExpressionRenderer().Render(term)); |
| } |
| |
| [Fact] |
| public void Every_modbus_field_expression_parses() |
| { |
| // The formulas that appear verbatim in modbus.mspec. |
| string[] expressions = |
| { |
| "pdu.lengthInBytes + 1", |
| "COUNT(value)", |
| "COUNT(events) + 6", |
| "byteCount - 6", |
| "(COUNT(fifoValue) * 2) + 2", |
| "(COUNT(fifoValue) * 2) / 2", |
| "ARRAY_SIZE_IN_BYTES(items)", |
| "COUNT(data) + 1", |
| "dataLength - 1", |
| "COUNT(recordData) / 2", |
| "recordLength * 2", |
| "COUNT(objects)", |
| "STATIC_CALL(\"rtuCrcCheck\", address, pdu)", |
| "STATIC_CALL(\"asciiLrcCheck\", address, pdu)", |
| }; |
| |
| var renderer = new CSharpExpressionRenderer(); |
| foreach (var expr in expressions) |
| { |
| var term = MspecExpressionParser.Parse(expr); |
| Assert.False(string.IsNullOrEmpty(renderer.Render(term)), expr); |
| } |
| } |
| |
| [Fact] |
| public void A_broken_expression_reports_where() |
| { |
| var ex = Assert.Throws<MspecParseException>(() => MspecExpressionParser.Parse("1 +")); |
| Assert.Contains("'1 +'", ex.Message); |
| } |
| |
| private sealed class PrefixScope : IExpressionScope |
| { |
| private readonly string _prefix; |
| public PrefixScope(string prefix) => _prefix = prefix; |
| public string ResolveReference(string name) => _prefix + name; |
| } |
| } |
| } |