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
16 changes: 16 additions & 0 deletions Schema.Test/GeneratedDataEditorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@
public void TestAGeneratorNamingAKnownDataSourceValidates()
{
Schema schema = CreateSchema(out _);
Assert.IsFalse(schema.Validate().Any(i => i.Message.Contains("data source", StringComparison.Ordinal)));

Check warning on line 118 in Schema.Test/GeneratedDataEditorTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.DoesNotContain' instead of 'Assert.IsFalse'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Schema&issues=AaGkUFf9Kimkqz2Nc7Bv&open=AaGkUFf9Kimkqz2Nc7Bv&pullRequest=360
}

[TestMethod]
Expand All @@ -123,7 +123,7 @@
{
Schema bound = CreateSchema(out _);
string json = SchemaSerializer.Serialize(bound);
StringAssert.Contains(json, "\"dataSource\": \"Items\"");

Check warning on line 126 in Schema.Test/GeneratedDataEditorTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.Contains' instead of 'StringAssert.Contains'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Schema&issues=AaGkUFf9Kimkqz2Nc7Bw&open=AaGkUFf9Kimkqz2Nc7Bw&pullRequest=360

Assert.IsTrue(SchemaSerializer.TryDeserialize(json, out Schema? reloaded));
Assert.AreEqual("Items", reloaded!.CodeGenerators.Single().DataSource.ToString());
Expand Down Expand Up @@ -187,7 +187,7 @@

string[] expected = [.. SchemaDataValidator.ValidateData(item, payload, "Items").Select(i => i.ToString())];
string[] actual = [.. Issues(editor).Select(i => i.ToString())];
CollectionAssert.AreEqual(expected, actual, payload);

Check warning on line 190 in Schema.Test/GeneratedDataEditorTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.AreSequenceEqual' instead of 'CollectionAssert.AreEqual'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Schema&issues=AaGkUFf9Kimkqz2Nc7Bx&open=AaGkUFf9Kimkqz2Nc7Bx&pullRequest=360
Assert.AreEqual(expected.Length == 0 || !expected.Any(e => e.StartsWith("Error", StringComparison.Ordinal)), Get(editor, "IsValid"), payload);
}
}
Expand All @@ -203,10 +203,10 @@
// A record added and never filled in is missing its members, which the validator reports.
_ = Invoke(editor, "Add");
string json = (string)Invoke(editor, "ToJson");
Assert.AreNotEqual(0, SchemaDataValidator.ValidateData(item, json, "Items").Count);

Check warning on line 206 in Schema.Test/GeneratedDataEditorTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.IsNotEmpty' instead of 'Assert.AreNotEqual'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Schema&issues=AaGkUFf9Kimkqz2Nc7Bz&open=AaGkUFf9Kimkqz2Nc7Bz&pullRequest=360
CollectionAssert.AreEqual(
SchemaDataValidator.ValidateData(item, json, "Items").Select(i => i.ToString()).ToArray(),
Issues(editor).Select(i => i.ToString()).ToArray());

Check warning on line 209 in Schema.Test/GeneratedDataEditorTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.AreSequenceEqual' instead of 'CollectionAssert.AreEqual'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Schema&issues=AaGkUFf9Kimkqz2Nc7By&open=AaGkUFf9Kimkqz2Nc7By&pullRequest=360
}

// ---- typed accessors ------------------------------------------------------
Expand Down Expand Up @@ -308,6 +308,22 @@
Assert.ThrowsExactly<InvalidOperationException>(() => SchemaDataValues.GetString(record, "Missing"));
}

