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
24 changes: 23 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,29 @@ dotnet-coverage collect "dotnet test SysML2.NET.sln --no-build" -f xml -o covera

Test framework: **NUnit**. Test classes use `[TestFixture]` and `[Test]` attributes.

**When writing or modifying unit tests** in any `*.Tests/` project: read `TESTING.md` at the repo root for the NUnit conventions (one `[Test]` per method-under-test, `Assert.That` everywhere, `Assert.EnterMultipleScope` only for consecutive asserts, mandatory positive + negative coverage, assertion idiom preferences, `Verify{MethodUnderTest}` naming).
## Reading `TESTING.md` is MANDATORY

**Before you write or modify a single line in any `*.Tests/` project, you MUST `Read` `TESTING.md` at the repo root — in full, in the current session.** It is the authoritative, binding specification for every NUnit fixture in this solution.

This applies to *every* test change, including ones that feel too small to warrant it: adding one `[Test]`, adding an assertion to an existing one, or creating a new fixture alongside a production change. There is no "obvious enough to skip it" case.

**Do NOT author tests from memory, from the conventions of another .NET project, or by copying the shape of a neighbouring fixture.** Several fixtures in the repo predate `TESTING.md` and violate it — imitating them reproduces the violation. `TESTING.md` wins over any existing file you are looking at.

The rules most often got wrong, all of which have caused review churn:

| Rule | Section | Get-it-wrong symptom |
| --- | --- | --- |
| One `[Test]` per method-under-test, all scenarios packed inside | §2, §10 | a family of `…_WhenX_DoesY` tests that share setup |
| Name it `Verify{MethodUnderTest}` — no scenario suffix | §6, §10 | `Verify_that_foo_returns_bar`, `VerifyComputeFooWhenNull` |
| One fixture per production type, mirroring its namespace | §1 | a second `…AspectTestFixture` bolted on beside the real one |
| Every `[Test]` covers positive AND negative | §3, §10 | a happy-path-only test (very easy to do for an async overload) |
| `Assert.That` only — never `Assert.Throws` / `IsTrue` / `AreEqual` | §4 | legacy NUnit API |
| `Has.Count.EqualTo(n)`, not `result.Count, Is.EqualTo(n)` | §8 | asserting on `.Count` directly |
| `Assert.EnterMultipleScope` only around **2+ consecutive** asserts | §5, §10 | a scope wrapping one long fluent chain |
| Indexer / range over LINQ; `Is.SameAs` for POCO identity; `Is.EquivalentTo` when order is irrelevant | §8 | `.First()`, `.Last()`, `Is.EqualTo` on a POCO |
| Assert an out-of-scope `NotSupportedException` stub — don't implement it | §9 | scope creep out of the test project |

The table is a checklist, **not a substitute for reading the file** — it omits the reference fixtures (§11) and the criteria for when separated `[Test]` methods *are* allowed (§2).

