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
145 changes: 145 additions & 0 deletions ECoreNetto.Tests/Resource/DiagnosticsTestFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// -------------------------------------------------------------------------------------------------
// <copyright file="DiagnosticsTestFixture.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.Resource
{
using System;
using System.IO;
using System.Linq;

using ECoreNetto.Resource;

using NUnit.Framework;

/// <summary>
/// Suite of tests that verify the <see cref="ECoreNetto.ECoreParser"/> records a <see cref="Diagnostic"/>
/// in <see cref="Resource.Errors"/> for malformed or unrecognized input before aborting the load (see issue #35).
/// </summary>
[TestFixture]
public class DiagnosticsTestFixture
{
private ResourceSet resourceSet = null!;

[SetUp]
public void SetUp()
{
this.resourceSet = new ResourceSet();
}

[Test]
public void Verify_that_a_malformed_boolean_attribute_is_recorded_as_an_error_and_aborts_the_load()
{
var model = Package("malformed-bool", "<eClassifiers xsi:type=\"ecore:EClass\" name=\"A\" abstract=\"notabool\"/>");
var resource = this.CreateResourceForContent("malformed-bool.ecore", model);

Assert.Throws<FormatException>(() => resource.Load(null));

var error = resource.Errors.SingleOrDefault();
Assert.That(error, Is.Not.Null);
Assert.Multiple(() =>
{
Assert.That(error!.Message, Does.Contain("abstract"));
Assert.That(error.Message, Does.Contain("notabool"));
Assert.That(error.Location, Is.EqualTo(resource.URI.AbsoluteUri));
});
}

[Test]
public void Verify_that_a_malformed_integer_attribute_is_recorded_as_an_error_and_aborts_the_load()
{
var model = Package(
"malformed-int",
"<eClassifiers xsi:type=\"ecore:EEnum\" name=\"E\">\r\n" +
" <eLiterals name=\"X\" value=\"notanint\"/>\r\n" +
" </eClassifiers>");
var resource = this.CreateResourceForContent("malformed-int.ecore", model);

Assert.Throws<FormatException>(() => resource.Load(null));

var error = resource.Errors.SingleOrDefault();
Assert.That(error, Is.Not.Null);
Assert.Multiple(() =>
{
Assert.That(error!.Message, Does.Contain("value"));
Assert.That(error.Message, Does.Contain("notanint"));
});
}

[Test]
public void Verify_that_an_unrecognized_classifier_type_is_recorded_as_an_error_and_aborts_the_load()
{
var model = Package("unknown-classifier", "<eClassifiers xsi:type=\"ecore:Bogus\" name=\"A\"/>");
var resource = this.CreateResourceForContent("unknown-classifier.ecore", model);

Assert.Throws<InvalidOperationException>(() => resource.Load(null));

var error = resource.Errors.SingleOrDefault();
Assert.That(error, Is.Not.Null);
Assert.That(error!.Message, Does.Contain("Bogus"));
}

[Test]
public void Verify_that_an_unrecognized_structural_feature_type_is_recorded_as_an_error_and_aborts_the_load()
{
var model = Package(
"unknown-feature",
"<eClassifiers xsi:type=\"ecore:EClass\" name=\"A\">\r\n" +
" <eStructuralFeatures xsi:type=\"ecore:Bogus\" name=\"f\"/>\r\n" +
" </eClassifiers>");
var resource = this.CreateResourceForContent("unknown-feature.ecore", model);

Assert.Throws<InvalidOperationException>(() => resource.Load(null));

var error = resource.Errors.SingleOrDefault();
Assert.That(error, Is.Not.Null);
Assert.That(error!.Message, Does.Contain("Bogus"));
}

/// <summary>
/// Wraps the supplied classifier markup in a minimal Ecore package whose name matches
/// <paramref name="packageName"/>.
/// </summary>
private static string Package(string packageName, string body)
{
return
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" +
"<ecore:EPackage xmi:version=\"2.0\" xmlns:xmi=\"http://www.omg.org/XMI\" " +
"xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" " +
"xmlns:ecore=\"http://www.eclipse.org/emf/2002/Ecore\" " +
$"name=\"{packageName}\" nsURI=\"{packageName}\" nsPrefix=\"{packageName}\">\r\n" +
$" {body}\r\n" +
"</ecore:EPackage>";
}

/// <summary>
/// Writes the provided <paramref name="content"/> to a file in the test directory and
/// creates a <see cref="Resource"/> for it.
/// </summary>
private Resource CreateResourceForContent(string fileName, string content)
{
var path = Path.Combine(TestContext.CurrentContext.TestDirectory, fileName);
File.WriteAllText(path, content);

var uri = new Uri(Path.GetFullPath(path));

return this.resourceSet.CreateResource(uri);
}
}
}
60 changes: 59 additions & 1 deletion ECoreNetto/ModelElement/EObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
/// <returns>
/// The meta class.
/// </returns>
public EClass EClass()

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

