diff --git a/Schema.Cpp.Test/ExemplarSemanticTypeTests.cs b/Schema.Cpp.Test/ExemplarSemanticTypeTests.cs index 26a9bf9..07d6755 100644 --- a/Schema.Cpp.Test/ExemplarSemanticTypeTests.cs +++ b/Schema.Cpp.Test/ExemplarSemanticTypeTests.cs @@ -2,6 +2,8 @@ namespace ktsu.Schema.Cpp.Test; +using System.Diagnostics; + using ktsu.Schema.Models; using ktsu.Schema.Models.Names; using ktsu.Schema.Models.Types; @@ -55,6 +57,7 @@ public sealed class ExemplarSemanticTypeTests #pragma once + #include #include #include @@ -169,6 +172,124 @@ public void ATypeTheTargetAlreadyHasIsNotGenerated() Assert.Contains("EntityId.gen.hpp", files.Keys); } + /// + /// Every generated header compiles with nothing included ahead of it. + /// + /// + /// The shape of the defect rather than the one header that had it. Every other compiling test + /// here includes the whole emitted set as one translation unit, so a header that depends on + /// something it never included still compiles as long as some other header in the set reached + /// it first. Compiling each one alone is what makes an include the header's own business. + /// + /// A missing include only fails here if it is needed to declare what the header + /// declares, which is why this loop passes an empty main and why it did not, on its + /// own, catch the <compare> defect - see the test below. + /// + /// + [TestMethod] + public void EveryGeneratedHeaderCompilesOnItsOwn() + { + IReadOnlyDictionary files = Generate(); + + foreach (string header in files.Keys.Order(StringComparer.Ordinal)) + { + AssertCompilesAlone(header, files); + } + } + + /// + /// Ordering two values of a semantic type compiles with only that type's header included. + /// + /// + /// The comparison is defaulted, so nothing deduces its return type until something orders two + /// values: include the header, compile an empty main, and a missing + /// <compare> goes unnoticed on every platform. Ordering a pair is what forces + /// std::strong_ordering to be named, and neither libstdc++ nor libc++ reaches it + /// through <cstdint> or <type_traits> - so with the include removed + /// this fails on Linux too, which is what stops the next omission of its kind waiting for a + /// macOS run to be found. + /// + [TestMethod] + public void OrderingASemanticTypeCompilesWithOnlyItsOwnHeader() => + AssertCompilesAlone( + "EntityId.gen.hpp", + Generate(), + "return (holo::EntityId{} <=> holo::EntityId{}) < 0 ? 1 : 0;"); + + /// + /// Writes the whole emitted set so includes between headers still resolve, then compiles a + /// translation unit whose only include is the header under test. + /// + /// The header to compile. + /// Every generated file, by name. + /// The body of main, for a header whose defect only a use reveals. + private static void AssertCompilesAlone( + string header, + IReadOnlyDictionary files, + string body = "return 0;") + { + string directory = Path.Join(Path.GetTempPath(), $"schema-alone-{Guid.NewGuid():N}"); + + Directory.CreateDirectory(directory); + + try + { + foreach ((string name, string text) in files) + { + File.WriteAllText(Path.Join(directory, name), text); + } + + File.WriteAllText( + Path.Join(directory, "only.cpp"), + $"#include \"{header}\"\n\nint main()\n{{\n {body}\n}}\n"); + + (int exitCode, string output) = Compile(directory, "only.cpp"); + + Assert.AreEqual(0, exitCode, $"{header} should compile with nothing included ahead of it:\n{output}"); + } + finally + { + Directory.Delete(directory, recursive: true); + } + } + + private static (int ExitCode, string Output) Compile(string directory, string file) + { + string? compiler = Find("g++") ?? Find("clang++"); + + if (compiler is null) + { + Assert.Inconclusive("no C++ compiler on PATH, so the generated headers were not compiled."); + } + + using Process process = new() + { + StartInfo = new ProcessStartInfo(compiler!) + { + WorkingDirectory = directory, + RedirectStandardError = true, + RedirectStandardOutput = true, + }, + }; + + foreach (string argument in (string[])["-std=c++20", "-Wall", "-Wextra", "-fsyntax-only", "-I.", file]) + { + process.StartInfo.ArgumentList.Add(argument); + } + + process.Start(); + string output = process.StandardError.ReadToEnd() + process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + + return (process.ExitCode, output); + } + + private static string? Find(string executable) => + (Environment.GetEnvironmentVariable("PATH") ?? string.Empty) + .Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries) + .Select(directory => Path.Join(directory, Path.GetFileName(executable))) + .FirstOrDefault(File.Exists); + private static IReadOnlyDictionary Generate() => Generate(new CppGeneratorOptions()); private static IReadOnlyDictionary Generate(CppGeneratorOptions options) diff --git a/Schema.Cpp/CppFileBuilder.cs b/Schema.Cpp/CppFileBuilder.cs index f333f0d..b001c25 100644 --- a/Schema.Cpp/CppFileBuilder.cs +++ b/Schema.Cpp/CppFileBuilder.cs @@ -146,6 +146,15 @@ public SourceFile SemanticType(SchemaSemanticType semanticType) declaration.Members.Add(Narrowing(name, refined.Name.ToString())); } + // A defaulted operator<=> has std::strong_ordering - or whichever of its siblings the + // representation yields - as its return type, so this header depends on . It + // never said so, and nothing noticed: the tests that compile generated C++ include the + // whole emitted set as one translation unit, and under libstdc++ another header in that + // set reaches while under libc++ none does. That is the whole reason Apple + // clang was the only compiler to refuse it. Required here rather than left to whoever + // includes this one, which is the rule the refined header above already follows. + mapper.Require(""); + declaration.Members.Add(Comparison(name, "==", "bool")); declaration.Members.Add(Comparison(name, "<=>", "auto")); declaration.Members.Add(new FieldDeclaration(ValueField, UnderlyingAlias) { Visibility = Visibility.Private });