Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.kebnf text eol=lf
2 changes: 1 addition & 1 deletion SySML2.NET.REST.Tests/RestClientTestFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ namespace SySML2.NET.REST.Tests
/// Suite of tests for the <see cref="RestClient"/> class.
/// </summary>
[TestFixture]
[Category("Integration")]
[Explicit("Host not reachable ATM")]
public class RestClientTestFixture
{
private string baseUri;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ public override ILiteralRational Read(XmlReader xmiReader, Uri currentLocation)

if (!string.IsNullOrWhiteSpace(valueXmlAttribute))
{
if (double.TryParse(valueXmlAttribute, out var valueXmlAttributeAsDouble))
if (double.TryParse(valueXmlAttribute, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var valueXmlAttributeAsDouble))
{
poco.Value = valueXmlAttributeAsDouble;
}
Expand Down Expand Up @@ -658,7 +658,7 @@ public override ILiteralRational Read(XmlReader xmiReader, Uri currentLocation)

if (!string.IsNullOrWhiteSpace(valueValue))
{
if (double.TryParse(valueValue, out var valueValueAsDouble))
if (double.TryParse(valueValue, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var valueValueAsDouble))
{
poco.Value = valueValueAsDouble;
}
Expand Down Expand Up @@ -919,7 +919,7 @@ public override async Task<ILiteralRational> ReadAsync(XmlReader xmiReader, Uri

if (!string.IsNullOrWhiteSpace(valueXmlAttribute))
{
if (double.TryParse(valueXmlAttribute, out var valueXmlAttributeAsDouble))
if (double.TryParse(valueXmlAttribute, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var valueXmlAttributeAsDouble))
{
poco.Value = valueXmlAttributeAsDouble;
}
Expand Down Expand Up @@ -1267,7 +1267,7 @@ public override async Task<ILiteralRational> ReadAsync(XmlReader xmiReader, Uri

if (!string.IsNullOrWhiteSpace(valueValue))
{
if (double.TryParse(valueValue, out var valueValueAsDouble))
if (double.TryParse(valueValue, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var valueValueAsDouble))
{
poco.Value = valueValueAsDouble;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@

namespace SysML2.NET.CodeGenerator.Tests.Extensions
{
using System.IO;
using System.Linq;

using NUnit.Framework;

using SysML2.NET.CodeGenerator.Extensions;
using SysML2.NET.CodeGenerator.Grammar;

[TestFixture]
public class GrammarErrataTestFixture
Expand All @@ -44,10 +46,11 @@ public void VerifyApplyProductions()
Assert.That(GrammarErrata.ApplyProductions(" "), Is.EqualTo(" "));
}

// A grammar carrying none of the corrected productions is returned untouched.
// A grammar carrying none of the corrected productions keeps its content; only line endings
// are normalised, so the correction layer behaves identically on every platform.
const string unrelated = "Foo : Bar =\r\n Baz";

Assert.That(GrammarErrata.ApplyProductions(unrelated), Is.EqualTo(unrelated));
Assert.That(GrammarErrata.ApplyProductions(unrelated), Is.EqualTo("Foo : Bar =\n Baz"));

var corrected = GrammarErrata.ApplyProductions(CaseBodyItemOriginal);

Expand All @@ -72,6 +75,57 @@ public void VerifyApplyProductions()
}
}

/// <summary>
/// Pins the line-ending independence of the production corrections. A multi-line <c>Original</c>
/// used to be written with <c>\r\n</c>, so it matched a CRLF working tree (Windows, with
/// <c>core.autocrlf=true</c>) and matched NOTHING on a LF checkout (Linux CI) — the same commit
/// then generated different builders on the two platforms, and the divergence surfaced only as an
/// unrelated downstream test failure.
/// </summary>
[Test]
public void VerifyApplyProductionsIsLineEndingIndependent()
{
var appliedToCrLf = GrammarErrata.ApplyProductions("// leading\r\nCaseBodyItem : Type =\r\n ActionBodyItem\r\n// trailing");
var appliedToLf = GrammarErrata.ApplyProductions("// leading\nCaseBodyItem : Type =\n ActionBodyItem\n// trailing");

using (Assert.EnterMultipleScope())
{
Assert.That(appliedToCrLf, Is.EqualTo(appliedToLf),
"The same grammar must correct identically whether it was checked out with CRLF or LF endings.");
Assert.That(appliedToLf, Does.Contain("CalculationBodyItem"),
"The CaseBodyItem correction must apply to a LF checkout — this is the case that silently no-opped on Linux CI.");
Assert.That(appliedToCrLf, Does.Contain("CalculationBodyItem"),
"The CaseBodyItem correction must apply to a CRLF checkout.");
}
}

/// <summary>
/// Asserts that every recorded erratum still matches the grammar it corrects, by loading the real
/// KEBNF files through the production loader and then querying what stayed unapplied.
/// </summary>
/// <remarks>
/// An erratum that matches nothing is silently inert — the generator only writes a console note
/// (<c>UmlCoreTextualNotationBuilderGenerator</c>), so nothing fails and the missing correction shows
/// up much later as wrong generated code. Two causes are both worth catching here: OMG fixed the
/// defect upstream and the entry should be pruned, or the entry stopped matching for a mechanical
/// reason such as line endings.
/// <para><c>AppliedRuleNames</c> is static and accumulates across the run, so this assertion is
/// order-independent: earlier fixtures can only ever mark MORE entries applied, never fewer.</para>
/// </remarks>
[Test]
public void VerifyEveryErratumStillMatchesTheGrammar()
{
var textualRulesFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, "datamodel");

GrammarLoader.LoadTextualNotationSpecification(Path.Combine(textualRulesFolder, "KerML-textual-bnf.kebnf"));
GrammarLoader.LoadTextualNotationSpecification(Path.Combine(textualRulesFolder, "SysML-textual-bnf.kebnf"));

var unapplied = GrammarErrata.QueryUnappliedErrata();

Assert.That(unapplied, Is.Empty,
$"Erratum/errata matched nothing against the real grammar and are silently inert: {string.Join(", ", unapplied.Select(erratum => erratum.RuleName))}");
}

[Test]
public void VerifyQueryUnappliedErrata()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// -------------------------------------------------------------------------------------------------
// <copyright file="GuardedBodyItemRuleAnalysisTestFixture.cs" company="Starion Group S.A.">
//
// Copyright 2022-2026 Starion Group S.A.
//
// Licensed 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
//
// http://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.
//
// </copyright>
// ------------------------------------------------------------------------------------------------

namespace SysML2.NET.CodeGenerator.Tests.HandleBarHelpers
{
using System;
using System.IO;
using System.Linq;

using NUnit.Framework;

using SysML2.NET.CodeGenerator.Grammar;
using SysML2.NET.CodeGenerator.Grammar.Model;
using SysML2.NET.CodeGenerator.HandleBarHelpers;

/// <summary>
/// Test fixture for the <see cref="GuardedBodyItemRuleAnalysis" /> class
/// </summary>
[TestFixture]
public class GuardedBodyItemRuleAnalysisTestFixture
{
/// <summary>
/// The merged KerML + SysML rule set, SysML rules taking precedence by name, exactly as the
/// textual notation builder generator merges them.
/// </summary>
private TextualNotationSpecification textualNotationSpecification;

/// <summary>
/// Loads and merges the KerML and SysML KEBNF grammars.
/// </summary>
[OneTimeSetUp]
public void OneTimeSetup()
{
var textualRulesFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, "datamodel");
var kermlRules = GrammarLoader.LoadTextualNotationSpecification(Path.Combine(textualRulesFolder, "KerML-textual-bnf.kebnf"));
var sysmlRules = GrammarLoader.LoadTextualNotationSpecification(Path.Combine(textualRulesFolder, "SysML-textual-bnf.kebnf"));

var combinedRules = new TextualNotationSpecification();
combinedRules.Rules.AddRange(sysmlRules.Rules);

foreach (var rule in kermlRules.Rules.Where(rule => combinedRules.Rules.All(existingRule => existingRule.RuleName != rule.RuleName)))
{
combinedRules.Rules.Add(rule);
}

this.textualNotationSpecification = combinedRules;
}

/// <summary>
/// Calibrates the structural predicate against the <c>IsGuardedBodyItemRule</c> allowlist it was
/// meant to replace, pinning the measured relationship between the two rather than an equivalence
/// the grammar cannot support.
/// </summary>
/// <remarks>
/// The predicate models the TRAILING-CONSUMER hazard: a brace-positioned <c>X*</c> loop followed by
/// a further consumer of the same cursor. That is real — <c>CaseBodyItem</c> (the rule's own
/// <c>( ResultExpressionMember )?</c>) and <c>DefinitionBodyItem</c> (<c>PortDefinition</c>'s
/// trailing <c>ConjugatedPortDefinitionMember</c>) are both found — and it also finds the two
/// dispatcher arms those rules delegate to.
/// <para>It does NOT reproduce the allowlist, and cannot: <c>InterfaceBodyItem</c> is correctly
/// absent, because <c>InterfaceBody</c> is the last element of both <c>InterfaceDefinition</c> and
/// <c>InterfaceUsage</c> and so has no trailing consumer at all. Its guard is nonetheless
/// load-bearing for the SECOND hazard the allowlist encodes — the item dispatcher declines an
/// unmatched element without advancing the cursor, so an unguarded loop spins — which is a runtime
/// property of the hand-coded dispatcher, not a grammar property this analysis can see.</para>
/// </remarks>
[Test]
public void VerifyCompute()
{
var guardedRuleNames = GuardedBodyItemRuleAnalysis.Compute(this.textualNotationSpecification.Rules);

Console.WriteLine($"Computed guarded body-item rules ({guardedRuleNames.Count}): {string.Join(", ", guardedRuleNames.OrderBy(name => name, StringComparer.Ordinal))}");

using (Assert.EnterMultipleScope())
{
Assert.That(guardedRuleNames, Does.Contain("CaseBodyItem"),
"CaseBody's `'{' CaseBodyItem* ( ownedRelationship += ResultExpressionMember )? '}'` is the canonical trailing-consumer threat and must be detected.");
Assert.That(guardedRuleNames, Does.Contain("DefinitionBodyItem"),
"PortDefinition's trailing `ownedRelationship += ConjugatedPortDefinitionMember` threatens the DefinitionBodyItem loop reached through Definition -> DefinitionBody.");
Assert.That(guardedRuleNames, Does.Contain("ActionBodyItem"),
"ActionBodyItem is reached as a bare dispatcher arm of the threatened CaseBodyItem -> CalculationBodyItem chain.");
Assert.That(guardedRuleNames, Does.Contain("CalculationBodyItem"),
"CalculationBodyItem is the bare dispatcher arm between CaseBodyItem and ActionBodyItem and shares their cursor population.");
Assert.That(guardedRuleNames, Does.Not.Contain("InterfaceBodyItem"),
"InterfaceBody is the last element of both InterfaceDefinition and InterfaceUsage, so the trailing-consumer analysis must report no threat — its guard covers the separate non-advancing-dispatcher hazard instead.");
}
}
}
}
28 changes: 24 additions & 4 deletions SysML2.NET.CodeGenerator/Extensions/GrammarErrata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ public static class GrammarErrata
private static readonly GrammarProductionErratum[] ProductionEntries =
[
new("CaseBodyItem",
"CaseBodyItem : Type =\r\n ActionBodyItem",
"CaseBodyItem : Type =\r\n CalculationBodyItem",
"CaseBodyItem : Type =\n ActionBodyItem",
"CaseBodyItem : Type =\n CalculationBodyItem",
"SysML 8.2.2.22.1 gives CaseBodyItem the alternative 'ActionBodyItem', which reaches no " +
"ReturnParameterMember, so 'return' cannot be written in a case body. Three independent " +
"sources say it must be: (1) the pilot implementation's own grammar uses " +
Expand Down Expand Up @@ -143,6 +143,13 @@ public static string ApplyTarget(string ruleName, string targetElementName)
/// correction cannot partially match, and re-applying it to already-corrected text is a no-op.
/// Both KEBNF files are passed through this, so an entry only fires against the file that carries
/// its production.
/// <para>Line endings are normalised to <c>\n</c> FIRST, and every <c>Original</c> / <c>Replacement</c>
/// is written with <c>\n</c>. A multi-line correction is otherwise silently inert on whichever
/// platform disagrees with the checked-out line endings: the entries used to carry <c>\r\n</c>, which
/// matched on Windows (<c>core.autocrlf=true</c> yields a CRLF working tree) and matched NOTHING on
/// Linux CI, so the same commit generated different builders on the two platforms and the mismatch
/// surfaced only as a downstream test failure. Normalising also makes the text handed to the parser
/// byte-identical across platforms, so the whole generation pipeline is deterministic.</para>
/// </remarks>
public static string ApplyProductions(string kebnfSource)
{
Expand All @@ -151,16 +158,29 @@ public static string ApplyProductions(string kebnfSource)
return kebnfSource;
}

var normalisedSource = NormaliseLineEndings(kebnfSource);

return ProductionEntries
.Where(erratum => kebnfSource.Contains(erratum.Original, StringComparison.Ordinal))
.Aggregate(kebnfSource, (corrected, erratum) =>
.Where(erratum => normalisedSource.Contains(erratum.Original, StringComparison.Ordinal))
.Aggregate(normalisedSource, (corrected, erratum) =>
{
AppliedRuleNames.Add(erratum.RuleName);

return corrected.Replace(erratum.Original, erratum.Replacement);
});
}

/// <summary>
/// Normalises CRLF and lone CR line endings to <c>\n</c> so a multi-line correction matches
/// regardless of how the grammar file was checked out.
/// </summary>
/// <param name="source">The grammar text as read from disk.</param>
/// <returns>The text with every line ending expressed as <c>\n</c>.</returns>
private static string NormaliseLineEndings(string source)
{
return source.Replace("\r\n", "\n").Replace('\r', '\n');
}

/// <summary>
/// Returns the corrections that matched nothing during this generator run.
/// </summary>
Expand Down
Loading
Loading