[TestMethod]
public void TestANumberPastItsTypesRangeThrowsRatherThanReadingAsInfinity()
{
JsonObject record = new()
{
["Speed"] = JsonNode.Parse("1e39"),
["D"] = JsonNode.Parse("1e309"),
["Position"] = JsonNode.Parse("[1, 1e39, 2]"),
};

Assert.ThrowsExactly<InvalidOperationException>(() => SchemaDataValues.GetFloat(record, "Speed"));
Assert.ThrowsExactly<InvalidOperationException>(() => SchemaDataValues.GetDouble(record, "D"));
Assert.ThrowsExactly<InvalidOperationException>(() => SchemaDataValues.GetVector3(record, "Position"));
Assert.AreEqual(1e39, SchemaDataValues.GetDouble(record, "Speed"));
}

[TestMethod]
public void TestANonFiniteValueIsRefusedAndTheEditorKeepsWorking()
{
Expand All @@ -322,7 +338,7 @@
{
TargetInvocationException thrown = Assert.ThrowsExactly<TargetInvocationException>(() => Set(sword, "Weight", bad));
Assert.IsInstanceOfType<ArgumentOutOfRangeException>(thrown.InnerException);
StringAssert.Contains(thrown.InnerException!.Message, "'Weight'");

Check warning on line 341 in Schema.Test/GeneratedDataEditorTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.Contains' instead of 'StringAssert.Contains'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Schema&issues=AaGkUFf9Kimkqz2Nc7B1&open=AaGkUFf9Kimkqz2Nc7B1&pullRequest=360
}

TargetInvocationException tint = Assert.ThrowsExactly<TargetInvocationException>(
Expand All @@ -332,7 +348,7 @@
// The refused assignments changed nothing, so everything that goes through ToJson still works.
Assert.AreEqual(1.5f, Get(sword, "Weight"));
Assert.AreEqual(new ColorRgb(1, 0.5f, 0), Get(sword, "Tint"));
CollectionAssert.AreEqual(issuesBefore, Issues(editor).Select(i => i.ToString()).ToArray());

Check warning on line 351 in Schema.Test/GeneratedDataEditorTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.AreSequenceEqual' instead of 'CollectionAssert.AreEqual'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Schema&issues=AaGkUFf9Kimkqz2Nc7B0&open=AaGkUFf9Kimkqz2Nc7B0&pullRequest=360
Assert.IsTrue((bool)Get(editor, "IsValid")!);
Assert.AreEqual(ConformingItems, Invoke(editor, "ToJson"));
Invoke(editor, "Save", Path.Combine(workingDirectory, "items.json"));
Expand Down
56 changes: 56 additions & 0 deletions Schema.Test/SchemaDataValidatorTypeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
[TestMethod]
public void ASemanticValueIsCheckedAsItsRepresentation()
{
Assert.HasCount(0, Validate(EntityId, "42"));

Check warning on line 68 in Schema.Test/SchemaDataValidatorTypeTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.IsEmpty' instead of 'Assert.HasCount'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Schema&issues=AaGkUFevKimkqz2Nc7Bh&open=AaGkUFevKimkqz2Nc7Bh&pullRequest=360
Assert.HasCount(1, Validate(EntityId, "\"42\""));
Assert.HasCount(1, Validate(EntityId, "4.5"));
}
Expand All @@ -75,37 +75,37 @@
{
Semantic missing = new() { SemanticTypeName = "Missing".As<SemanticTypeName>() };

Assert.HasCount(0, Validate(missing, "\"anything\""));

Check warning on line 78 in Schema.Test/SchemaDataValidatorTypeTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.IsEmpty' instead of 'Assert.HasCount'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Schema&issues=AaGkUFevKimkqz2Nc7Bi&open=AaGkUFevKimkqz2Nc7Bi&pullRequest=360
}

[TestMethod]
public void AMagnitudeIsOneNumber()
{
Assert.HasCount(0, Validate(Quantity("Mass"), "75.5"));

Check warning on line 84 in Schema.Test/SchemaDataValidatorTypeTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.IsEmpty' instead of 'Assert.HasCount'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Schema&issues=AaGkUFevKimkqz2Nc7Bj&open=AaGkUFevKimkqz2Nc7Bj&pullRequest=360
Assert.HasCount(1, Validate(Quantity("Mass"), "[75.5]"));
}

[TestMethod]
public void AQuantityStoredInAWholeNumberRefusesAFraction()
{
Assert.HasCount(0, Validate(Quantity("Mass", new Int()), "75"));

Check warning on line 91 in Schema.Test/SchemaDataValidatorTypeTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.IsEmpty' instead of 'Assert.HasCount'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Schema&issues=AaGkUFevKimkqz2Nc7Bk&open=AaGkUFevKimkqz2Nc7Bk&pullRequest=360
Assert.HasCount(1, Validate(Quantity("Mass", new Int()), "75.5"));
}

[TestMethod]
public void AVectorQuantityIsAnArrayOfItsComponents()
{
Assert.HasCount(0, Validate(Quantity("Velocity3D"), "[1, 2, 3]"));

Check warning on line 98 in Schema.Test/SchemaDataValidatorTypeTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.IsEmpty' instead of 'Assert.HasCount'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Schema&issues=AaGkUFevKimkqz2Nc7Bm&open=AaGkUFevKimkqz2Nc7Bm&pullRequest=360

Collection<SchemaValidationIssue> issues = Validate(Quantity("Velocity3D"), "[1, true]");
Assert.IsTrue(issues.Any(i => i.Path == "data.Value[1]"), string.Join("; ", issues));

Check warning on line 101 in Schema.Test/SchemaDataValidatorTypeTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.Contains' instead of 'Assert.IsTrue'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Schema&issues=AaGkUFevKimkqz2Nc7Bn&open=AaGkUFevKimkqz2Nc7Bn&pullRequest=360
Assert.IsTrue(issues.Any(i => i.Path == "data.Value" && i.Message.Contains('3', StringComparison.Ordinal)), string.Join("; ", issues));

Check warning on line 102 in Schema.Test/SchemaDataValidatorTypeTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.Contains' instead of 'Assert.IsTrue'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Schema&issues=AaGkUFevKimkqz2Nc7Bo&open=AaGkUFevKimkqz2Nc7Bo&pullRequest=360
}

[TestMethod]
public void AnUnknownQuantityIsLeftToSchemaValidation()
{
Assert.HasCount(0, Validate(Quantity("Squiggles"), "\"anything\""));

Check warning on line 108 in Schema.Test/SchemaDataValidatorTypeTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.IsEmpty' instead of 'Assert.HasCount'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Schema&issues=AaGkUFevKimkqz2Nc7Bl&open=AaGkUFevKimkqz2Nc7Bl&pullRequest=360
}

[TestMethod]
Expand All @@ -113,16 +113,16 @@
{
Optional optional = new() { ElementType = new Int() };

Assert.HasCount(0, Validate(optional, "null"));

Check warning on line 116 in Schema.Test/SchemaDataValidatorTypeTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.IsEmpty' instead of 'Assert.HasCount'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Schema&issues=AaGkUFevKimkqz2Nc7Br&open=AaGkUFevKimkqz2Nc7Br&pullRequest=360
Assert.HasCount(0, Validate(optional, "3"));

Check warning on line 117 in Schema.Test/SchemaDataValidatorTypeTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.IsEmpty' instead of 'Assert.HasCount'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Schema&issues=AaGkUFevKimkqz2Nc7Bs&open=AaGkUFevKimkqz2Nc7Bs&pullRequest=360
Assert.HasCount(1, Validate(optional, "\"nope\""));
}

[TestMethod]
public void AVectorChecksItsComponentsAgainstItsElementType()
{
Assert.HasCount(0, Validate(new Vector3 { ElementType = new Int() }, "[1, 2, 3]"));

Check warning on line 124 in Schema.Test/SchemaDataValidatorTypeTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.IsEmpty' instead of 'Assert.HasCount'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Schema&issues=AaGkUFevKimkqz2Nc7Bp&open=AaGkUFevKimkqz2Nc7Bp&pullRequest=360
Assert.HasCount(0, Validate(new Vector3(), "[1.5, 2, 3]"));

Check warning on line 125 in Schema.Test/SchemaDataValidatorTypeTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.IsEmpty' instead of 'Assert.HasCount'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Schema&issues=AaGkUFevKimkqz2Nc7Bq&open=AaGkUFevKimkqz2Nc7Bq&pullRequest=360
Assert.HasCount(1, Validate(new Vector3 { ElementType = EntityId }, "[1, 2.5, 3]"));
}

Expand All @@ -136,4 +136,60 @@
Assert.AreEqual(SchemaValidationSeverity.Warning, issues[0].Severity, type.DisplayName);
}
}