View workflow job for this annotation

GitHub Actions / Build

Make 'EClass' a static method.
{
throw new NotImplementedException();
}
Expand All @@ -110,7 +110,7 @@
/// <returns>
/// The <see cref="EStructuralFeature"/> that actually contains the object.
/// </returns>
public EStructuralFeature EContainingFeature()

Check warning on line 113 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 +123,7 @@
/// <returns>
/// The feature that properly contains the object.
/// </returns>
public EReference EContainmentFeature()

Check warning on line 126 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 +138,7 @@
/// <returns>
/// A list view of the content objects.
/// </returns>
public IEnumerable<EObject> EContents()

Check warning on line 141 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 +149,7 @@
/// <returns>
/// A tree iterator that iterates over all contents.
/// </returns>
public IEnumerable<EObject> EAllContents()

Check warning on line 152 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 +165,7 @@
/// <returns>
/// true if this object is a proxy or false, otherwise.
/// </returns>
public bool EIsProxy()

Check warning on line 168 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 +181,7 @@
/// <returns>
/// A list view of the cross referenced objects.
/// </returns>
public IEnumerable<EObject> ECrossReferences()

Check warning on line 184 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 @@ -356,7 +356,65 @@
this.Attributes.Add(readerAttribute.Name, attributeValue);
}
}


/// <summary>
/// Parses the value of a boolean attribute, recording an error <see cref="Resource.Diagnostic"/> and
/// throwing when the value is malformed.
/// </summary>
/// <param name="attributeName">
/// The name of the attribute being parsed; used in the diagnostic message.
/// </param>
/// <param name="rawValue">
/// The raw attribute value to parse.
/// </param>
/// <returns>
/// The parsed <see cref="bool"/> value.
/// </returns>
/// <exception cref="FormatException">
/// Thrown when <paramref name="rawValue"/> is not a valid boolean. The issue is recorded in
/// <see cref="Resource.Resource.Errors"/> before the load is aborted.
/// </exception>
protected bool ParseBoolean(string attributeName, string rawValue)
{
if (bool.TryParse(rawValue, out var value))
{
return value;
}

var message = $"The '{attributeName}' attribute of '{this.Identifier}' has a malformed boolean value '{rawValue}'.";
this.EResource.AddError(message);
throw new FormatException(message);
}

/// <summary>
/// Parses the value of an integer attribute, recording an error <see cref="Resource.Diagnostic"/> and
/// throwing when the value is malformed.
/// </summary>
/// <param name="attributeName">
/// The name of the attribute being parsed; used in the diagnostic message.
/// </param>
/// <param name="rawValue">
/// The raw attribute value to parse.
/// </param>
/// <returns>
/// The parsed <see cref="int"/> value.
/// </returns>
/// <exception cref="FormatException">
/// Thrown when <paramref name="rawValue"/> is not a valid integer. The issue is recorded in
/// <see cref="Resource.Resource.Errors"/> before the load is aborted.
/// </exception>
protected int ParseInt32(string attributeName, string rawValue)
{
if (int.TryParse(rawValue, out var value))
{
return value;
}

var message = $"The '{attributeName}' attribute of '{this.Identifier}' has a malformed integer value '{rawValue}'.";
this.EResource.AddError(message);
throw new FormatException(message);
}

