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
18 changes: 9 additions & 9 deletions Schema.Test/CodeGenerationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@
SchemaGenerationResult result = SchemaGenerator.Generate(schema, generator);

Assert.AreEqual(SchemaGenerationStatus.SchemaInvalid, result.Status);
Assert.IsTrue(result.Issues.Any(i => i.Message.Contains("Name 'Item' is used by both a class and an enum")));

Check warning on line 128 in Schema.Test/CodeGenerationTests.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=AaFhmF0HDd14X8jnxiOX&open=AaFhmF0HDd14X8jnxiOX&pullRequest=331
}

[TestMethod]
Expand Down Expand Up @@ -248,17 +248,17 @@
[DataRow("Ratio", "public float Ratio")]
[DataRow("Precise", "public double Precise")]
[DataRow("Name", "public string Name")]
[DataRow("CreatedAt", "public System.DateTime CreatedAt")]
[DataRow("Uptime", "public System.TimeSpan Uptime")]
[DataRow("Position2", "public System.Numerics.Vector2 Position2")]
[DataRow("Position3", "public System.Numerics.Vector3 Position3")]
[DataRow("Position4", "public System.Numerics.Vector4 Position4")]
[DataRow("Tint", "public ktsu.Schema.Runtime.ColorRgb Tint")]
[DataRow("Overlay", "public ktsu.Schema.Runtime.ColorRgba Overlay")]
[DataRow("CreatedAt", "public global::System.DateTime CreatedAt")]
[DataRow("Uptime", "public global::System.TimeSpan Uptime")]
[DataRow("Position2", "public global::System.Numerics.Vector2 Position2")]
[DataRow("Position3", "public global::System.Numerics.Vector3 Position3")]
[DataRow("Position4", "public global::System.Numerics.Vector4 Position4")]
[DataRow("Tint", "public global::ktsu.Schema.Runtime.ColorRgb Tint")]
[DataRow("Overlay", "public global::ktsu.Schema.Runtime.ColorRgba Overlay")]
[DataRow("Role", "public Role Role")]
[DataRow("Favourite", "public Item Favourite")]
[DataRow("Tags", "public System.Collections.Generic.List<string> Tags")]
[DataRow("Items", "public System.Collections.Generic.Dictionary<string, Item> Items")]
[DataRow("Tags", "public global::System.Collections.Generic.List<string> Tags")]
[DataRow("Items", "public global::System.Collections.Generic.Dictionary<string, Item> Items")]
public void TestEveryTypeMapsAsDocumented(string memberName, string expectedDeclaration)
{
Schema schema = CreateFullSchema();
Expand Down
131 changes: 131 additions & 0 deletions Schema.Test/GeneratedNameCollisionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace ktsu.Schema.Tests;

using System.Collections.ObjectModel;

using ktsu.Schema.Generation;
using ktsu.Schema.Models;
using ktsu.Schema.Models.Names;
using ktsu.Schema.Models.Types;
using ktsu.Semantics.Strings;

using Microsoft.VisualStudio.TestTools.UnitTesting;

/// <summary>
/// Names a schema may legitimately choose that collide with something the generated C# needs.
/// </summary>
/// <remarks>
/// Two kinds. A class or namespace named <c>System</c> or <c>ktsu</c> shadows the namespace a
/// generated type reference starts with, which every reference now avoids by being written from
/// <c>global::</c>. A member named after the type declaring it has no spelling at all in C# or
/// C++, so validation refuses it before a generator is asked.
/// </remarks>
[TestClass]
public sealed class GeneratedNameCollisionTests
{
/// <summary>
/// A class called <c>System</c> beside a class whose members are spelled with <c>System</c>
/// types. Without <c>global::</c> the second class resolved <c>System.Numerics</c> against the
/// first and did not compile.
/// </summary>
[TestMethod]
[DataRow("System")]
[DataRow("ktsu")]
public void AClassNamedAfterARootNamespaceStillCompiles(string name)
{
Schema schema = new();
schema.AddClass(name.As<ClassName>())!.AddMember("Id".As<MemberName>())!.SetType(new Int());
AddTransform(schema);

CompileGenerated(schema, "Generated");
}

/// <summary>
/// A generator namespace whose last segment is <c>System</c> shadows the root namespace for
/// every type declared inside it.
/// </summary>
[TestMethod]
public void ANamespaceEndingInSystemStillCompiles()
{
Schema schema = new();
AddTransform(schema);

CompileGenerated(schema, "Game.System");
}

[TestMethod]
public void AMemberNamedAfterItsClassIsRefused()
{
Schema schema = new();
schema.AddClass("Score".As<ClassName>())!.AddMember("Score".As<MemberName>())!.SetType(new Int());

AssertRefused(schema, "Score.Score", "Member 'Score' has the same name as its class");
}

[TestMethod]
public void ASemanticTypeNamedValueIsRefused()
{
Schema schema = new();
schema.AddSemanticType("Value".As<SemanticTypeName>())!.SetUnderlyingType(new Float());

AssertRefused(schema, "Value", "Semantic type 'Value' has the same name as the 'Value' member");
}

[TestMethod]
public void AFunctionNamedAfterItsInterfaceIsRefused()
{
Schema schema = new();
schema.AddInterface("Draw".As<InterfaceName>())!
.AddFunction("Draw".As<FunctionName>())!
.SetReturnType(new Void());

AssertRefused(schema, "Draw.Draw", "Function 'Draw' has the same name as its interface");
}

/// <summary>
/// The comparison is ordinal, because both languages are case sensitive: <c>score</c> in
/// <c>Score</c> is a legal declaration and stays accepted.
/// </summary>
[TestMethod]
public void ANameDifferingOnlyInCaseIsAccepted()
{
Schema schema = new();
schema.AddClass("Score".As<ClassName>())!.AddMember("score".As<MemberName>())!.SetType(new Int());

Collection<SchemaValidationIssue> issues = schema.Validate();

Assert.IsEmpty(issues, string.Join("; ", issues));
}

private static void AddTransform(Schema schema)
{
SchemaClass transform = schema.AddClass("Transform".As<ClassName>())!;
transform.AddMember("Position".As<MemberName>())!.SetType(new Vector3());
transform.AddMember("Tags".As<MemberName>())!.SetType(new Array
{
ElementType = new String(),
Container = Array.VectorContainer.As<ContainerName>(),
});
transform.AddMember("Created".As<MemberName>())!.SetType(new DateTime());
}

private static void CompileGenerated(Schema schema, string codeNamespace)
{
SchemaGenerationResult result = SchemaGenerator.Generate(schema, CodeGenerationTests.ConfigureGenerator(schema, codeNamespace));
Assert.IsTrue(result.IsSuccess, result.Message);
GeneratedSourceCompiler.Compile(result.Files);
}

private static void AssertRefused(Schema schema, string path, string message)
{
Collection<SchemaValidationIssue> issues = schema.Validate();

Assert.Contains(
i => i.Severity == SchemaValidationSeverity.Error
&& i.Path == path
&& i.Message.Contains(message, System.StringComparison.Ordinal),
issues,
string.Join("; ", issues));
}
}
7 changes: 6 additions & 1 deletion Schema.Test/GeneratorRegistryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,15 @@ namespace ktsu.Schema.Tests;
/// found by the language a schema names, which is the whole reason the registry is not a fixed
/// table.
/// <para>
/// The registry is process-wide, so each test here registers a language nothing else uses.
/// The registry is process-wide, so each test here registers a language nothing else uses. It is
/// also an unsynchronised dictionary, documented as meant to be filled once at start-up, so two of
/// these tests registering at once can lose a write while it resizes. That failed CI once on
/// Windows, with a generator registered a line earlier reported as unknown, so the class runs its
/// tests one at a time.
/// </para>
/// </remarks>
[TestClass]
[DoNotParallelize]
public sealed class GeneratorRegistryTests
{
/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion Schema/Cli/SchemaCommandLine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public static int Run(string[] args, TextWriter output, TextWriter error, Schema
{
"generate" => RejectUnknownOptions([.. args.Skip(1)], [GeneratorOption], output, error, host)
?? Generate([.. args.Skip(1)], output, error),
"validate" => RejectUnknownOptions([.. args.Skip(1)], [], output, error, host)
"validate" => RejectUnknownOptions([.. args.Skip(1)], [NoDataOption], output, error, host)
?? Validate([.. args.Skip(1)], output, error),
_ => UnknownCommand(args[0], output, error, host),
};
Expand Down
Loading
Loading