// ---- floating-point overflow -----------------------------------------------

[TestMethod]
[DataRow("1e39")]
[DataRow("-1e39")]
public void AFloatPastFloatsRangeIsAnError(string value)
{
Collection<SchemaValidationIssue> issues = Validate(new Float(), value);

Assert.HasCount(1, issues, string.Join("; ", issues));
Assert.AreEqual(SchemaValidationSeverity.Error, issues[0].Severity);
Assert.Contains("range of Float", issues[0].Message);
}

[TestMethod]
[DataRow("3.4028234663852886E+38")]
[DataRow("1e38")]
[DataRow("0")]
public void AFloatAtOrInsideFloatsRangeIsAccepted(string value) =>
Assert.HasCount(0, Validate(new Float(), value));

Check warning on line 159 in Schema.Test/SchemaDataValidatorTypeTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.IsEmpty' instead of 'Assert.HasCount'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Schema&issues=AaGkUFevKimkqz2Nc7Bt&open=AaGkUFevKimkqz2Nc7Bt&pullRequest=360

[TestMethod]
public void ADoublePastDoublesRangeIsAnError()
{
Assert.HasCount(1, Validate(new Double(), "1e309"));
Assert.HasCount(0, Validate(new Double(), "1e39"));

Check warning on line 165 in Schema.Test/SchemaDataValidatorTypeTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.IsEmpty' instead of 'Assert.HasCount'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Schema&issues=AaGkUFevKimkqz2Nc7Bu&open=AaGkUFevKimkqz2Nc7Bu&pullRequest=360
}