/// <summary>
/// Instantiate new <see cref="EObject"/> from the current node of the <see cref="XmlReader"/>
/// </summary>
Expand Down
8 changes: 5 additions & 3 deletions ECoreNetto/ModelElement/NamedElement/Classifier/EClass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -170,12 +170,12 @@ internal override void SetProperties()

if (this.Attributes.TryGetValue(EcoreAbstractKeyword, out var output))
{
this.Abstract = bool.Parse(output);
this.Abstract = this.ParseBoolean(EcoreAbstractKeyword, output);
}

if (this.Attributes.TryGetValue(EcoreInterfaceKeyword, out output))
{
this.Interface = bool.Parse(output);
this.Interface = this.ParseBoolean(EcoreInterfaceKeyword, output);
}

if (this.Attributes.TryGetValue(EcoreSuperTypeKeyword, out output))
Expand Down Expand Up @@ -219,7 +219,9 @@ protected override void DeserializeChildNode(XmlNode reader)
ecoreAttribute.ReadXml(reader);
break;
default:
throw new InvalidOperationException($"Type of structural feature not recognized: {ecoreType}");
var featureError = $"Type of structural feature not recognized: {ecoreType}";
this.EResource.AddError(featureError);
throw new InvalidOperationException(featureError);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ internal override void SetProperties()

if (this.Attributes.TryGetValue("serializable", out var output))
{
this.Serializable = bool.Parse(output);
this.Serializable = this.ParseBoolean("serializable", output);
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion ECoreNetto/ModelElement/NamedElement/EEnumLiteral.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ internal override void SetProperties()

if (this.Attributes.TryGetValue("value", out var output))
{
this.Value = int.Parse(output);
this.Value = this.ParseInt32("value", output);
}
}

Expand Down
4 changes: 3 additions & 1 deletion ECoreNetto/ModelElement/NamedElement/EPackage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
/// <summary>
/// Gets the <see cref="EFactoryInstance"/>
/// </summary>
public EFactory? EFactoryInstance { get; private set; }

Check warning on line 98 in ECoreNetto/ModelElement/NamedElement/EPackage.cs

View workflow job for this annotation

GitHub Actions / Build

Remove the unused private setter 'set_EFactoryInstance'.

Check warning on line 98 in ECoreNetto/ModelElement/NamedElement/EPackage.cs

View workflow job for this annotation

GitHub Actions / Build

Remove the unused private setter 'set_EFactoryInstance'.

/// <summary>
/// Gets the collection of sub <see cref="EPackage"/>
Expand Down Expand Up @@ -183,7 +183,9 @@
ecoreEnum.ReadXml(reader);
break;
default:
throw new InvalidOperationException($"Type of classifier not recognized: {ecoreType}");
var classifierError = $"Type of classifier not recognized: {ecoreType}";
this.EResource.AddError(classifierError);
throw new InvalidOperationException(classifierError);
}
}

Expand Down
12 changes: 6 additions & 6 deletions ECoreNetto/ModelElement/NamedElement/ETypedElement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,32 +103,32 @@ internal override void SetProperties()

if (this.Attributes.TryGetValue("ordered", out var output))
{
this.Ordered = bool.Parse(output);
this.Ordered = this.ParseBoolean("ordered", output);
}

if (this.Attributes.TryGetValue("unique", out output))
{
this.Unique = bool.Parse(output);
this.Unique = this.ParseBoolean("unique", output);
}

if (this.Attributes.TryGetValue("many", out output))
{
this.Many = bool.Parse(output);
this.Many = this.ParseBoolean("many", output);
}