## Architecture

Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,117 +1,108 @@
if (jsonElement.TryGetProperty("{{ property.Name }}"u8, out var {{ property.Name }}Property))
if (reader.ValueTextEquals("{{ property.Name }}"u8))
{
{{ property.Name }}Seen = true;
reader.Read();

{{#if (Property.QueryIsReferenceProperty property)}}
{{#if (Property.QueryIsEnumerable property)}}
foreach (var arrayItem in {{ property.Name }}Property.EnumerateArray())
{
if (arrayItem.TryGetProperty("@id"u8, out var {{ property.Name }}ExternalIdProperty))
{
var propertyValue = {{ property.Name }}ExternalIdProperty.GetString();
Utf8JsonReaderHelper.ExpectArrayStart(ref reader);

if (propertyValue != null)
while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
{
dtoInstance.{{Property.WritePropertyName property}}.Add(Guid.Parse(propertyValue));
}
if (Utf8JsonReaderHelper.TryReadReferenceIdentifier(ref reader, out var {{ property.Name }}Value))
{
dtoInstance.{{Property.WritePropertyName property}}.Add({{ property.Name }}Value);
}
}
{{ else }}
if ({{ property.Name }}Property.ValueKind == JsonValueKind.Null)
if (reader.TokenType == JsonTokenType.Null)
{
{{#if (Property.QueryIsNullable property) }}
dtoInstance.{{Property.WritePropertyName property}} = null;
{{else}}
dtoInstance.{{Property.WritePropertyName property}} = Guid.Empty;
logger.LogDebug($"the {{ classContext.Name }}.{{Property.WritePropertyName property}} property was not found in the Json. The value is set to Guid.Empty");

if (logger.IsEnabled(LogLevel.Debug))
{
logger.LogDebug("the {{ classContext.Name }}.{{Property.WritePropertyName property}} property was not found in the Json. The value is set to Guid.Empty");
}
{{/if}}
}
else
{
if ({{ property.Name }}Property.TryGetProperty("@id"u8, out var {{ property.Name }}ExternalIdProperty))
{
var propertyValue = {{ property.Name }}ExternalIdProperty.GetString();

if (propertyValue != null)
else if (Utf8JsonReaderHelper.TryReadReferenceIdentifier(ref reader, out var {{ property.Name }}Value))
{
dtoInstance.{{Property.WritePropertyName property}} = Guid.Parse(propertyValue);
}
}
dtoInstance.{{Property.WritePropertyName property}} = {{ property.Name }}Value;
}
{{/if}}
{{ else }}
{{#if (Property.QueryIsEnumerable property)}}
foreach (var arrayItem in {{ property.Name }}Property.EnumerateArray())
Utf8JsonReaderHelper.ExpectArrayStart(ref reader);

while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
{
{{#if (Property.QueryIsBool property )}}
var propertyValue = arrayItem.GetBoolean();

if (propertyValue != null)
{
dtoInstance.{{Property.WritePropertyName property}}.Add(propertyValue);
}
dtoInstance.{{Property.WritePropertyName property}}.Add(reader.GetBoolean());
{{else if (Property.QueryIsEnum property )}}
throw new NotImplementedException("Enumerable Enum - {{ classContext.Name }}.{{property.Name}} is not yet supported");
{{else if (Property.QueryIsNumeric property )}}
{{#if (Property.QueryIsInteger property) }}
dtoInstance.{{Property.WritePropertyName property}}.Add(arrayItem.GetInt32());
dtoInstance.{{Property.WritePropertyName property}}.Add(reader.GetInt32());
{{else if (Property.QueryIsDouble property) }}
dtoInstance.{{Property.WritePropertyName property}}.Add(arrayItem.GetDouble());
dtoInstance.{{Property.WritePropertyName property}}.Add(reader.GetDouble());
{{ else }}
throw new NotImplementedException("Enumerable Double - {{ classContext.Name }}.{{property.Name}} is not yet supported");
{{/if}}
{{else}}
var propertyValue = arrayItem.GetString();
var {{ property.Name }}Value = reader.GetString();

if (propertyValue != null)
if ({{ property.Name }}Value != null)
{
dtoInstance.{{Property.WritePropertyName property}}.Add(propertyValue);
dtoInstance.{{Property.WritePropertyName property}}.Add({{ property.Name }}Value);
}
{{/if}}
}
{{ else if (Property.QueryIsNullable property) }}
{{#if (Property.QueryIsBool property )}}
dtoInstance.{{Property.WritePropertyName property}} = {{ property.Name }}Property.GetBoolean();
dtoInstance.{{Property.WritePropertyName property}} = reader.GetBoolean();
{{else if (Property.QueryIsEnum property )}}
dtoInstance.{{Property.WritePropertyName property}} = {{ property.Type.Name }}DeSerializer.DeserializeNullable({{ property.Name }}Property.GetString());
dtoInstance.{{Property.WritePropertyName property}} = {{ property.Type.Name }}DeSerializer.DeserializeNullable(reader.GetString());
{{else if (Property.QueryIsNumeric property )}}
{{#if (Property.QueryIsInteger property) }}
dtoInstance.{{Property.WritePropertyName property}} = {{ property.Name }}Property.GetInt32();
dtoInstance.{{Property.WritePropertyName property}} = reader.GetInt32();
{{else if (Property.QueryIsDouble property) }}
dtoInstance.{{Property.WritePropertyName property}} = {{ property.Name }}Property.GetDouble();
dtoInstance.{{Property.WritePropertyName property}} = reader.GetDouble();
{{ else }}
new NotImplementedException("nullable - {{ classContext.Name }}.{{property.Name}} is not yet supported");
{{/if}}
{{else}}
dtoInstance.{{Property.WritePropertyName property}} = {{ property.Name }}Property.GetString();
dtoInstance.{{Property.WritePropertyName property}} = reader.GetString();
{{/if}}
{{ else if (Property.QueryIsScalar property) }}
{{#if (Property.QueryIsBool property )}}
if ({{ property.Name }}Property.ValueKind != JsonValueKind.Null)
if (reader.TokenType != JsonTokenType.Null)
{
dtoInstance.{{Property.WritePropertyName property}} = {{ property.Name }}Property.GetBoolean();
dtoInstance.{{Property.WritePropertyName property}} = reader.GetBoolean();
}
{{else if (Property.QueryIsEnum property )}}
dtoInstance.{{Property.WritePropertyName property}} = {{ property.Type.Name }}DeSerializer.Deserialize({{ property.Name }}Property.GetString());
dtoInstance.{{Property.WritePropertyName property}} = {{ property.Type.Name }}DeSerializer.Deserialize(reader.GetString());
{{else if (Property.QueryIsNumeric property )}}
{{#if (Property.QueryIsInteger property) }}
dtoInstance.{{Property.WritePropertyName property}} = {{ property.Name }}Property.GetInt32();
dtoInstance.{{Property.WritePropertyName property}} = reader.GetInt32();
{{else if (Property.QueryIsDouble property) }}
dtoInstance.{{Property.WritePropertyName property}} = {{ property.Name }}Property.GetDouble();
dtoInstance.{{Property.WritePropertyName property}} = reader.GetDouble();
{{ else }}
new NotImplementedException("Scalar - {{ classContext.Name }}.{{property.Name}} is not yet supported");
{{/if}}
{{else}}
var propertyValue = {{property.Name }}Property.GetString();
var {{ property.Name }}Value = reader.GetString();

if (propertyValue != null)
if ({{ property.Name }}Value != null)
{
dtoInstance.{{Property.WritePropertyName property}} = propertyValue;
dtoInstance.{{Property.WritePropertyName property}} = {{ property.Name }}Value;
}
{{/if}}
{{/if}}
{{/if}}
}
else
{
logger.LogDebug("the {{ property.Name }} Json property was not found in the {{ classContext.Name }}: {Id}", dtoInstance.Id);

continue;
}

Original file line number Diff line number Diff line change
Expand Up @@ -41,27 +41,27 @@ namespace SysML2.NET.Serializer.Json.Core.DTO
/// <summary>
/// a dictionary that provides delegates for deserialization
/// </summary>
private static readonly Dictionary<string, Func<JsonElement, SerializationModeKind, bool, ILoggerFactory, IData>> DeSerializerActionMap = new Dictionary<string, Func<JsonElement, SerializationModeKind, bool, ILoggerFactory, IData>>
private static readonly Dictionary<string, DeSerializeDelegate> DeSerializerActionMap = new Dictionary<string, DeSerializeDelegate>
{
{{#each this as | class |}}
{ "{{ class.Name }}", {{ class.Name }}DeSerializer.DeSerialize },
{{/each}}
};

/// <summary>
/// Provides the delegate <see cref="Func{JsonElement, SerializationModeKind, bool, ILoggerFactory, IData}"/> for the
/// Provides the <see cref="DeSerializeDelegate"/> for the
/// <see cref="System.Type"/> that is to be deserialized
/// </summary>
/// <param name="typeName">
/// The name of the subject <see cref="System.Type"/> that is to be serialized
/// </param>
/// <returns>
/// A Delegate of <see cref="Func{JsonElement, SerializationModeKind, bool, ILoggerFactory, IData}"/>
/// A <see cref="DeSerializeDelegate"/>
/// </returns>
/// <exception cref="NotSupportedException">
/// Thrown when the <see cref="System.Type"/> is not supported.
/// </exception>
internal static Func<JsonElement, SerializationModeKind, bool, ILoggerFactory, IData> Provide(string typeName)
internal static DeSerializeDelegate Provide(string typeName)
{
if (!DeSerializerActionMap.TryGetValue(typeName, out var func))
{
Expand Down
Loading
Loading