[TestMethod]
public void AFloatVectorComponentPastFloatsRangeIsAnError()
{
Collection<SchemaValidationIssue> issues = Validate(new Vector3(), "[1, 1e39, 2]");

Assert.HasCount(1, issues, string.Join("; ", issues));
Assert.AreEqual("data.Value[1]", issues[0].Path);
}

[TestMethod]
public void AColourComponentAndAQuantityPastFloatsRangeAreErrors()
{
Assert.HasCount(1, Validate(new ColorRGB(), "[0, 1e39, 0]"));
Assert.HasCount(1, Validate(Quantity("Mass"), "1e39"));
}

[TestMethod]
public void ASemanticTypeOverAFloatIsHeldToFloatsRange()
{
SchemaClass schemaClass = ClassWith((schema, c) =>
{
schema.AddSemanticType("Speed".As<SemanticTypeName>())!.SetUnderlyingType(new Float());
c.AddMember("Value".As<MemberName>())!.SetType(new Semantic { SemanticTypeName = "Speed".As<SemanticTypeName>() });
});

Assert.HasCount(1, SchemaDataValidator.ValidateData(schemaClass, """{ "Value": 1e39 }""", "data"));
}
}
21 changes: 19 additions & 2 deletions Schema/Models/SchemaDataValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ private static void ValidateValue(BaseType type, JsonElement value, string path,
break;

case Float or Double:
Expect(value, JsonValueKind.Number, path, type, issues);
ExpectNumber(value, type, path, type, issues);
break;

case Types.String:
Expand Down Expand Up @@ -399,7 +399,24 @@ private static void ExpectNumber(JsonElement value, BaseType storage, string pat
return;
}

Expect(value, JsonValueKind.Number, path, type, issues);
if (value.ValueKind != JsonValueKind.Number)
{
Expect(value, JsonValueKind.Number, path, type, issues);
return;
}

// A number past what the storage holds reads back as an infinity rather than failing, so
// "1e39" in a Float passes every reader and arrives as one. Refuse it here, as the
// integral types already are and as schema validation already refuses such a default.
bool finite = value.TryGetDouble(out double number) && double.IsFinite(number);
if (represented is Float && !(finite && Math.Abs(number) <= float.MaxValue))
{
issues.Add(Error(path, $"Expected a number within the range of Float (±{float.MaxValue.ToString("R", CultureInfo.InvariantCulture)}) for type '{type.DisplayName}' but found {value.GetRawText()}."));
}
else if (represented is Double && !finite)
{
issues.Add(Error(path, $"Expected a number within the range of Double (±{double.MaxValue.ToString("R", CultureInfo.InvariantCulture)}) for type '{type.DisplayName}' but found {value.GetRawText()}."));
}
}