if (this.Attributes.TryGetValue("required", out output))
{
this.Required = bool.Parse(output);
this.Required = this.ParseBoolean("required", output);
}

if (this.Attributes.TryGetValue("lowerBound", out output))
{
this.LowerBound = int.Parse(output);
this.LowerBound = this.ParseInt32("lowerBound", output);
}

if (this.Attributes.TryGetValue("upperBound", out output))
{
this.UpperBound = int.Parse(output);
this.UpperBound = this.ParseInt32("upperBound", output);
}

if (this.Attributes.TryGetValue("eType", out output))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,17 +135,17 @@ internal override void SetProperties()

if (this.Attributes.TryGetValue("changeable", out var output))
{
this.Changeable = bool.Parse(output);
this.Changeable = this.ParseBoolean("changeable", output);
}

if (this.Attributes.TryGetValue("volatile", out output))
{
this.Volatile = bool.Parse(output);
this.Volatile = this.ParseBoolean("volatile", output);
}

if (this.Attributes.TryGetValue("transient", out output))
{
this.Transient = bool.Parse(output);
this.Transient = this.ParseBoolean("transient", output);
}

if (this.Attributes.TryGetValue("defaultValueLiteral", out output))
Expand All @@ -155,12 +155,12 @@ internal override void SetProperties()

if (this.Attributes.TryGetValue("unsettable", out output))
{
this.Unsettable = bool.Parse(output);
this.Unsettable = this.ParseBoolean("unsettable", output);
}

if (this.Attributes.TryGetValue("derived", out output))
{
this.Derived = bool.Parse(output);
this.Derived = this.ParseBoolean("derived", output);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
/// <see cref="EAttribute"/> models attributes, the components of an object's data. They are identified by name,
/// and they have a type.
/// </remarks>
public class EAttribute : EStructuralFeature

Check warning on line 33 in ECoreNetto/ModelElement/NamedElement/TypedElement/StructuralFeature/EAttribute.cs

View workflow job for this annotation

GitHub Actions / Build

This class has 6 parents which is greater than 5 authorized.

Check warning on line 33 in ECoreNetto/ModelElement/NamedElement/TypedElement/StructuralFeature/EAttribute.cs

View workflow job for this annotation

GitHub Actions / Build

This class has 6 parents which is greater than 5 authorized.
{
/// <summary>
/// The <see cref="ILogger"/> used to log
Expand Down Expand Up @@ -70,7 +70,7 @@

if (this.Attributes.TryGetValue("iD", out var output))
{
this.ID = bool.Parse(output);
this.ID = this.ParseBoolean("iD", output);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
/// references are identified by name and have a type. However, this type must be the <see cref="EClass"/> at the other end of the
/// association.
/// </remarks>
public class EReference : EStructuralFeature

Check warning on line 34 in ECoreNetto/ModelElement/NamedElement/TypedElement/StructuralFeature/EReference.cs

View workflow job for this annotation

GitHub Actions / Build

This class has 6 parents which is greater than 5 authorized.

Check warning on line 34 in ECoreNetto/ModelElement/NamedElement/TypedElement/StructuralFeature/EReference.cs

View workflow job for this annotation

GitHub Actions / Build

This class has 6 parents which is greater than 5 authorized.
{
/// <summary>
/// The <see cref="ILogger"/> used to log
Expand Down Expand Up @@ -103,17 +103,17 @@

if (this.Attributes.TryGetValue("container", out var output))
{
this.IsContainer = bool.Parse(output);
this.IsContainer = this.ParseBoolean("container", output);
}

if (this.Attributes.TryGetValue("containment", out output))
{
this.IsContainment = bool.Parse(output);
this.IsContainment = this.ParseBoolean("containment", output);
}

if (this.Attributes.TryGetValue("resolveProxies", out output))
{
this.IsResolveProxies = bool.Parse(output);
this.IsResolveProxies = this.ParseBoolean("resolveProxies", output);
}

if (this.Attributes.TryGetValue("eOpposite", out output))
Expand Down
Loading
Loading