diff --git a/ECoreNetto.Tests/Resource/DiagnosticsTestFixture.cs b/ECoreNetto.Tests/Resource/DiagnosticsTestFixture.cs new file mode 100644 index 0000000..1dbd433 --- /dev/null +++ b/ECoreNetto.Tests/Resource/DiagnosticsTestFixture.cs @@ -0,0 +1,145 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// 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. +// +// +// ------------------------------------------------------------------------------------------------ + +namespace ECoreNetto.Tests.Resource +{ + using System; + using System.IO; + using System.Linq; + + using ECoreNetto.Resource; + + using NUnit.Framework; + + /// + /// Suite of tests that verify the records a + /// in for malformed or unrecognized input before aborting the load (see issue #35). + /// + [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", ""); + var resource = this.CreateResourceForContent("malformed-bool.ecore", model); + + Assert.Throws(() => 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", + "\r\n" + + " \r\n" + + " "); + var resource = this.CreateResourceForContent("malformed-int.ecore", model); + + Assert.Throws(() => 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", ""); + var resource = this.CreateResourceForContent("unknown-classifier.ecore", model); + + Assert.Throws(() => 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", + "\r\n" + + " \r\n" + + " "); + var resource = this.CreateResourceForContent("unknown-feature.ecore", model); + + Assert.Throws(() => resource.Load(null)); + + var error = resource.Errors.SingleOrDefault(); + Assert.That(error, Is.Not.Null); + Assert.That(error!.Message, Does.Contain("Bogus")); + } + + /// + /// Wraps the supplied classifier markup in a minimal Ecore package whose name matches + /// . + /// + private static string Package(string packageName, string body) + { + return + "\r\n" + + "\r\n" + + $" {body}\r\n" + + ""; + } + + /// + /// Writes the provided to a file in the test directory and + /// creates a for it. + /// + 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); + } + } +} diff --git a/ECoreNetto/ModelElement/EObject.cs b/ECoreNetto/ModelElement/EObject.cs index b5a26ce..51c4309 100644 --- a/ECoreNetto/ModelElement/EObject.cs +++ b/ECoreNetto/ModelElement/EObject.cs @@ -356,7 +356,65 @@ protected void SaveAttributes(XmlNode reader) this.Attributes.Add(readerAttribute.Name, attributeValue); } } - + + /// + /// Parses the value of a boolean attribute, recording an error and + /// throwing when the value is malformed. + /// + /// + /// The name of the attribute being parsed; used in the diagnostic message. + /// + /// + /// The raw attribute value to parse. + /// + /// + /// The parsed value. + /// + /// + /// Thrown when is not a valid boolean. The issue is recorded in + /// before the load is aborted. + /// + 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); + } + + /// + /// Parses the value of an integer attribute, recording an error and + /// throwing when the value is malformed. + /// + /// + /// The name of the attribute being parsed; used in the diagnostic message. + /// + /// + /// The raw attribute value to parse. + /// + /// + /// The parsed value. + /// + /// + /// Thrown when is not a valid integer. The issue is recorded in + /// before the load is aborted. + /// + 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); + } + /// /// Instantiate new from the current node of the /// diff --git a/ECoreNetto/ModelElement/NamedElement/Classifier/EClass.cs b/ECoreNetto/ModelElement/NamedElement/Classifier/EClass.cs index 28c5f1a..2be59a6 100644 --- a/ECoreNetto/ModelElement/NamedElement/Classifier/EClass.cs +++ b/ECoreNetto/ModelElement/NamedElement/Classifier/EClass.cs @@ -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)) @@ -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); } } diff --git a/ECoreNetto/ModelElement/NamedElement/Classifier/EDataType.cs b/ECoreNetto/ModelElement/NamedElement/Classifier/EDataType.cs index 7930e88..0340412 100644 --- a/ECoreNetto/ModelElement/NamedElement/Classifier/EDataType.cs +++ b/ECoreNetto/ModelElement/NamedElement/Classifier/EDataType.cs @@ -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); } } } diff --git a/ECoreNetto/ModelElement/NamedElement/EEnumLiteral.cs b/ECoreNetto/ModelElement/NamedElement/EEnumLiteral.cs index d8de023..8ac0fca 100644 --- a/ECoreNetto/ModelElement/NamedElement/EEnumLiteral.cs +++ b/ECoreNetto/ModelElement/NamedElement/EEnumLiteral.cs @@ -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); } } diff --git a/ECoreNetto/ModelElement/NamedElement/EPackage.cs b/ECoreNetto/ModelElement/NamedElement/EPackage.cs index 815ddeb..37a58a5 100644 --- a/ECoreNetto/ModelElement/NamedElement/EPackage.cs +++ b/ECoreNetto/ModelElement/NamedElement/EPackage.cs @@ -183,7 +183,9 @@ protected override void DeserializeChildNode(XmlNode reader) 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); } } diff --git a/ECoreNetto/ModelElement/NamedElement/ETypedElement.cs b/ECoreNetto/ModelElement/NamedElement/ETypedElement.cs index 458a8e0..cb0dc54 100644 --- a/ECoreNetto/ModelElement/NamedElement/ETypedElement.cs +++ b/ECoreNetto/ModelElement/NamedElement/ETypedElement.cs @@ -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)) diff --git a/ECoreNetto/ModelElement/NamedElement/TypedElement/EStructuralFeature.cs b/ECoreNetto/ModelElement/NamedElement/TypedElement/EStructuralFeature.cs index 80d2f3b..fa51cbe 100644 --- a/ECoreNetto/ModelElement/NamedElement/TypedElement/EStructuralFeature.cs +++ b/ECoreNetto/ModelElement/NamedElement/TypedElement/EStructuralFeature.cs @@ -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)) @@ -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); } } diff --git a/ECoreNetto/ModelElement/NamedElement/TypedElement/StructuralFeature/EAttribute.cs b/ECoreNetto/ModelElement/NamedElement/TypedElement/StructuralFeature/EAttribute.cs index 9eee076..294f70e 100644 --- a/ECoreNetto/ModelElement/NamedElement/TypedElement/StructuralFeature/EAttribute.cs +++ b/ECoreNetto/ModelElement/NamedElement/TypedElement/StructuralFeature/EAttribute.cs @@ -70,7 +70,7 @@ internal override void SetProperties() if (this.Attributes.TryGetValue("iD", out var output)) { - this.ID = bool.Parse(output); + this.ID = this.ParseBoolean("iD", output); } } } diff --git a/ECoreNetto/ModelElement/NamedElement/TypedElement/StructuralFeature/EReference.cs b/ECoreNetto/ModelElement/NamedElement/TypedElement/StructuralFeature/EReference.cs index b36f667..660afb6 100644 --- a/ECoreNetto/ModelElement/NamedElement/TypedElement/StructuralFeature/EReference.cs +++ b/ECoreNetto/ModelElement/NamedElement/TypedElement/StructuralFeature/EReference.cs @@ -103,17 +103,17 @@ internal override void SetProperties() 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)) diff --git a/ECoreNetto/Resource/Resource.cs b/ECoreNetto/Resource/Resource.cs index f429811..7414ce8 100644 --- a/ECoreNetto/Resource/Resource.cs +++ b/ECoreNetto/Resource/Resource.cs @@ -403,11 +403,26 @@ public void UnLoad() this.isLoaded = false; } + /// + /// Records an error that was encountered while loading the resource. + /// + /// + /// The translated message describing the issue. + /// + /// + /// The diagnostic is exposed through . The source location is the URI of this + /// resource; line and column are not available once parsing has reached the property-resolution phase. + /// + internal void AddError(string message) + { + this.errors.Add(new Diagnostic(0, 0, this.URI?.AbsoluteUri ?? string.Empty, message)); + } + /// /// Gets an of the errors in the resource; /// /// - /// These will typically be produced as the resource is loaded. + /// These will typically be produced as the resource is loaded. /// public IEnumerable Errors => this.errors;