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
4 changes: 3 additions & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ dotnet_diagnostic.S3776.severity = none
# Disable CS1570/CS1584/CS1658 for all codegen
dotnet_diagnostic.CS1570.severity = none
dotnet_diagnostic.CS1584.severity = none
dotnet_diagnostic.CS1658.severity = none
dotnet_diagnostic.CS1658.severity = none
# Disable CA1815: generated value types carry data only; equality semantics are not part of their contract
dotnet_diagnostic.CA1815.severity = none
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
// -------------------------------------------------------------------------------------------------
// <copyright file="ImpliedGuardParserTestFixture.cs" company="Starion Group S.A.">
//
// Copyright (C) 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.Generators.UmlHandleBarsGenerators
{
using System;
using System.Collections.Generic;
using System.Linq;

using NUnit.Framework;

using SysML2.NET.CodeGenerator.Extensions;

using uml4net.Classification;
using uml4net.SimpleClassifiers;
using uml4net.StructuredClassifiers;

[TestFixture]
public class ImpliedGuardParserTestFixture
{
/// <summary>
/// The metaclass names an owning-Type kind test over two alternatives yields.
/// </summary>
private static readonly string[] ExpectedPartTypeNames = ["PartDefinition", "PartUsage"];

/// <summary>
/// The metaclass name an owned-typing kind test yields.
/// </summary>
private static readonly string[] ExpectedDataTypeNames = ["DataType"];

[Test]
public void VerifyParse()
{
using (Assert.EnterMultipleScope())
{
var owningTypeKind = ImpliedGuardParser.Parse("isComposite and owningType <> null and (owningType.oclIsKindOf(PartDefinition) or owningType.oclIsKindOf(PartUsage))");
Assert.That(owningTypeKind.Shape, Is.EqualTo(ImpliedGuardShape.OwningTypeKind));
Assert.That(owningTypeKind.RequiresComposite, Is.True);
Assert.That(owningTypeKind.TypeNames, Is.EqualTo(ExpectedPartTypeNames));

var withoutComposite = ImpliedGuardParser.Parse("owningType <> null and (owningType.oclIsKindOf(ViewDefinition) or owningType.oclIsKindOf(ViewUsage))");
Assert.That(withoutComposite.Shape, Is.EqualTo(ImpliedGuardShape.OwningTypeKind));
Assert.That(withoutComposite.RequiresComposite, Is.False);

var operationCall = ImpliedGuardParser.Parse("isSubactionUsage()");
Assert.That(operationCall.Shape, Is.EqualTo(ImpliedGuardShape.OperationCall));
Assert.That(operationCall.MemberName, Is.EqualTo("isSubactionUsage"));
Assert.That(operationCall.IsNegated, Is.False);

var negated = ImpliedGuardParser.Parse("not isTriggerAction()");
Assert.That(negated.Shape, Is.EqualTo(ImpliedGuardShape.OperationCall));
Assert.That(negated.IsNegated, Is.True);

var withArgument = ImpliedGuardParser.Parse("isSubstateUsage(true)");
Assert.That(withArgument.Shape, Is.EqualTo(ImpliedGuardShape.OperationCall));
Assert.That(withArgument.Literal, Is.EqualTo("true"));

var endCount = ImpliedGuardParser.Parse("ownedEndFeature->size() = 2");
Assert.That(endCount.Shape, Is.EqualTo(ImpliedGuardShape.OwnedEndFeatureCount));
Assert.That(endCount.Literal, Is.EqualTo("2"));

var notEmpty = ImpliedGuardParser.Parse("ownedEndFeatures->notEmpty()");
Assert.That(notEmpty.Shape, Is.EqualTo(ImpliedGuardShape.OwnedEndFeatureCount));
Assert.That(notEmpty.Literal, Is.Null);

var ownedTyping = ImpliedGuardParser.Parse("ownedTyping.type->exists(selectByKind(DataType))");
Assert.That(ownedTyping.Shape, Is.EqualTo(ImpliedGuardShape.OwnedTypingKind));
Assert.That(ownedTyping.TypeNames, Is.EqualTo(ExpectedDataTypeNames));

var membership = ImpliedGuardParser.Parse("owningFeatureMembership <> null and owningFeatureMembership.oclIsKindOf(StakeholderMembership)");
Assert.That(membership.Shape, Is.EqualTo(ImpliedGuardShape.OwningFeatureMembershipKind));

var enumeration = ImpliedGuardParser.Parse("portionKind = PortionKind::timeslice");
Assert.That(enumeration.Shape, Is.EqualTo(ImpliedGuardShape.EnumerationComparison));
Assert.That(enumeration.MemberName, Is.EqualTo("portionKind"));
Assert.That(enumeration.Literal, Is.EqualTo("timeslice"));

var booleanProperty = ImpliedGuardParser.Parse("isIndividual");
Assert.That(booleanProperty.Shape, Is.EqualTo(ImpliedGuardShape.BooleanProperty));

// Multi-line OCL from the XMI must normalise before matching.
var multiLine = ImpliedGuardParser.Parse("owningType <> null and\n (owningType.oclIsKindOf(Behavior) or\n owningType.oclIsKindOf(Step))");
Assert.That(multiLine.Shape, Is.EqualTo(ImpliedGuardShape.OwningTypeKind));
}
}

[Test]
public void VerifyParseRejectsWhatItCannotTranslate()
{
using (Assert.EnterMultipleScope())
{
// A nested oclAsType navigation is beyond the recognised shapes and must NOT be approximated.
var nested = ImpliedGuardParser.Parse("isComposite and owningType <> null and (owningType.oclIsKindOf(Structure) or owningType.oclIsKindOf(Feature) and owningType.oclAsType(Feature).type->exists(oclIsKindOf(Structure)))");
Assert.That(nested.Shape, Is.EqualTo(ImpliedGuardShape.RequiresHandCoding));

// An extra conjunct beyond the recognised owner-kind shape likewise falls back.
var extraConjunct = ImpliedGuardParser.Parse("isComposite and owningType <> null and (owningType.oclIsKindOf(StateDefinition) or owningType.oclIsKindOf(StateUsage)) and source <> null and source.oclIsKindOf(StateUsage)");
Assert.That(extraConjunct.Shape, Is.EqualTo(ImpliedGuardShape.RequiresHandCoding));

Assert.That(ImpliedGuardParser.Parse(null).Shape, Is.EqualTo(ImpliedGuardShape.RequiresHandCoding));
Assert.That(ImpliedGuardParser.Parse(string.Empty).Shape, Is.EqualTo(ImpliedGuardShape.RequiresHandCoding));
Assert.That(ImpliedGuardParser.Parse(" ").Shape, Is.EqualTo(ImpliedGuardShape.RequiresHandCoding));
}
}

/// <summary>
/// Pins how much of the REAL constraint set the parser covers, so a regression in the patterns shows
/// up as a coverage drop rather than silently shifting guards into hand-coding.
/// </summary>
[Test]
public void VerifyCoverageOfTheActualConstraintSet()
{
var guarded = GeneratorSetupFixture.XmiReaderResult
.QueryImpliedRelationshipRules()
.Where(rule => rule.Form == ImpliedRuleForm.GuardedLibrarySpecialization)
.ToList();

var parsed = guarded
.Select(rule => ImpliedGuardParser.Parse(rule.GuardExpression))
.ToList();

var translatable = parsed.Count(expression => expression.Shape != ImpliedGuardShape.RequiresHandCoding);

using (Assert.EnterMultipleScope())
{
Assert.That(guarded, Has.Count.EqualTo(63), "The number of guarded constraints in the abstract syntax changed.");
Assert.That(translatable, Is.EqualTo(46), "Guard-shape coverage changed; re-check the patterns against the OCL.");
}
}

/// <summary>
/// Emits a predicate for every translatable guard in the real constraint set, so a shape that parses
/// but cannot be rendered — an unknown metaclass, say — is caught here rather than as a compile
/// failure in the generated assembly.
/// </summary>
[Test]
public void VerifyEveryTranslatableGuardEmitsAPredicate()
{
var interfaceFqnByName = QueryInterfaceFqnByName();
var enumerationFqnByName = QueryEnumerationFqnByName();

var unrenderable = GeneratorSetupFixture.XmiReaderResult
.QueryImpliedRelationshipRules()
.Where(rule => rule.Form == ImpliedRuleForm.GuardedLibrarySpecialization)
.Select(rule => new
{
rule.ConstraintName,
rule.MetaclassName,
Expression = ImpliedGuardParser.Parse(rule.GuardExpression)
})
.Where(candidate => candidate.Expression.Shape != ImpliedGuardShape.RequiresHandCoding)
.Where(candidate => !interfaceFqnByName.TryGetValue(candidate.MetaclassName, out var declaringFqn)
|| ImpliedGuardEmitter.Emit(candidate.Expression, declaringFqn, interfaceFqnByName, enumerationFqnByName) == null)
.Select(candidate => candidate.ConstraintName)
.ToList();

Assert.That(unrenderable, Is.Empty, $"These guards parse but emit no predicate: {string.Join(", ", unrenderable)}");
}

private static Dictionary<string, string> QueryInterfaceFqnByName()
{
return GeneratorSetupFixture.XmiReaderResult
.QueryContainedAndImported("SysML")
.SelectMany(package => package.PackagedElement.OfType<IClass>())
.GroupBy(umlClass => umlClass.Name, StringComparer.Ordinal)
.ToDictionary(group => group.Key, group => group.First().QueryFullyQualifiedTypeName(), StringComparer.Ordinal);
}

private static Dictionary<string, string> QueryEnumerationFqnByName()
{
return GeneratorSetupFixture.XmiReaderResult
.QueryContainedAndImported("SysML")
.SelectMany(package => package.PackagedElement.OfType<IEnumeration>())
.GroupBy(enumeration => enumeration.Name, StringComparer.Ordinal)
.ToDictionary(group => group.Key, group => group.First().QueryFullyQualifiedTypeName(), StringComparer.Ordinal);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// -------------------------------------------------------------------------------------------------
// <copyright file="ImpliedRelationshipExtensionsTestFixture.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.Generators.UmlHandleBarsGenerators
{
using System.Linq;

using NUnit.Framework;

using SysML2.NET.CodeGenerator.Extensions;

[TestFixture]
public class ImpliedRelationshipExtensionsTestFixture
{
[Test]
public void VerifyQueryImpliedRelationshipRules()
{
var rules = GeneratorSetupFixture.XmiReaderResult.QueryImpliedRelationshipRules();

using (Assert.EnterMultipleScope())
{
// Only `check` rules are semantic constraints. `derive` rules are derivations and `validate`
// rules are validation constraints (KerML §8.3.1); neither implies a Relationship, and both
// carry the same category keywords in their names, so they must not leak in.
Assert.That(rules.Select(rule => rule.ConstraintName), Is.All.StartWith("check"));

// Category totals, cross-checked against an independent scan of the raw XMI.
Assert.That(rules.Count(rule => rule.Category == ImpliedConstraintCategory.Specialization), Is.EqualTo(175));
Assert.That(rules.Count(rule => rule.Category == ImpliedConstraintCategory.Redefinition), Is.EqualTo(15));
Assert.That(rules.Count(rule => rule.Category == ImpliedConstraintCategory.TypeFeaturing), Is.EqualTo(7));
Assert.That(rules.Count(rule => rule.Category == ImpliedConstraintCategory.BindingConnector), Is.EqualTo(11));

// Form split — this is what decides how much is generated versus hand-written.
Assert.That(rules.Count(rule => rule.Form == ImpliedRuleForm.UnconditionalLibrarySpecialization), Is.EqualTo(85));
Assert.That(rules.Count(rule => rule.Form == ImpliedRuleForm.GuardedLibrarySpecialization), Is.EqualTo(63));
Assert.That(rules.Count(rule => rule.Form == ImpliedRuleForm.SpecificationTbd), Is.EqualTo(2));
}

// A representative unconditional rule: the whole body is the library target.
var portUsage = rules.Single(rule => rule.ConstraintName == "checkPortUsageSpecialization");

using (Assert.EnterMultipleScope())
{
Assert.That(portUsage.MetaclassName, Is.EqualTo("PortUsage"));
Assert.That(portUsage.Form, Is.EqualTo(ImpliedRuleForm.UnconditionalLibrarySpecialization));
Assert.That(portUsage.TargetLibraryName, Is.EqualTo("Ports::ports"));
Assert.That(portUsage.GuardExpression, Is.Null);
}

// A representative guarded rule: the target is still extracted mechanically, and the guard is
// captured verbatim for a hand-written predicate.
var subport = rules.Single(rule => rule.ConstraintName == "checkPortUsageSubportSpecialization");

using (Assert.EnterMultipleScope())
{
Assert.That(subport.Form, Is.EqualTo(ImpliedRuleForm.GuardedLibrarySpecialization));
Assert.That(subport.TargetLibraryName, Is.EqualTo("Ports::Port::subports"));
Assert.That(subport.GuardExpression, Is.Not.Empty);
Assert.That(subport.GuardExpression, Does.Not.Contain("specializesFromLibrary"));
}

// Categories 2-4 relate user-model elements, so they never carry a library target.
Assert.That(
rules.Where(rule => rule.Category != ImpliedConstraintCategory.Specialization).Select(rule => rule.TargetLibraryName),
Is.All.Null);

// Every rule carries the OCL it was classified from, so a hand-coded arm can be checked
// against the source of truth without re-reading the XMI.
Assert.That(rules.Select(rule => rule.Ocl), Is.All.Not.Null);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// -------------------------------------------------------------------------------------------------
// <copyright file="UmlCoreImpliedRelationshipGeneratorTestFixture.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.Generators.UmlHandleBarsGenerators
{
using System.IO;
using System.Threading.Tasks;

using NUnit.Framework;

using SysML2.NET.CodeGenerator.Generators.UmlHandleBarsGenerators;

[TestFixture]
public class UmlCoreImpliedRelationshipGeneratorTestFixture
{
private DirectoryInfo outputDirectory;
private UmlCoreImpliedRelationshipGenerator generator;

[OneTimeSetUp]
public void OneTimeSetup()
{
var directoryInfo = new DirectoryInfo(TestContext.CurrentContext.TestDirectory);

var path = Path.Combine("UML", "_SysML2.NET.Semantics.AutoGenImplied");

this.outputDirectory = directoryInfo.CreateSubdirectory(path);
this.generator = new UmlCoreImpliedRelationshipGenerator();
}

[Test]
public async Task VerifyImpliedRelationshipTableIsGenerated()
{
await Assert.ThatAsync(
() => this.generator.GenerateAsync(GeneratorSetupFixture.XmiReaderResult, this.outputDirectory),
Throws.Nothing);
}

[Test]
public async Task VerifyGeneratedTableCarriesTheExtractedRules()
{
var generatedCode = await this.generator.GenerateImpliedRelationshipTable(
GeneratorSetupFixture.XmiReaderResult,
this.outputDirectory);

using (Assert.EnterMultipleScope())
{
// An unconditional rule declared on the metaclass itself.
Assert.That(generatedCode, Does.Contain(@"new(""checkPortUsageSpecialization"", ""Ports::ports"", ""PortUsage"", false)"));

// A guarded rule keeps its target but is flagged so the caller consults the hand-written
// predicate before applying it.
Assert.That(generatedCode, Does.Contain(@"new(""checkPortUsageSubportSpecialization"", ""Ports::Port::subports"", ""PortUsage"", true)"));

// Constraints are flattened DOWN the metaclass hierarchy: PartUsage declares none of these,
// it inherits them, and the generated arm must still carry them.
Assert.That(generatedCode, Does.Contain("IPartUsageRules").Or.Contain("PartUsageRules"));
Assert.That(generatedCode, Does.Contain(@"""checkFeatureSpecialization"", ""Base::things"", ""Feature"""));

// The manifest must account for every constraint that could not be generated, including the
// two whose specification body the OMG left as TBD.
Assert.That(generatedCode, Does.Contain("checkInvocationExpressionDefaultValueBindingConnector"));
Assert.That(generatedCode, Does.Contain("specification body is TBD"));

// The hand-maintained half of the table — the part the OCL cannot supply — must survive
// into the generated file.
Assert.That(generatedCode, Does.Contain("SubclassificationMetaclasses"));
Assert.That(generatedCode, Does.Contain(@"""PartDefinition"""));
}
}
}
}
Loading
Loading