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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ The repository ships two NuGet packages that version independently: **ReqIFSharp

- A document that binds the ReqIF namespace to a prefix (`<reqif:REQ-IF xmlns:reqif="...">`) was read as zero documents, and type, source/target and relation-group references in it were not read. Elements are now matched on their local name and namespace ([#87](https://github.com/STARIONGROUP/ReqIFSharp/issues/87)).
- A document without whitespace between its elements (minified XML) lost data without an error: every other header field (such as `CREATION-TIME` and `REQ-IF-TOOL-ID`) and every other enumeration value of an attribute value was skipped, and the spec relations of a relation group were not read or caused an `XmlException` ([#94](https://github.com/STARIONGROUP/ReqIFSharp/issues/94)).
- `AttributeValueXHTML.TheOriginalValue` was not read from `THE-ORIGINAL-VALUE`, so it was always `null` after reading, and a document that was read and written again lost the original values of its simplified XHTML attribute values ([#95](https://github.com/STARIONGROUP/ReqIFSharp/issues/95)).

## [10.1.2] - 2026-10-18

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
// -------------------------------------------------------------------------------------------------
// <copyright file="AttributeValueXHTMLReadTestFixture.cs" company="Starion Group S.A.">
//
// Copyright 2017-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 ReqIFSharp.Tests
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;

using NUnit.Framework;

using ReqIFSharp;

/// <summary>
/// Verifies that <see cref="AttributeValueXHTML"/> reads THE-VALUE, THE-ORIGINAL-VALUE and DEFINITION
/// in any order, in indented and minified documents, see GH-95.
/// </summary>
[TestFixture]
public class AttributeValueXHTMLReadTestFixture
{
private const string FileName = "reqifsharpgenerated.reqif";

private static readonly XNamespace Xhtml = "http://www.w3.org/1999/xhtml";

private static readonly object[] ChildOrders =
{
new object[] { new[] { "DEFINITION", "THE-VALUE", "THE-ORIGINAL-VALUE" } },
new object[] { new[] { "DEFINITION", "THE-ORIGINAL-VALUE", "THE-VALUE" } },
new object[] { new[] { "THE-VALUE", "THE-ORIGINAL-VALUE", "DEFINITION" } },
new object[] { new[] { "THE-ORIGINAL-VALUE", "THE-VALUE", "DEFINITION" } }
};

/// <summary>
/// Orders with an element that is not part of the schema. It contains a THE-VALUE and a DEFINITION with an
/// unknown reference: reading either would change the value or throw.
/// </summary>
private static readonly object[] ChildOrdersWithUnknownElement =
{
new object[] { new[] { "UNKNOWN", "DEFINITION", "THE-VALUE", "THE-ORIGINAL-VALUE" } },
new object[] { new[] { "DEFINITION", "THE-VALUE", "UNKNOWN", "THE-ORIGINAL-VALUE" } }
};

[TestCase(false)]
[TestCase(true)]
public async Task Verify_that_the_original_value_is_read(bool async)
{
var original = TestDocuments.Read(FileName);
var expectedCount = XDocument.Parse(original).Descendants(TestDocuments.ReqIFNamespace + "THE-ORIGINAL-VALUE").Count();

Assert.That(expectedCount, Is.GreaterThan(0), "the test document must contain THE-ORIGINAL-VALUE elements");

var values = XhtmlValues(await Deserialize(original, SupportedFileExtensionKind.Reqif, async));
var withOriginal = values.Where(x => x.TheOriginalValue != null).ToList();

Assert.Multiple(() =>
{
Assert.That(withOriginal, Has.Count.EqualTo(expectedCount));
Assert.That(withOriginal.Select(x => x.TheOriginalValue), Has.All.StartWith("<xhtml:p").And.Contain("text after del"));
Assert.That(withOriginal.Select(x => x.IsSimplified), Has.All.True);
});
}

[TestCase(false)]
[TestCase(true)]
public async Task Verify_that_the_original_value_is_kept_when_a_document_is_read_written_and_read_again(bool async)
{
var first = await Deserialize(TestDocuments.Read(FileName), SupportedFileExtensionKind.Reqif, async);

string written;

using (var stream = new MemoryStream())
{
var serializer = new ReqIFSerializer();

if (async)
{
await serializer.SerializeAsync(first, stream, SupportedFileExtensionKind.Reqif, CancellationToken.None);
}
else
{
serializer.Serialize(first, stream, SupportedFileExtensionKind.Reqif);
}

stream.Position = 0;
written = new StreamReader(stream).ReadToEnd();
}

var second = await Deserialize(written, SupportedFileExtensionKind.Reqif, async);

var expected = XhtmlValues(first).Select(x => (x.IsSimplified, x.TheOriginalValue)).ToList();
var actual = XhtmlValues(second).Select(x => (x.IsSimplified, x.TheOriginalValue)).ToList();

Assert.That(expected.Count(x => x.TheOriginalValue != null), Is.GreaterThan(0));
Assert.That(actual, Is.EqualTo(expected));
}

[TestCaseSource(nameof(ChildOrders))]
[TestCaseSource(nameof(ChildOrdersWithUnknownElement))]
public async Task Verify_that_the_children_are_read_in_any_order(string[] order)
{
var (xml, expected) = CreateDocumentWithChildOrder(order);

foreach (var document in new[] { xml, TestDocuments.Minify(xml) })
{
foreach (var kind in TestDocuments.Kinds)
{
foreach (var async in new[] { false, true })
{
var values = XhtmlValues(await Deserialize(document, kind, async));

var actual = values
.Select(x => (Definition: x.Definition?.Identifier, Value: Marker(x.TheValue, "value"), Original: Marker(x.TheOriginalValue, "original")))
.ToList();

Assert.That(actual, Is.EquivalentTo(expected), $"order {string.Join(", ", order)}, {kind}, async: {async}, minified: {document != xml}");
}
}
}
}

[Test]
public void Verify_that_an_unknown_definition_reference_throws()
{
var ns = TestDocuments.ReqIFNamespace;
var document = XDocument.Parse(TestDocuments.Read(FileName));

document.Descendants(ns + "ATTRIBUTE-VALUE-XHTML").First()
.Element(ns + "DEFINITION")!
.Element(ns + "ATTRIBUTE-DEFINITION-XHTML-REF")!.Value = "unknown-xhtml-definition";

var xml = document.ToString();

foreach (var copy in new[] { xml, TestDocuments.Minify(xml) })
{
foreach (var kind in TestDocuments.Kinds)
{
foreach (var async in new[] { false, true })
{
Assert.That(async () => await Deserialize(copy, kind, async),
Throws.InstanceOf<InvalidOperationException>().With.Message.Contains("The attribute-definition XHTML unknown-xhtml-definition could not be found"),
$"{kind}, async: {async}, minified: {copy != xml}");
}
}
}
}

/// <summary>
/// Returns a copy of the test document in which every XHTML value has a THE-VALUE "value i" and a
/// THE-ORIGINAL-VALUE "original i", with its children in the given order, and the values expected after reading it.
/// </summary>
private static (string Xml, List<(string Definition, string Value, string Original)> Expected) CreateDocumentWithChildOrder(string[] order)
{
var ns = TestDocuments.ReqIFNamespace;
var document = XDocument.Parse(TestDocuments.Read(FileName));
var expected = new List<(string Definition, string Value, string Original)>();

var index = 0;

foreach (var attributeValue in document.Descendants(ns + "ATTRIBUTE-VALUE-XHTML").ToList())
{
var definition = attributeValue.Element(ns + "DEFINITION");
var definitionReference = definition!.Element(ns + "ATTRIBUTE-DEFINITION-XHTML-REF")!.Value;

var children = new Dictionary<string, XElement>
{
["DEFINITION"] = definition,
["THE-VALUE"] = new XElement(ns + "THE-VALUE", new XElement(Xhtml + "p", $"value {index}")),
["THE-ORIGINAL-VALUE"] = new XElement(ns + "THE-ORIGINAL-VALUE", new XElement(Xhtml + "p", $"original {index}")),
["UNKNOWN"] = new XElement(ns + "UNKNOWN",
new XElement(ns + "THE-VALUE", new XElement(Xhtml + "p", "value of the unknown element")),
new XElement(ns + "DEFINITION", new XElement(ns + "ATTRIBUTE-DEFINITION-XHTML-REF", "unknown-xhtml-definition")))
};

attributeValue.RemoveNodes();
attributeValue.Add(order.Select(name => children[name]));

expected.Add((definitionReference, $"value {index}", $"original {index}"));
index++;
}

Assert.That(expected, Is.Not.Empty, "the test document must contain XHTML values");

return (document.ToString(), expected);
}

private static string Marker(string xhtml, string word)
{
return xhtml == null ? null : Regex.Match(xhtml, $@"{word} \d+").Value;
}

private static List<AttributeValueXHTML> XhtmlValues(IEnumerable<ReqIF> reqifs)
{
var content = reqifs.Single().CoreContent;

return content.SpecObjects.SelectMany(x => x.Values)
.Concat(content.SpecRelations.SelectMany(x => x.Values))
.Concat(content.Specifications.SelectMany(x => x.Values))
.Concat(content.SpecRelationGroups.SelectMany(x => x.Values))
.OfType<AttributeValueXHTML>()
.ToList();
}

private static async Task<List<ReqIF>> Deserialize(string xml, SupportedFileExtensionKind kind, bool async)
{
var deserializer = new ReqIFDeserializer();
using var stream = TestDocuments.ToStream(xml, kind);

return async
? (await deserializer.DeserializeAsync(stream, kind, CancellationToken.None)).ToList()
: deserializer.Deserialize(stream, kind).ToList();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,27 @@ public void Verify_That_WriteXmlAsync_Without_Definition_Set_Throws_Serializatio
Throws.Exception.TypeOf<SerializationException>());
}

[Test]
public void Verify_that_ReadXmlAsync_throws_exception_when_cancelled()
{
const string xml = "<ATTRIBUTE-VALUE-XHTML xmlns=\"http://www.omg.org/spec/ReqIF/20110401/reqif.xsd\">" +
"<THE-VALUE><xhtml:p xmlns:xhtml=\"http://www.w3.org/1999/xhtml\">text</xhtml:p></THE-VALUE>" +
"</ATTRIBUTE-VALUE-XHTML>";

var cts = new CancellationTokenSource();
cts.Cancel();

using var xmlReader = XmlReader.Create(new StringReader(xml), new XmlReaderSettings { Async = true });
xmlReader.MoveToContent();

var attributeValueXhtml = new AttributeValueXHTML(this.loggerFactory);

Assert.That(async () => await attributeValueXhtml.ReadXmlAsync(xmlReader, cts.Token),
Throws.Exception.InstanceOf<OperationCanceledException>());

Assert.That(attributeValueXhtml.TheValue, Is.Null, "nothing must be read after the cancellation");
}

[Test]
public void Verify_That_WriteXmlAsync_Throws_Exception_when_cancelled()
{
Expand Down
42 changes: 7 additions & 35 deletions ReqIFSharp.Tests/MinifiedDocumentTestFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ public async Task Verify_that_every_header_field_of_a_minified_document_is_read_
public void Verify_that_a_minified_document_is_read_the_same_as_the_original(string fileName)
{
var original = TestDocuments.Read(fileName);
var minified = CreateMinifiedCopy(original);
var minified = TestDocuments.Minify(original);

foreach (var kind in TestDocuments.Kinds)
{
Expand All @@ -113,7 +113,7 @@ public void Verify_that_a_minified_document_is_read_the_same_as_the_original(str
public async Task Verify_that_a_minified_document_is_read_the_same_as_the_original_async(string fileName)
{
var original = TestDocuments.Read(fileName);
var minified = CreateMinifiedCopy(original);
var minified = TestDocuments.Minify(original);

foreach (var kind in TestDocuments.Kinds)
{
Expand All @@ -130,7 +130,7 @@ public async Task Verify_that_a_minified_document_is_read_the_same_as_the_origin
public void Verify_that_the_spec_relations_of_a_relation_group_in_a_minified_document_are_read()
{
var original = CreateDocumentWithSpecRelationsInRelationGroup();
var minified = CreateMinifiedCopy(original);
var minified = TestDocuments.Minify(original);

foreach (var kind in TestDocuments.Kinds)
{
Expand All @@ -149,7 +149,7 @@ public void Verify_that_the_spec_relations_of_a_relation_group_in_a_minified_doc
public async Task Verify_that_the_spec_relations_of_a_relation_group_in_a_minified_document_are_read_async()
{
var original = CreateDocumentWithSpecRelationsInRelationGroup();
var minified = CreateMinifiedCopy(original);
var minified = TestDocuments.Minify(original);

foreach (var kind in TestDocuments.Kinds)
{
Expand Down Expand Up @@ -188,7 +188,7 @@ public async Task Verify_that_the_elements_after_the_spec_relations_of_a_relatio
{
var original = CreateDocumentWithSpecRelationsInRelationGroup(insertBefore: "TYPE");

foreach (var xml in new[] { original, CreateMinifiedCopy(original) })
foreach (var xml in new[] { original, TestDocuments.Minify(original) })
{
foreach (var kind in TestDocuments.Kinds)
{
Expand All @@ -213,7 +213,7 @@ public async Task Verify_that_an_empty_spec_relations_element_of_a_relation_grou

Assert.That(original, Does.Contain("<SPEC-RELATIONS />"), "the document must contain an empty SPEC-RELATIONS element");

foreach (var xml in new[] { original, CreateMinifiedCopy(original) })
foreach (var xml in new[] { original, TestDocuments.Minify(original) })
{
foreach (var kind in TestDocuments.Kinds)
{
Expand All @@ -240,7 +240,7 @@ public async Task Verify_that_an_unsupported_child_of_spec_relations_is_skipped_

var original = CreateDocumentWithSpecRelationsInRelationGroup(insertBefore: "TYPE", unsupportedChild: unsupported);

foreach (var xml in new[] { original, CreateMinifiedCopy(original) })
foreach (var xml in new[] { original, TestDocuments.Minify(original) })
{
foreach (var kind in TestDocuments.Kinds)
{
Expand Down Expand Up @@ -341,33 +341,5 @@ private static void AssertHeader(ReqIFHeader header)
Assert.That(header.Title, Is.EqualTo("a title"));
});
}

/// <summary>
/// Returns a copy of the document without the whitespace between its ReqIF elements. Whitespace inside
/// XHTML values and tool extensions is kept: it is content, read as raw XML.
/// </summary>
private static string CreateMinifiedCopy(string original)
{
var document = XDocument.Parse(original, LoadOptions.PreserveWhitespace);

var whitespace = document.DescendantNodes()
.OfType<XText>()
.Where(t => string.IsNullOrWhiteSpace(t.Value) && !IsRawXmlContent(t))
.ToList();

Assert.That(whitespace, Is.Not.Empty, "the original must contain whitespace between elements");

whitespace.ForEach(t => t.Remove());

return document.ToString(SaveOptions.DisableFormatting);
}

private static bool IsRawXmlContent(XText text)
{
return text.Ancestors().Any(a =>
a.Name.Namespace != TestDocuments.ReqIFNamespace
|| a.Name.LocalName == "THE-VALUE"
|| a.Name.LocalName == "REQ-IF-TOOL-EXTENSION");
}
}
}
Loading
Loading