private static void ValidateEnumValue(Types.Enum enumType, JsonElement value, string path, Collection<SchemaValidationIssue> issues)
Expand Down
7 changes: 4 additions & 3 deletions Schema/Runtime/SchemaDataValues.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
return true;
}

foreach (KeyValuePair<string, JsonNode?> property in record)

Check warning on line 45 in Schema/Runtime/SchemaDataValues.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Loops should be simplified using the "Where" LINQ method

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Schema&issues=AaGkUFdTKimkqz2Nc7Bf&open=AaGkUFdTKimkqz2Nc7Bf&pullRequest=360
{
if (string.Equals(property.Key, name, StringComparison.OrdinalIgnoreCase))
{
Expand Down Expand Up @@ -115,7 +115,7 @@
/// <param name="name">The member's declared name.</param>
/// <returns>The value.</returns>
public static float GetFloat(JsonObject record, string name) =>
ReadNumber(record, name, static (JsonElement e, out float v) => e.TryGetSingle(out v));
ReadNumber(record, name, static (JsonElement e, out float v) => e.TryGetSingle(out v) && float.IsFinite(v));

/// <summary>Writes a <c>Float</c> member.</summary>
/// <param name="record">The record.</param>
Expand All @@ -130,7 +130,7 @@
/// <param name="name">The member's declared name.</param>
/// <returns>The value.</returns>
public static double GetDouble(JsonObject record, string name) =>
ReadNumber(record, name, static (JsonElement e, out double v) => e.TryGetDouble(out v));
ReadNumber(record, name, static (JsonElement e, out double v) => e.TryGetDouble(out v) && double.IsFinite(v));

/// <summary>Writes a <c>Double</c> member.</summary>
/// <param name="record">The record.</param>
Expand Down Expand Up @@ -206,7 +206,7 @@
string text = GetString(record, name);

// Ordinal, as the validator matches: a value spelled in another case is not that value.
foreach (TEnum value in System.Enum.GetValues<TEnum>())

Check warning on line 209 in Schema/Runtime/SchemaDataValues.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Loops should be simplified using the "Where" LINQ method

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Schema&issues=AaGkUFdTKimkqz2Nc7Bg&open=AaGkUFdTKimkqz2Nc7Bg&pullRequest=360
{
if (string.Equals(value.ToString(), text, StringComparison.Ordinal))
{
Expand Down Expand Up @@ -346,7 +346,8 @@
{
JsonNode? component = array[i];
if (component?.GetValueKind() != JsonValueKind.Number
|| !JsonSerializer.SerializeToElement(component).TryGetSingle(out components[i]))
|| !JsonSerializer.SerializeToElement(component).TryGetSingle(out components[i])
|| !float.IsFinite(components[i]))
{
throw Malformed(name, $"component {i} is not a number");
}
Expand Down
Loading