diff --git a/packages/cultmath/tests/CultMath.Tests/GlslMirrorTests.Mutants.cs b/packages/cultmath/tests/CultMath.Tests/GlslMirrorTests.Mutants.cs new file mode 100644 index 00000000..a9701838 --- /dev/null +++ b/packages/cultmath/tests/CultMath.Tests/GlslMirrorTests.Mutants.cs @@ -0,0 +1,59 @@ +using System.Reflection; +using System.Text.RegularExpressions; +using Xunit; + +namespace CultMath.Tests; + +public sealed partial class GlslMirrorTests +{ + private const string Weighted = "cultmath_weighted_lattice_cells("; + private const string SmoothStep = "cultmath_lattice_smooth_step("; + + // Edits of the shader text that each break one rule of the lattice loop. M7 (the exit one ring late) is absent: by the + // window-independence proof a later exit visits more cells and changes nothing, so no case can kill it. + private static readonly (string Name, string Function, string Old, string New, int Kills)[] LatticeMutants = + { + ("M1 exit one ring early", Weighted, "0.5 * (float)(k - 1) >= max(f2", "0.5 * (float)k >= max(f2", 3), + ("M2 exit ignores the smoothing band", Weighted, ">= max(f2, f1 + 2.0 * smoothing))", ">= f2)", 1), + ("M3 no pre-normalisation of the normal", Weighted, "normal = stretchNormal / scale;", "normal = stretchNormal;", 1), + ("M4 no unit length for the normal", Weighted, "normal = normal / length(normal);", "normal = normal * 1.0;", 1), + ("M5 smooth step without its clamp", SmoothStep, "return min(next, f1 + smoothing);", "return next;", 1), + ("M6 box pass skips its first row", Weighted, "for (int dz = -reach; dz <= reach; dz++)", "for (int dz = -reach + 1; dz <= reach; dz++)", 1), + }; + + /// + /// Each rule of the lattice loop is pinned by the committed fixture's own cases: the shader text with one rule broken (a + /// mutant mirror, the text a device runs) returns another result than the fixture on at least the stated number of + /// cases of the family that exercises it, the exit one ring early on at least three. The device judge holds the same + /// cases to their recorded bound, so a mutant these cases separate fails it too. + /// + [Fact] + public void TheFixtureCasesSeparateEveryLatticeMutantFromTheShader() + { + var families = FixtureFamilies(); + foreach (var (name, function, old, replacement, kills) in LatticeMutants) + { + var (assembly, errors) = HlslSourceCompatibilityTests.CompileShaderMirror(mutate: text => + { + Assert.True(text.Split(old).Length == 2, $"{name}: the shader text no longer holds exactly one \"{old}\""); + return text.Replace(old, replacement); + }); + Assert.True(assembly is not null, name + ": " + string.Join(Environment.NewLine, errors)); + var shaderType = assembly!.GetType("CultMathHlsl.HlslShader")!; + var shader = Activator.CreateInstance(shaderType); + var mirror = shaderType.GetMethod(function[..^1], BindingFlags.Instance | BindingFlags.NonPublic)!; + + var f = Array.FindIndex(Families, family => family.Glsl.Contains(function)); + var method = Resolve(Families[f].Glsl); + var killed = 0; + foreach (var entry in families[f]!["cases"]!.AsArray()) + { + var sides = Sides(entry); + if (Hex(new[] { mirror.Invoke(shader, Arguments(method, sides[0]))! }) != sides[1]) + killed++; + } + + Assert.True(killed >= kills, $"{name}: the {Families[f].Glsl} fixture cases separate it on {killed} of {Points}, fewer than {kills}"); + } + } +} diff --git a/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs b/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs index 41770546..2c994be8 100644 --- a/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs +++ b/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs @@ -782,9 +782,11 @@ public sealed class HlslSourceCompatibilityTests return "using CultMath;\nusing static CultMath.math;\nnamespace CultMathHlsl;\npublic class HlslShader\n{\n" + members + source + "\n}\n"; } - internal static (Assembly? Assembly, IReadOnlyList Errors) CompileShaderMirror(string members = "", IReadOnlyDictionary? defines = null) + internal static (Assembly? Assembly, IReadOnlyList Errors) CompileShaderMirror(string members = "", IReadOnlyDictionary? defines = null, Func? mutate = null) { var hlsl = ReadShaderSource(FindCultMathRoot()); + if (mutate is not null) + hlsl = mutate(hlsl); var tree = CSharpSyntaxTree.ParseText(TransformHlslToCSharp(hlsl, members, defines)); var references = ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")!) .Split(Path.PathSeparator)