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
10 changes: 10 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,16 @@ Logging throughout uses optional injected `ILoggerFactory`, falling back to `Nul

Sample models live in `TestData/` (`ecore.ecore`, `recipe.ecore`, `wizardEcore.ecore`) and are linked into test projects' output under `Data/` via `<None>` items with `CopyToOutputDirectory`. Test framework is **NUnit 4** with **Moq**; coverage via coverlet. `InternalsVisibleTo` exposes internals (e.g. `ECoreParser`) to each project's `.Tests` assembly.

## Code coverage requirement

**All new code must reach at least 80% line/branch coverage** — this is enforced by the SonarCloud quality gate on new code. When implementing a feature or fix, add tests that exercise every new branch (happy path *and* guard/error paths) so coverage on the changed lines is ≥ 80%. Verify locally before opening/updating a PR:

```powershell
dotnet test ECoreNetto.Tests/ECoreNetto.Tests.csproj --collect:"XPlat Code Coverage" --settings coverlet.runsettings
```

Then inspect the generated Cobertura report (or run ReportGenerator) and confirm the new members are covered. Do not consider an issue "done" until its new code clears the 80% bar.

## Code style (from .github/CONTRIBUTING.md)

These differ from default .NET conventions — match them:
Expand Down
133 changes: 133 additions & 0 deletions ECoreNetto.Tests/ModelElement/EObjectMetaClassTestFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// -------------------------------------------------------------------------------------------------
// <copyright file="EObjectMetaClassTestFixture.cs" company="Starion Group S.A.">
//
// Copyright 2017-2025 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 ECoreNetto.Tests.ModelElement
{
using System;
using System.IO;
using System.Linq;

using ECoreNetto.Resource;

using NUnit.Framework;

/// <summary>
/// Suite of tests that verify <see cref="EObject.EClass"/> returns the Ecore meta class that
/// describes the runtime type of the model element (see issue #10).
/// </summary>
[TestFixture]
public class EObjectMetaClassTestFixture
{
private Resource resource = null!;

private EPackage rootPackage = null!;

[SetUp]
public void SetUp()
{
var path = Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "recipe.ecore");
var uri = new Uri(Path.GetFullPath(path));

var resourceSet = new ResourceSet();
this.resource = resourceSet.CreateResource(uri);
this.rootPackage = this.resource.Load(null);
}

[Test]
public void Verify_that_EClass_returns_the_expected_meta_class_for_each_element_kind()
{
var eClass = this.rootPackage.EClassifiers.OfType<EClass>().First();
var attribute = this.rootPackage.EClassifiers.OfType<EClass>()
.SelectMany(c => c.EStructuralFeatures).OfType<EAttribute>().First();
var reference = this.rootPackage.EClassifiers.OfType<EClass>()
.SelectMany(c => c.EStructuralFeatures).OfType<EReference>().First();
var eEnum = this.rootPackage.EClassifiers.OfType<EEnum>().First();
var literal = this.resource.GetEObject("recipe.ecore#//Unit/PIECE");

Assert.That(literal, Is.Not.Null);

Assert.Multiple(() =>
{
Assert.That(this.rootPackage.EClass().Name, Is.EqualTo("EPackage"));
Assert.That(eClass.EClass().Name, Is.EqualTo("EClass"));
Assert.That(attribute.EClass().Name, Is.EqualTo("EAttribute"));
Assert.That(reference.EClass().Name, Is.EqualTo("EReference"));
Assert.That(eEnum.EClass().Name, Is.EqualTo("EEnum"));
Assert.That(literal!.EClass().Name, Is.EqualTo("EEnumLiteral"));
});
}

[Test]
public void Verify_that_the_returned_meta_class_is_the_resource_registered_instance()
{
var eClass = this.rootPackage.EClassifiers.OfType<EClass>().First();

var metaClass = eClass.EClass();

Assert.Multiple(() =>
{
// the meta class is itself an EClass, and is the same instance the resource exposes for "//EClass"
Assert.That(metaClass, Is.InstanceOf<EClass>());
Assert.That(metaClass, Is.SameAs(this.resource.GetEObject("//EClass")));
});
}

[Test]
public void Verify_that_the_meta_class_is_consistent_across_instances_of_the_same_type()
{
var attributes = this.rootPackage.EClassifiers.OfType<EClass>()
.SelectMany(c => c.EStructuralFeatures).OfType<EAttribute>().Take(2).ToList();

Assert.That(attributes, Has.Count.EqualTo(2));
Assert.That(attributes[0].EClass(), Is.SameAs(attributes[1].EClass()));
}

[Test]
public void Verify_that_EClass_throws_when_no_meta_class_is_registered_for_the_runtime_type()
{
// a runtime type whose name is not present in the Ecore meta model registry
var unregistered = new UnregisteredEObject(this.resource);

Assert.That(() => unregistered.EClass(), Throws.TypeOf<InvalidOperationException>());
}

[Test]
public void Verify_that_GetMetaClass_returns_null_for_an_unknown_type_name()
{
Assert.That(this.resource.GetMetaClass("ThisTypeDoesNotExist"), Is.Null);
}

/// <summary>
/// A minimal <see cref="EObject"/> subtype whose runtime type name has no corresponding Ecore
/// meta class, used to exercise the guard path of <see cref="EObject.EClass"/>.
/// </summary>
private sealed class UnregisteredEObject : EObject
{
public UnregisteredEObject(Resource resource)
: base(resource)
{
}

internal override void SetProperties()
{
}
}
}
}
10 changes: 9 additions & 1 deletion ECoreNetto/ModelElement/EObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,15 @@
/// </returns>
public EClass EClass()
{
throw new NotImplementedException();
var metaClass = this.EResource.GetMetaClass(this.GetType().Name);

if (metaClass == null)
{
throw new InvalidOperationException(
$"No Ecore meta class is registered for '{this.GetType().Name}'.");
}

return metaClass;
}

/// <summary>
Expand All @@ -110,7 +118,7 @@
/// <returns>
/// The <see cref="EStructuralFeature"/> that actually contains the object.
/// </returns>
public EStructuralFeature EContainingFeature()

Check warning on line 121 in ECoreNetto/ModelElement/EObject.cs

View workflow job for this annotation

GitHub Actions / Build

Make 'EContainingFeature' a static method.

Check warning on line 121 in ECoreNetto/ModelElement/EObject.cs

View workflow job for this annotation

GitHub Actions / Build

Make 'EContainingFeature' a static method.
{
throw new NotImplementedException();
}
Expand All @@ -123,7 +131,7 @@
/// <returns>
/// The feature that properly contains the object.
/// </returns>
public EReference EContainmentFeature()

Check warning on line 134 in ECoreNetto/ModelElement/EObject.cs

View workflow job for this annotation

GitHub Actions / Build

Make 'EContainmentFeature' a static method.

Check warning on line 134 in ECoreNetto/ModelElement/EObject.cs

View workflow job for this annotation

GitHub Actions / Build

Make 'EContainmentFeature' a static method.
{
throw new NotImplementedException();
}
Expand All @@ -138,7 +146,7 @@
/// <returns>
/// A list view of the content objects.
/// </returns>
public IEnumerable<EObject> EContents()

Check warning on line 149 in ECoreNetto/ModelElement/EObject.cs

View workflow job for this annotation

GitHub Actions / Build

Make 'EContents' a static method.

Check warning on line 149 in ECoreNetto/ModelElement/EObject.cs

View workflow job for this annotation

GitHub Actions / Build

Make 'EContents' a static method.
{
throw new NotImplementedException();
}
Expand All @@ -149,7 +157,7 @@
/// <returns>
/// A tree iterator that iterates over all contents.
/// </returns>
public IEnumerable<EObject> EAllContents()

Check warning on line 160 in ECoreNetto/ModelElement/EObject.cs

View workflow job for this annotation

GitHub Actions / Build

Make 'EAllContents' a static method.

Check warning on line 160 in ECoreNetto/ModelElement/EObject.cs

View workflow job for this annotation

GitHub Actions / Build

Make 'EAllContents' a static method.
{
throw new NotImplementedException();
}
Expand All @@ -165,7 +173,7 @@
/// <returns>
/// true if this object is a proxy or false, otherwise.
/// </returns>
public bool EIsProxy()

Check warning on line 176 in ECoreNetto/ModelElement/EObject.cs

View workflow job for this annotation

GitHub Actions / Build

Make 'EIsProxy' a static method.
{
throw new NotImplementedException();
}
Expand All @@ -181,7 +189,7 @@
/// <returns>
/// A list view of the cross referenced objects.
/// </returns>
public IEnumerable<EObject> ECrossReferences()

Check warning on line 192 in ECoreNetto/ModelElement/EObject.cs

View workflow job for this annotation

GitHub Actions / Build

Make 'ECrossReferences' a static method.
{
throw new NotImplementedException();
}
Expand Down Expand Up @@ -220,7 +228,7 @@
/// <exception cref="ArgumentException">
/// If the <see cref="EStructuralFeature"/> is not one the meta class's features and is also not affiliated with one of the meta class's features.
/// </exception>
public object EGet(EStructuralFeature feature, bool resolve)

Check warning on line 231 in ECoreNetto/ModelElement/EObject.cs

View workflow job for this annotation

GitHub Actions / Build

Make 'EGet' a static method.
{
throw new NotImplementedException();
}
Expand Down Expand Up @@ -248,7 +256,7 @@
/// <exception cref="InvalidCastException">
/// If there is a type conflict.
/// </exception>
public void ESet(EStructuralFeature feature, object newValue)

Check warning on line 259 in ECoreNetto/ModelElement/EObject.cs

View workflow job for this annotation

GitHub Actions / Build

Make 'ESet' a static method.
{
throw new NotImplementedException();
}
Expand All @@ -272,7 +280,7 @@
/// <exception cref="ArgumentException">
/// If the feature is not one the meta class's <see cref="EStructuralFeature"/>s.
/// </exception>
public bool EIsSet(EStructuralFeature feature)

Check warning on line 283 in ECoreNetto/ModelElement/EObject.cs

View workflow job for this annotation

GitHub Actions / Build

Make 'EIsSet' a static method.
{
throw new NotImplementedException();
}
Expand All @@ -291,7 +299,7 @@
/// <exception cref="ArgumentException">
/// If the feature is not one the meta class's <see cref="EStructuralFeature"/>s, or it isn't changeable.
/// </exception>
public void EUnSet(EStructuralFeature feature)

Check warning on line 302 in ECoreNetto/ModelElement/EObject.cs

View workflow job for this annotation

GitHub Actions / Build

Make 'EUnSet' a static method.
{
throw new NotImplementedException();
}
Expand Down
20 changes: 20 additions & 0 deletions ECoreNetto/Resource/Resource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,26 @@ public IEnumerable<EObject> AllContents()
return this.Cache.Values;
}

/// <summary>
/// Gets the Ecore meta class registered for the provided simple type name.
/// </summary>
/// <param name="name">
/// The simple name of the meta class (e.g. <c>EClass</c>, <c>EAttribute</c>), which matches the
/// runtime type name of the model elements.
/// </param>
/// <returns>
/// The <see cref="EClass"/> meta class, or null when no meta class is registered for <paramref name="name"/>.
/// </returns>
internal EClass? GetMetaClass(string name)
{
if (this.eCoreTypes.TryGetValue($"//{name}", out var metaClass) && metaClass is EClass eClass)
{
return eClass;
}

return null;
}

/// <summary>
/// Returns the URI fragment that, when passed to <see cref="GetEObject(string)"/>, will return the given object.
/// </summary>
Expand Down
Loading