Merge Stryker adoption and the CultMath tests it pointed at
Stryker.NET becomes CultLib's mutation tool (CultMath config, baseline 2243 mutants, 350 survived). Behavioural tests close the three areas Aetheria leans on: the BatchMath SIMD body, first_order_intercept's branches, and equality and comparison across the vector types. 350 -> 264 survivors; Soul passed the cut after one accumulation test was added. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
29e50ad315
7 changed files with 1308 additions and 0 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -6,6 +6,9 @@
|
|||
# dotenv files
|
||||
.env
|
||||
|
||||
# Stryker.NET mutation-testing output
|
||||
StrykerOutput/
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
|
|
|
|||
86
docs/mutation-testing.md
Normal file
86
docs/mutation-testing.md
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
# Mutation Testing
|
||||
|
||||
CultLib measures its C# test suites with Stryker.NET, pinned as a repo-local
|
||||
tool in `dotnet-tools.json`. Coverage is added project by project; a project
|
||||
with no `stryker-config.json` next to its test project has not been scoped
|
||||
yet (see "Follow-ups" below for what's outstanding).
|
||||
|
||||
## Running it
|
||||
|
||||
Restore the tool once per checkout:
|
||||
|
||||
```powershell
|
||||
dotnet tool restore
|
||||
```
|
||||
|
||||
Run a full mutation pass from a covered test project's directory, for
|
||||
example CultMath:
|
||||
|
||||
```powershell
|
||||
cd packages/cultmath/tests/CultMath.Tests
|
||||
dotnet stryker -t mtp
|
||||
```
|
||||
|
||||
`-t mtp` selects Stryker's Microsoft.Testing.Platform runner. It is required
|
||||
for test projects on `xunit.v3` (CultMath.Tests is one): Stryker's default
|
||||
`vstest` runner's coverage capture does not work against xunit.v3's native
|
||||
test host and the run stalls retrying test sessions indefinitely. Projects
|
||||
still on `xunit` v2 with `xunit.runner.visualstudio` can use the default
|
||||
`vstest` runner.
|
||||
|
||||
For a single cut, scope the run to what changed instead of the whole
|
||||
project:
|
||||
|
||||
```powershell
|
||||
dotnet stryker -t mtp --since:<base-commit-or-branch>
|
||||
```
|
||||
|
||||
Reports land in `StrykerOutput/` next to the test project (git-ignored).
|
||||
Open the `reports/mutation-report.html` file for a browsable view, or read
|
||||
`reports/mutation-report.json` for a scriptable one.
|
||||
|
||||
## Triage rules
|
||||
|
||||
The mutation score is not a gate (`break` threshold is 0). Every surviving
|
||||
mutant is a finding about the tests, not a build failure, and gets triaged
|
||||
into one of:
|
||||
|
||||
- **missing behavioural test** — name the test that should exist and what
|
||||
branch, edge case, or component it would exercise.
|
||||
- **degenerate fixture** — the code path is exercised, but the fixture's
|
||||
values make two different mutants produce the same observable result (a
|
||||
zero component that erases a `+`/`-` distinction, an already-normalized
|
||||
input that erases a `*`/`/` distinction, all-equal or all-same-sign inputs
|
||||
that erase a boundary or `&&`/`||` distinction). Fix the fixture, don't
|
||||
invent a new one that only kills that one mutant.
|
||||
- **equivalent** — the mutant cannot change behaviour. Float-threshold
|
||||
boundary flips (`>` to `>=`) are equivalent by default. A reasoned
|
||||
equivalence claim (for example, `>>` and `>>>` on an already-unsigned
|
||||
operand) needs its one-line reason recorded, not assumed.
|
||||
|
||||
Do not write a hand-rolled mutation suite or anchor test as a substitute.
|
||||
Where the tool can't reach (native/shader/editor code), the defence is a
|
||||
behavioural test at the layer the rule is decided, not a committed mutation
|
||||
harness.
|
||||
|
||||
## Known tool caveat
|
||||
|
||||
Stryker 5.0.0's `mtp` runner is documented as preview. Its per-test coverage
|
||||
capture was found to report false "Survived" verdicts specifically for
|
||||
mutations inside `static readonly` field initializers that a test does
|
||||
cover (confirmed by hand-mutating `bool3.@false` and `bool4.@true` and
|
||||
running the full suite directly with `dotnet test`, both of which fail
|
||||
against the mutation that Stryker reported as surviving). Treat a survivor
|
||||
inside a `static readonly` field initializer as unproven until confirmed
|
||||
with a direct `dotnet test` run against the same mutation; do not triage it
|
||||
as a missing test without that check.
|
||||
|
||||
## Follow-ups
|
||||
|
||||
CultMath.Tests is the only project currently scoped. `GameCult.Caching`,
|
||||
`GameCult.Geometry`, `GameCult.Mesh` (+ `.Quic`, `.Quic.Native`),
|
||||
`GameCult.Networking` (+ `.WebSockets`), and the non-.NET runtimes
|
||||
(`cultcache-rs/py/ts`, `cultmesh-*`, `cultnet-*`) each need their own
|
||||
`stryker-config.json` (or ecosystem-equivalent tool: `cargo-mutants` for
|
||||
Rust, `StrykerJS` for TypeScript, `mutmut` for Python) and a baseline pass.
|
||||
None of that is wired into CI yet.
|
||||
13
dotnet-tools.json
Normal file
13
dotnet-tools.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"version": 1,
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-stryker": {
|
||||
"version": "5.0.0",
|
||||
"commands": [
|
||||
"dotnet-stryker"
|
||||
],
|
||||
"rollForward": false
|
||||
}
|
||||
}
|
||||
}
|
||||
427
packages/cultmath/tests/CultMath.Tests/BatchMathTests.cs
Normal file
427
packages/cultmath/tests/CultMath.Tests/BatchMathTests.cs
Normal file
|
|
@ -0,0 +1,427 @@
|
|||
using System;
|
||||
using CultMath;
|
||||
using Xunit;
|
||||
|
||||
namespace CultMath.Tests;
|
||||
|
||||
// Stryker's mtp baseline on hands/adopt-stryker found BatchMath undefended in three ways:
|
||||
// both existing tests used 2-3 particles while LaneCount >= 4, so the vector body never ran
|
||||
// and only the scalar tail executed; both held y at 0 for every particle, so every y-axis
|
||||
// mutation was invisible; and the guard branches (early return, negative deltaTime, length
|
||||
// mismatch) were never exercised. These tests read LaneCount at runtime, use
|
||||
// LaneCount * 3 + 1 particles (a non-empty vector body AND a non-empty remainder), give every
|
||||
// particle distinct nonzero x and y, and pin results against a scalar reference computed here
|
||||
// rather than by calling the code under test.
|
||||
public sealed class BatchMathTests
|
||||
{
|
||||
private const float Tolerance = 1e-4f;
|
||||
|
||||
[Fact]
|
||||
public void RadialFalloffMatchesScalarReferenceAcrossVectorBodyAndRemainder()
|
||||
{
|
||||
var lanes = BatchMath.LaneCount;
|
||||
var count = lanes * 3 + 1;
|
||||
Assert.True(count > lanes, "fixture must exceed one lane to exercise the vector body");
|
||||
Assert.True((count % lanes) != 0, "fixture must leave a non-empty scalar remainder");
|
||||
|
||||
var px = new float[count];
|
||||
var py = new float[count];
|
||||
var ax = new float[count];
|
||||
var ay = new float[count];
|
||||
// Distinct, asymmetric per-particle values: x and y follow different formulas so an
|
||||
// x/y swap mutation is observable, and no particle sits at the falloff center.
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
px[i] = i * 1.3f - 4.0f;
|
||||
py[i] = i * -0.7f + 2.5f;
|
||||
}
|
||||
|
||||
const float centerX = 3.0f;
|
||||
const float centerY = -1.0f;
|
||||
const float strength = 5.0f;
|
||||
// Chosen so some particles fall inside the radius and some fall outside it, exercising
|
||||
// both the ConditionalSelect(inRange, ...) vector path and the scalar "continue".
|
||||
const float radius = 6.0f;
|
||||
|
||||
// The scalar remainder is exactly one particle (index count-1). The generator above puts
|
||||
// it far outside the radius, which would leave any y-axis mutation in the scalar tail's
|
||||
// "in range" arithmetic unobserved (the only remainder particle never reaches it). Pin it
|
||||
// inside the radius with a nonzero, non-degenerate y offset instead.
|
||||
px[count - 1] = centerX + 2.0f;
|
||||
py[count - 1] = centerY + 1.5f;
|
||||
|
||||
BatchMath.AddRadialFalloffAcceleration2D(px, py, centerX, centerY, strength, radius, ax, ay);
|
||||
|
||||
var insideCount = 0;
|
||||
var outsideCount = 0;
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var dx = centerX - px[i];
|
||||
var dy = centerY - py[i];
|
||||
var distanceSquared = Math.Max(dx * dx + dy * dy, 1.0e-8f);
|
||||
var distance = MathF.Sqrt(distanceSquared);
|
||||
|
||||
if (distance > radius)
|
||||
{
|
||||
outsideCount++;
|
||||
Assert.Equal(0.0f, ax[i], Tolerance);
|
||||
Assert.Equal(0.0f, ay[i], Tolerance);
|
||||
continue;
|
||||
}
|
||||
|
||||
insideCount++;
|
||||
var falloff = Math.Max(0.0f, 1.0f - distance / radius);
|
||||
var magnitude = strength * falloff;
|
||||
var expectedAx = dx / distance * magnitude;
|
||||
var expectedAy = dy / distance * magnitude;
|
||||
Assert.Equal(expectedAx, ax[i], Tolerance);
|
||||
Assert.Equal(expectedAy, ay[i], Tolerance);
|
||||
}
|
||||
|
||||
Assert.True(insideCount > 0, "fixture must include particles inside the radius");
|
||||
Assert.True(outsideCount > 0, "fixture must include particles outside the radius");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RadialFalloffAccumulatesOntoExistingAcceleration()
|
||||
{
|
||||
// Guards against a mutation that assigns instead of accumulating: seed a nonzero
|
||||
// acceleration and confirm the falloff contribution is added to it, not overwritten.
|
||||
var px = new[] { 0.0f };
|
||||
var py = new[] { 0.0f };
|
||||
var ax = new[] { 10.0f };
|
||||
var ay = new[] { -20.0f };
|
||||
|
||||
BatchMath.AddRadialFalloffAcceleration2D(px, py, 1.0f, 0.0f, 4.0f, 8.0f, ax, ay);
|
||||
|
||||
var dx = 1.0f - 0.0f;
|
||||
var dy = 0.0f - 0.0f;
|
||||
var distance = MathF.Sqrt(dx * dx + dy * dy);
|
||||
var falloff = Math.Max(0.0f, 1.0f - distance / 8.0f);
|
||||
var magnitude = 4.0f * falloff;
|
||||
Assert.Equal(10.0f + dx / distance * magnitude, ax[0], Tolerance);
|
||||
Assert.Equal(-20.0f + dy / distance * magnitude, ay[0], Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RadialFalloffValidatesEqualSpanLengths()
|
||||
{
|
||||
var px = new float[3];
|
||||
var py = new float[2];
|
||||
var ax = new float[3];
|
||||
var ay = new float[3];
|
||||
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
BatchMath.AddRadialFalloffAcceleration2D(px, py, 0.0f, 0.0f, 1.0f, 1.0f, ax, ay));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RadialFalloffIsNoOpWhenRadiusIsNonPositive()
|
||||
{
|
||||
var px = new[] { 1.0f, 2.0f };
|
||||
var py = new[] { 3.0f, -4.0f };
|
||||
var ax = new[] { 5.0f, 6.0f };
|
||||
var ay = new[] { 7.0f, 8.0f };
|
||||
|
||||
BatchMath.AddRadialFalloffAcceleration2D(px, py, 0.0f, 0.0f, 9.0f, 0.0f, ax, ay);
|
||||
|
||||
Assert.Equal(new[] { 5.0f, 6.0f }, ax);
|
||||
Assert.Equal(new[] { 7.0f, 8.0f }, ay);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RadialFalloffIsNoOpWhenStrengthIsZero()
|
||||
{
|
||||
var px = new[] { 1.0f, 2.0f };
|
||||
var py = new[] { 3.0f, -4.0f };
|
||||
var ax = new[] { 5.0f, 6.0f };
|
||||
var ay = new[] { 7.0f, 8.0f };
|
||||
|
||||
BatchMath.AddRadialFalloffAcceleration2D(px, py, 0.0f, 0.0f, 0.0f, 10.0f, ax, ay);
|
||||
|
||||
Assert.Equal(new[] { 5.0f, 6.0f }, ax);
|
||||
Assert.Equal(new[] { 7.0f, 8.0f }, ay);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RadialFalloffIsNoOpWhenSpansAreEmpty()
|
||||
{
|
||||
var px = Array.Empty<float>();
|
||||
var py = Array.Empty<float>();
|
||||
var ax = Array.Empty<float>();
|
||||
var ay = Array.Empty<float>();
|
||||
|
||||
// Must not throw for a valid but degenerate zero-length batch.
|
||||
BatchMath.AddRadialFalloffAcceleration2D(px, py, 0.0f, 0.0f, 1.0f, 1.0f, ax, ay);
|
||||
|
||||
Assert.Empty(ax);
|
||||
Assert.Empty(ay);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EulerIntegrationMatchesScalarReferenceAcrossVectorBodyAndRemainder()
|
||||
{
|
||||
var lanes = BatchMath.LaneCount;
|
||||
var count = lanes * 3 + 1;
|
||||
Assert.True((count % lanes) != 0, "fixture must leave a non-empty scalar remainder");
|
||||
|
||||
const float deltaTime = 0.25f;
|
||||
var dynamicMask = new float[count];
|
||||
var px = new float[count];
|
||||
var py = new float[count];
|
||||
var vx = new float[count];
|
||||
var vy = new float[count];
|
||||
var ax = new float[count];
|
||||
var ay = new float[count];
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
// Every third particle is frozen (mask 0) so the mask branch is exercised across
|
||||
// both the vector body and the remainder, not only at the array's edges.
|
||||
dynamicMask[i] = i % 3 == 0 ? 0.0f : 1.0f;
|
||||
px[i] = i * 0.4f - 1.0f;
|
||||
py[i] = i * -0.6f + 3.0f;
|
||||
vx[i] = i * 0.2f + 1.0f;
|
||||
vy[i] = i * -0.3f - 2.0f;
|
||||
ax[i] = i * 0.5f - 2.5f;
|
||||
ay[i] = i * -0.9f + 1.5f;
|
||||
}
|
||||
|
||||
// The scalar remainder is exactly one particle (index count-1). The mask pattern above
|
||||
// would leave it frozen for lane counts divisible by 3, which masks every y-axis (and
|
||||
// x-axis) mutation in the scalar tail's arithmetic to zero regardless of the mutation.
|
||||
// Force it dynamic so the remainder actually exercises the scalar tail's math.
|
||||
dynamicMask[count - 1] = 1.0f;
|
||||
|
||||
// Independent copies for the scalar reference; the arrays under test are mutated in place.
|
||||
var expectedPx = (float[])px.Clone();
|
||||
var expectedPy = (float[])py.Clone();
|
||||
var expectedVx = (float[])vx.Clone();
|
||||
var expectedVy = (float[])vy.Clone();
|
||||
|
||||
BatchMath.IntegrateSemiImplicitEuler2D(deltaTime, dynamicMask, px, py, vx, vy, ax, ay);
|
||||
|
||||
var anyFrozen = false;
|
||||
var anyMoving = false;
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var mask = dynamicMask[i];
|
||||
expectedVx[i] += ax[i] * deltaTime * mask;
|
||||
expectedVy[i] += ay[i] * deltaTime * mask;
|
||||
expectedPx[i] += expectedVx[i] * deltaTime * mask;
|
||||
expectedPy[i] += expectedVy[i] * deltaTime * mask;
|
||||
|
||||
if (mask == 0.0f) anyFrozen = true; else anyMoving = true;
|
||||
|
||||
Assert.Equal(expectedVx[i], vx[i], Tolerance);
|
||||
Assert.Equal(expectedVy[i], vy[i], Tolerance);
|
||||
Assert.Equal(expectedPx[i], px[i], Tolerance);
|
||||
Assert.Equal(expectedPy[i], py[i], Tolerance);
|
||||
}
|
||||
|
||||
Assert.True(anyFrozen, "fixture must include masked-off (frozen) particles");
|
||||
Assert.True(anyMoving, "fixture must include dynamic particles");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EulerIntegrationRejectsNonPositiveDeltaTime()
|
||||
{
|
||||
var dynamicMask = new[] { 1.0f };
|
||||
var px = new[] { 0.0f };
|
||||
var py = new[] { 0.0f };
|
||||
var vx = new[] { 0.0f };
|
||||
var vy = new[] { 0.0f };
|
||||
var ax = new[] { 0.0f };
|
||||
var ay = new[] { 0.0f };
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
||||
BatchMath.IntegrateSemiImplicitEuler2D(0.0f, dynamicMask, px, py, vx, vy, ax, ay));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
||||
BatchMath.IntegrateSemiImplicitEuler2D(-1.0f, dynamicMask, px, py, vx, vy, ax, ay));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EulerIntegrationValidatesEqualSpanLengths()
|
||||
{
|
||||
var dynamicMask = new float[3];
|
||||
var px = new float[3];
|
||||
var py = new float[3];
|
||||
var vx = new float[2];
|
||||
var vy = new float[3];
|
||||
var ax = new float[3];
|
||||
var ay = new float[3];
|
||||
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
BatchMath.IntegrateSemiImplicitEuler2D(0.5f, dynamicMask, px, py, vx, vy, ax, ay));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Float2RadialFalloffMatchesScalarReferenceWithNonzeroYAndOutOfRangeParticles()
|
||||
{
|
||||
var count = 5;
|
||||
var positions = new float2[count];
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
positions[i] = new float2(i * 1.1f - 2.0f, i * -0.8f + 1.0f);
|
||||
}
|
||||
|
||||
var center = new float2(1.5f, -0.5f);
|
||||
const float strength = 3.0f;
|
||||
const float radius = 3.0f;
|
||||
var acceleration = new float2[count];
|
||||
|
||||
BatchMath.AddRadialFalloffAcceleration2D(positions, center, strength, radius, acceleration);
|
||||
|
||||
var insideCount = 0;
|
||||
var outsideCount = 0;
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var delta = center - positions[i];
|
||||
var distanceSquared = Math.Max(delta.x * delta.x + delta.y * delta.y, 1.0e-8f);
|
||||
var distance = MathF.Sqrt(distanceSquared);
|
||||
|
||||
if (distance > radius)
|
||||
{
|
||||
outsideCount++;
|
||||
Assert.Equal(0.0f, acceleration[i].x, Tolerance);
|
||||
Assert.Equal(0.0f, acceleration[i].y, Tolerance);
|
||||
continue;
|
||||
}
|
||||
|
||||
insideCount++;
|
||||
var falloff = Math.Max(0.0f, 1.0f - distance / radius);
|
||||
var expected = delta / distance * (strength * falloff);
|
||||
Assert.Equal(expected.x, acceleration[i].x, Tolerance);
|
||||
Assert.Equal(expected.y, acceleration[i].y, Tolerance);
|
||||
}
|
||||
|
||||
Assert.True(insideCount > 0, "fixture must include particles inside the radius");
|
||||
Assert.True(outsideCount > 0, "fixture must include particles outside the radius");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Float2RadialFalloffAccumulatesOntoExistingAcceleration()
|
||||
{
|
||||
// Guards against a mutation that assigns instead of accumulating (BatchMath.cs:115):
|
||||
// seed a nonzero, per-element-distinct acceleration and confirm the falloff
|
||||
// contribution is added to it, not overwritten. An out-of-range particle must be left
|
||||
// exactly at its seed value, not reset to zero.
|
||||
var count = 5;
|
||||
var positions = new float2[count];
|
||||
var seedAcceleration = new float2[count];
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
positions[i] = new float2(i * 1.1f - 2.0f, i * -0.8f + 1.0f);
|
||||
seedAcceleration[i] = new float2(i * 2.3f + 4.0f, i * -1.7f - 6.0f);
|
||||
}
|
||||
|
||||
var center = new float2(1.5f, -0.5f);
|
||||
const float strength = 3.0f;
|
||||
const float radius = 3.0f;
|
||||
var acceleration = (float2[])seedAcceleration.Clone();
|
||||
|
||||
BatchMath.AddRadialFalloffAcceleration2D(positions, center, strength, radius, acceleration);
|
||||
|
||||
var insideCount = 0;
|
||||
var outsideCount = 0;
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var delta = center - positions[i];
|
||||
var distanceSquared = Math.Max(delta.x * delta.x + delta.y * delta.y, 1.0e-8f);
|
||||
var distance = MathF.Sqrt(distanceSquared);
|
||||
|
||||
if (distance > radius)
|
||||
{
|
||||
outsideCount++;
|
||||
Assert.Equal(seedAcceleration[i].x, acceleration[i].x, Tolerance);
|
||||
Assert.Equal(seedAcceleration[i].y, acceleration[i].y, Tolerance);
|
||||
continue;
|
||||
}
|
||||
|
||||
insideCount++;
|
||||
var falloff = Math.Max(0.0f, 1.0f - distance / radius);
|
||||
var contribution = delta / distance * (strength * falloff);
|
||||
Assert.Equal(seedAcceleration[i].x + contribution.x, acceleration[i].x, Tolerance);
|
||||
Assert.Equal(seedAcceleration[i].y + contribution.y, acceleration[i].y, Tolerance);
|
||||
}
|
||||
|
||||
Assert.True(insideCount > 0, "fixture must include particles inside the radius");
|
||||
Assert.True(outsideCount > 0, "fixture must include particles outside the radius");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Float2EulerIntegrationMatchesScalarReferenceWithNonzeroYAndMask()
|
||||
{
|
||||
const float deltaTime = 0.2f;
|
||||
var dynamicMask = new[] { 1.0f, 0.0f, 1.0f };
|
||||
var position = new[] { new float2(0.0f, 0.0f), new float2(1.0f, 1.0f), new float2(-2.0f, 3.0f) };
|
||||
var velocity = new[] { new float2(1.0f, -1.0f), new float2(2.0f, 2.0f), new float2(-1.0f, 0.5f) };
|
||||
var acceleration = new[] { new float2(0.5f, 1.5f), new float2(3.0f, -3.0f), new float2(2.0f, -0.5f) };
|
||||
|
||||
var expectedPosition = (float2[])position.Clone();
|
||||
var expectedVelocity = (float2[])velocity.Clone();
|
||||
|
||||
BatchMath.IntegrateSemiImplicitEuler2D(deltaTime, dynamicMask, position, velocity, acceleration);
|
||||
|
||||
for (var i = 0; i < position.Length; i++)
|
||||
{
|
||||
var mask = dynamicMask[i];
|
||||
expectedVelocity[i] += acceleration[i] * deltaTime * mask;
|
||||
expectedPosition[i] += expectedVelocity[i] * deltaTime * mask;
|
||||
|
||||
Assert.Equal(expectedVelocity[i].x, velocity[i].x, Tolerance);
|
||||
Assert.Equal(expectedVelocity[i].y, velocity[i].y, Tolerance);
|
||||
Assert.Equal(expectedPosition[i].x, position[i].x, Tolerance);
|
||||
Assert.Equal(expectedPosition[i].y, position[i].y, Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Float2EulerIntegrationRejectsNonPositiveDeltaTime()
|
||||
{
|
||||
var dynamicMask = new[] { 1.0f };
|
||||
var position = new[] { float2.zero };
|
||||
var velocity = new[] { float2.zero };
|
||||
var acceleration = new[] { float2.zero };
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
||||
BatchMath.IntegrateSemiImplicitEuler2D(0.0f, dynamicMask, position, velocity, acceleration));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Float2EulerIntegrationValidatesEqualSpanLengths()
|
||||
{
|
||||
var dynamicMask = new float[3];
|
||||
var position = new float2[3];
|
||||
var velocity = new float2[2];
|
||||
var acceleration = new float2[3];
|
||||
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
BatchMath.IntegrateSemiImplicitEuler2D(0.5f, dynamicMask, position, velocity, acceleration));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Float2RadialFalloffValidatesEqualSpanLengths()
|
||||
{
|
||||
var positions = new float2[3];
|
||||
var acceleration = new float2[2];
|
||||
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
BatchMath.AddRadialFalloffAcceleration2D(positions, float2.zero, 1.0f, 1.0f, acceleration));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Float2RadialFalloffIsNoOpWhenRadiusIsNonPositiveOrStrengthIsZero()
|
||||
{
|
||||
var positions = new[] { new float2(1.0f, 3.0f), new float2(2.0f, -4.0f) };
|
||||
var acceleration = new[] { new float2(5.0f, 7.0f), new float2(6.0f, 8.0f) };
|
||||
|
||||
BatchMath.AddRadialFalloffAcceleration2D(positions, float2.zero, 9.0f, 0.0f, acceleration);
|
||||
Assert.Equal(new float2(5.0f, 7.0f), acceleration[0]);
|
||||
Assert.Equal(new float2(6.0f, 8.0f), acceleration[1]);
|
||||
|
||||
BatchMath.AddRadialFalloffAcceleration2D(positions, float2.zero, 0.0f, 10.0f, acceleration);
|
||||
Assert.Equal(new float2(5.0f, 7.0f), acceleration[0]);
|
||||
Assert.Equal(new float2(6.0f, 8.0f), acceleration[1]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
using CultMath;
|
||||
using Xunit;
|
||||
|
||||
namespace CultMath.Tests;
|
||||
|
||||
// math.first_order_intercept_time / first_order_intercept had one happy-path test before this
|
||||
// (MathTests.FirstOrderInterceptSolvesSimpleClosingTarget). Stryker's baseline on
|
||||
// hands/adopt-stryker found L264 (zero relative velocity), L270 (the a-approx-zero linear
|
||||
// fallback), L280/L283 (the quadratic determinant and its roots) and L285 (the t1>0 branch)
|
||||
// all undefended by fixtures that never varied relative velocity, never went linear, never
|
||||
// lacked a solution, and never used off-axis/off-origin shooter and target state.
|
||||
//
|
||||
// For the closing cases (a genuine positive root exists), the assertion is behavioural: the
|
||||
// returned aim point, fired at shotSpeed from a moving shooter, and the target's own motion
|
||||
// must put the shot and the target at the same place at the returned time. That is verified by
|
||||
// computing both positions independently in the test from math.first_order_intercept's own
|
||||
// contract (targetPosition + time * targetRelativeVelocity is the aim point; the shot's actual
|
||||
// world position folds in the shooter's velocity), not by re-deriving a second copy of the
|
||||
// production formula.
|
||||
//
|
||||
// For the no-solution branches, there is no physical coincidence to check (the fallback is not
|
||||
// a root of the intercept equation), so the assertion instead pins the contract read directly
|
||||
// from the source at math.cs:264-298: velocitySquared < 0.001 => 0; determinant < 0 => 0;
|
||||
// determinant > 0 with both roots <= 0 => 0 (via max(t2, 0)).
|
||||
public sealed class FirstOrderInterceptTests
|
||||
{
|
||||
private const int Precision = 4;
|
||||
|
||||
[Fact]
|
||||
public void ZeroRelativeVelocityReturnsZeroTime()
|
||||
{
|
||||
// Target and shooter share a velocity: targetRelativeVelocity is exactly zero, well
|
||||
// under the 0.001 lengthsq guard, regardless of either party's absolute speed.
|
||||
var time = math.first_order_intercept_time(
|
||||
shotSpeed: 50.0f,
|
||||
targetRelativePosition: new float3(12.0f, -7.0f, 3.0f),
|
||||
targetRelativeVelocity: float3.zero);
|
||||
|
||||
Assert.Equal(0.0f, time);
|
||||
|
||||
var aimPoint = math.first_order_intercept(
|
||||
shooterPosition: new float3(1.0f, 2.0f, 3.0f),
|
||||
shooterVelocity: new float3(4.0f, 4.0f, 4.0f),
|
||||
shotSpeed: 50.0f,
|
||||
targetPosition: new float3(13.0f, -5.0f, 6.0f),
|
||||
targetVelocity: new float3(4.0f, 4.0f, 4.0f));
|
||||
|
||||
// Contract: with time 0, the aim point is exactly the target's current position.
|
||||
Assert.Equal(13.0f, aimPoint.x, Precision);
|
||||
Assert.Equal(-5.0f, aimPoint.y, Precision);
|
||||
Assert.Equal(6.0f, aimPoint.z, Precision);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LinearFallbackWhenRelativeSpeedMatchesShotSpeedProducesPhysicalIntercept()
|
||||
{
|
||||
// velocitySquared == shotSpeed*shotSpeed makes a == 0 (|a| < 0.001), forcing the
|
||||
// linear branch: time = -lengthsq(pos) / (2 * dot(vel, pos)), clamped to >= 0.
|
||||
var shooterPosition = new float3(2.0f, -3.0f, 1.0f);
|
||||
var shooterVelocity = new float3(1.0f, 0.5f, -0.5f);
|
||||
var targetRelativePosition = new float3(10.0f, 0.0f, 0.0f);
|
||||
var targetRelativeVelocity = new float3(-2.0f, 0.0f, 0.0f);
|
||||
const float shotSpeed = 2.0f; // |targetRelativeVelocity| == 2 == shotSpeed => a == 0
|
||||
|
||||
var time = math.first_order_intercept_time(shotSpeed, targetRelativePosition, targetRelativeVelocity);
|
||||
|
||||
// -100 / (2 * -20) = 2.5, computed independently of the production formula's code path.
|
||||
Assert.Equal(2.5f, time, Precision);
|
||||
|
||||
var targetPosition = shooterPosition + targetRelativePosition;
|
||||
var targetVelocity = shooterVelocity + targetRelativeVelocity;
|
||||
|
||||
var aimPoint = math.first_order_intercept(shooterPosition, shooterVelocity, shotSpeed, targetPosition, targetVelocity);
|
||||
AssertShotAndTargetCoincideAtTime(shooterPosition, shooterVelocity, shotSpeed, aimPoint, targetPosition, targetVelocity, time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NegativeDeterminantReturnsZeroWhenTargetOutrunsTheProjectile()
|
||||
{
|
||||
// Target moving fast and mostly sideways, with a slow shot, so no real root of the
|
||||
// intercept quadratic exists. b = 2*dot(vel, pos) is deliberately nonzero (-40): if it
|
||||
// were 0, a mutant that drops the "determinant < 0" branch entirely and falls through to
|
||||
// `max(-b / (2a), 0)` would coincidentally also compute 0 and this test would not
|
||||
// distinguish the two.
|
||||
var time = math.first_order_intercept_time(
|
||||
shotSpeed: 1.0f,
|
||||
targetRelativePosition: new float3(10.0f, 0.0f, 0.0f),
|
||||
targetRelativeVelocity: new float3(-2.0f, 30.0f, 0.0f));
|
||||
|
||||
Assert.Equal(0.0f, time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BothQuadraticRootsPositiveReturnsTheEarlierOne()
|
||||
{
|
||||
// velocitySquared > shotSpeed*shotSpeed (a > 0): the target is relatively faster than
|
||||
// the shot, but closing almost head-on, so both quadratic roots are real and positive
|
||||
// (lengthsq(pos) >= 0 forces same-signed roots whenever a > 0). This is the only shape
|
||||
// that reaches the `t1 > 0` branch's `t2 > 0 ? min(t1, t2) : t1` line at all; every other
|
||||
// fixture in this file keeps t1 <= 0.
|
||||
var shooterPosition = new float3(4.0f, -2.0f, 1.0f);
|
||||
var shooterVelocity = new float3(0.5f, -0.5f, 0.0f);
|
||||
var shotSpeed = 5.0f;
|
||||
var targetRelativePosition = new float3(10.0f, 0.0f, 0.0f);
|
||||
var targetRelativeVelocity = new float3(-20.0f, 0.0f, 0.0f);
|
||||
|
||||
var time = math.first_order_intercept_time(shotSpeed, targetRelativePosition, targetRelativeVelocity);
|
||||
|
||||
// a = 400 - 25 = 375, b = -400, c = 100, determinant = 10000, root = 100:
|
||||
// t1 = (400 + 100) / 750 = 0.6667, t2 = (400 - 100) / 750 = 0.4. Both positive; the
|
||||
// earlier one (0.4) is the physically correct intercept time.
|
||||
Assert.Equal(0.4f, time, Precision);
|
||||
|
||||
var targetPosition = shooterPosition + targetRelativePosition;
|
||||
var targetVelocity = shooterVelocity + targetRelativeVelocity;
|
||||
var aimPoint = math.first_order_intercept(shooterPosition, shooterVelocity, shotSpeed, targetPosition, targetVelocity);
|
||||
AssertShotAndTargetCoincideAtTime(shooterPosition, shooterVelocity, shotSpeed, aimPoint, targetPosition, targetVelocity, time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RecedingTargetWithBothRootsNonPositiveReturnsZero()
|
||||
{
|
||||
// Target moving directly away from the shooter, faster than a slow shot: a real
|
||||
// determinant exists but both quadratic roots are negative, so t1 > 0 is false and the
|
||||
// function falls through to max(t2, 0) == 0.
|
||||
var time = math.first_order_intercept_time(
|
||||
shotSpeed: 1.0f,
|
||||
targetRelativePosition: new float3(10.0f, 0.0f, 0.0f),
|
||||
targetRelativeVelocity: new float3(5.0f, 0.0f, 0.0f));
|
||||
|
||||
Assert.Equal(0.0f, time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClosingCaseWithOffOriginOffAxisPositionsProducesPhysicallyConsistentIntercept()
|
||||
{
|
||||
// Neither shooter nor target sits at the origin, no vector is axis-aligned, and both
|
||||
// parties are moving. This is the fixture shape ("degenerate fixtures are how these
|
||||
// survived") the earlier single test never exercised.
|
||||
var shooterPosition = new float3(2.0f, 3.0f, -1.0f);
|
||||
var shooterVelocity = new float3(1.0f, -1.0f, 0.5f);
|
||||
var shotSpeed = 10.0f;
|
||||
var targetPosition = new float3(15.0f, 8.0f, 4.0f);
|
||||
var targetVelocity = new float3(-3.0f, 2.0f, 1.0f);
|
||||
|
||||
var targetRelativePosition = targetPosition - shooterPosition;
|
||||
var targetRelativeVelocity = targetVelocity - shooterVelocity;
|
||||
var time = math.first_order_intercept_time(shotSpeed, targetRelativePosition, targetRelativeVelocity);
|
||||
|
||||
Assert.True(time > 0.0f, "fixture must produce a genuine closing solution");
|
||||
|
||||
var aimPoint = math.first_order_intercept(shooterPosition, shooterVelocity, shotSpeed, targetPosition, targetVelocity);
|
||||
AssertShotAndTargetCoincideAtTime(shooterPosition, shooterVelocity, shotSpeed, aimPoint, targetPosition, targetVelocity, time);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fires from <paramref name="shooterPosition"/>, inheriting <paramref name="shooterVelocity"/>
|
||||
/// and adding <paramref name="shotSpeed"/> toward <paramref name="aimPoint"/>, and asserts the
|
||||
/// shot's world position at <paramref name="time"/> coincides with the target's own actual
|
||||
/// motion (<paramref name="targetPosition"/> + time * <paramref name="targetVelocity"/>).
|
||||
/// This is the behavioural contract first_order_intercept exists to serve: aim here, and the
|
||||
/// target will be there when the shot arrives.
|
||||
/// </summary>
|
||||
private static void AssertShotAndTargetCoincideAtTime(
|
||||
float3 shooterPosition,
|
||||
float3 shooterVelocity,
|
||||
float shotSpeed,
|
||||
float3 aimPoint,
|
||||
float3 targetPosition,
|
||||
float3 targetVelocity,
|
||||
float time)
|
||||
{
|
||||
var direction = math.normalize(aimPoint - shooterPosition);
|
||||
var shotVelocity = shooterVelocity + shotSpeed * direction;
|
||||
var shotPositionAtTime = shooterPosition + time * shotVelocity;
|
||||
var targetPositionAtTime = targetPosition + time * targetVelocity;
|
||||
|
||||
Assert.Equal(targetPositionAtTime.x, shotPositionAtTime.x, Precision);
|
||||
Assert.Equal(targetPositionAtTime.y, shotPositionAtTime.y, Precision);
|
||||
Assert.Equal(targetPositionAtTime.z, shotPositionAtTime.z, Precision);
|
||||
}
|
||||
}
|
||||
583
packages/cultmath/tests/CultMath.Tests/VectorEqualityTests.cs
Normal file
583
packages/cultmath/tests/CultMath.Tests/VectorEqualityTests.cs
Normal file
|
|
@ -0,0 +1,583 @@
|
|||
using System;
|
||||
using CultMath;
|
||||
using Xunit;
|
||||
|
||||
namespace CultMath.Tests;
|
||||
|
||||
// Hand-verified on hands/adopt-stryker: weakening float3.Equals from `&&` to `||` passes the
|
||||
// whole existing suite. The same shape of Equals/GetHashCode, ==/!=, and </>/<=/>= exists (with
|
||||
// no shared base type) across float2/3/4, double2/3, int2/3/4, bool2/3/4, quaternion,
|
||||
// float2x2 and float3x3. These tests are data-driven per component position rather than one
|
||||
// method per type: each type supplies a base value and, per component index, a value that
|
||||
// differs at exactly that index and agrees everywhere else, plus (for the four ordered numeric
|
||||
// families) a left/right pair whose relational result differs per component. bool2/3/4 and
|
||||
// quaternion have no ordering operators and are not included in the comparison-operator cases.
|
||||
// float2x2/float3x3 have no == / != operators (Equals and GetHashCode only) and are covered
|
||||
// separately, at row granularity.
|
||||
public sealed class VectorEqualityTests
|
||||
{
|
||||
// ---- shared, type-agnostic assertions -------------------------------------------------
|
||||
|
||||
private static void AssertEqualsAndHashCodeContract<T>(T a, T b, bool expectedEqual, string label)
|
||||
where T : IEquatable<T>
|
||||
{
|
||||
Assert.True(a.Equals(b) == expectedEqual, $"{label}: a.Equals(b) expected {expectedEqual}");
|
||||
Assert.True(b.Equals(a) == expectedEqual, $"{label}: b.Equals(a) expected {expectedEqual} (symmetry)");
|
||||
if (expectedEqual)
|
||||
{
|
||||
Assert.True(a.GetHashCode() == b.GetHashCode(), $"{label}: equal values must share a hash code");
|
||||
}
|
||||
}
|
||||
|
||||
private static void AssertElementwiseEqualityOperators<TLeft, TRight, TBool>(
|
||||
TLeft a,
|
||||
TRight b,
|
||||
int count,
|
||||
Func<int, bool> expectedEqualAt,
|
||||
Func<TLeft, TRight, TBool> eq,
|
||||
Func<TLeft, TRight, TBool> neq,
|
||||
Func<TBool, int, bool> at,
|
||||
string label)
|
||||
{
|
||||
var eqResult = eq(a, b);
|
||||
var neqResult = neq(a, b);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var expectedEqual = expectedEqualAt(i);
|
||||
Assert.True(at(eqResult, i) == expectedEqual, $"{label}: == component {i} expected {expectedEqual}");
|
||||
Assert.True(at(neqResult, i) == !expectedEqual, $"{label}: != component {i} expected {!expectedEqual}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void AssertElementwiseComparisonOperators<TLeft, TRight, TBool>(
|
||||
TLeft left,
|
||||
TRight right,
|
||||
double[] leftVals,
|
||||
double[] rightVals,
|
||||
Func<TLeft, TRight, TBool> lt,
|
||||
Func<TLeft, TRight, TBool> gt,
|
||||
Func<TLeft, TRight, TBool> le,
|
||||
Func<TLeft, TRight, TBool> ge,
|
||||
Func<TBool, int, bool> at,
|
||||
string label)
|
||||
{
|
||||
var ltResult = lt(left, right);
|
||||
var gtResult = gt(left, right);
|
||||
var leResult = le(left, right);
|
||||
var geResult = ge(left, right);
|
||||
for (var i = 0; i < leftVals.Length; i++)
|
||||
{
|
||||
var less = leftVals[i] < rightVals[i];
|
||||
var greater = leftVals[i] > rightVals[i];
|
||||
Assert.True(at(ltResult, i) == less, $"{label}: < component {i} (left={leftVals[i]}, right={rightVals[i]})");
|
||||
Assert.True(at(gtResult, i) == greater, $"{label}: > component {i} (left={leftVals[i]}, right={rightVals[i]})");
|
||||
Assert.True(at(leResult, i) == !greater, $"{label}: <= component {i} (left={leftVals[i]}, right={rightVals[i]})");
|
||||
Assert.True(at(geResult, i) == !less, $"{label}: >= component {i} (left={leftVals[i]}, right={rightVals[i]})");
|
||||
}
|
||||
}
|
||||
|
||||
// ---- float2 -----------------------------------------------------------------------------
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(1)]
|
||||
public void Float2EqualsHashCodeAndOperatorsDetectSingleComponentDifference(int index)
|
||||
{
|
||||
var a = new float2(3.0f, -7.0f);
|
||||
var b = a;
|
||||
b[index] += 5.0f;
|
||||
|
||||
AssertEqualsAndHashCodeContract(a, a, true, "float2 self");
|
||||
AssertEqualsAndHashCodeContract(a, b, false, $"float2 differing at {index}");
|
||||
AssertElementwiseEqualityOperators<float2, float2, bool2>(
|
||||
a, b, 2, i => i != index, (x, y) => x == y, (x, y) => x != y, (v, i) => v[i], "float2 vector==vector");
|
||||
|
||||
// Scalar overloads: a scalar equal to exactly one component, on both operand sides.
|
||||
var scalar = a[index];
|
||||
AssertElementwiseEqualityOperators<float2, float, bool2>(
|
||||
a, scalar, 2, i => a[i] == scalar, (x, y) => x == y, (x, y) => x != y, (v, i) => v[i], "float2 vector==scalar");
|
||||
AssertElementwiseEqualityOperators<float, float2, bool2>(
|
||||
scalar, a, 2, i => a[i] == scalar, (x, y) => x == y, (x, y) => x != y, (v, i) => v[i], "float2 scalar==vector");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Float2ComparisonOperatorsDifferPerComponent()
|
||||
{
|
||||
var left = new float2(1.0f, 5.0f);
|
||||
var right = new float2(2.0f, 3.0f);
|
||||
var leftVals = new double[] { 1.0, 5.0 };
|
||||
var rightVals = new double[] { 2.0, 3.0 };
|
||||
|
||||
AssertElementwiseComparisonOperators<float2, float2, bool2>(
|
||||
left, right, leftVals, rightVals,
|
||||
(x, y) => x < y, (x, y) => x > y, (x, y) => x <= y, (x, y) => x >= y, (v, i) => v[i], "float2 vector-vector");
|
||||
|
||||
const float scalar = 2.0f;
|
||||
AssertElementwiseComparisonOperators<float2, float, bool2>(
|
||||
left, scalar, leftVals, new double[] { scalar, scalar },
|
||||
(x, y) => x < y, (x, y) => x > y, (x, y) => x <= y, (x, y) => x >= y, (v, i) => v[i], "float2 vector-scalar");
|
||||
AssertElementwiseComparisonOperators<float, float2, bool2>(
|
||||
scalar, left, new double[] { scalar, scalar }, leftVals,
|
||||
(x, y) => x < y, (x, y) => x > y, (x, y) => x <= y, (x, y) => x >= y, (v, i) => v[i], "float2 scalar-vector");
|
||||
|
||||
// A value compared against an exact copy of itself: every component ties. This is the
|
||||
// complementary fixture to the no-tie one above. A tie is where < and <= (or > and >=)
|
||||
// actually disagree, so it catches a strict/non-strict boundary swap (e.g. < mutated to
|
||||
// <=) that a fixture with no equal components cannot: unlike a threshold compared against
|
||||
// an arbitrary float, an exact self-tie is neither rare nor equivalent here, since these
|
||||
// are general-purpose comparison operators (bounds/containment checks compare against
|
||||
// literal endpoints routinely, as CultMath's own touching-edges rect test does).
|
||||
AssertElementwiseComparisonOperators<float2, float2, bool2>(
|
||||
left, left, leftVals, leftVals,
|
||||
(x, y) => x < y, (x, y) => x > y, (x, y) => x <= y, (x, y) => x >= y, (v, i) => v[i], "float2 tied vector-vector");
|
||||
|
||||
// The scalar overloads need their own tie: a uniform vector against a scalar equal to
|
||||
// every component, so every component of the vector-scalar and scalar-vector overloads
|
||||
// ties too (the vector-vector tie above does not cover them).
|
||||
var uniform = new float2(4.0f, 4.0f);
|
||||
var uniformVals = new double[] { 4.0, 4.0 };
|
||||
AssertElementwiseComparisonOperators<float2, float, bool2>(
|
||||
uniform, 4.0f, uniformVals, uniformVals,
|
||||
(x, y) => x < y, (x, y) => x > y, (x, y) => x <= y, (x, y) => x >= y, (v, i) => v[i], "float2 tied vector-scalar");
|
||||
AssertElementwiseComparisonOperators<float, float2, bool2>(
|
||||
4.0f, uniform, uniformVals, uniformVals,
|
||||
(x, y) => x < y, (x, y) => x > y, (x, y) => x <= y, (x, y) => x >= y, (v, i) => v[i], "float2 tied scalar-vector");
|
||||
}
|
||||
|
||||
// ---- float3 -----------------------------------------------------------------------------
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(1)]
|
||||
[InlineData(2)]
|
||||
public void Float3EqualsHashCodeAndOperatorsDetectSingleComponentDifference(int index)
|
||||
{
|
||||
var a = new float3(3.0f, -7.0f, 4.5f);
|
||||
var b = a;
|
||||
b[index] += 5.0f;
|
||||
|
||||
AssertEqualsAndHashCodeContract(a, a, true, "float3 self");
|
||||
AssertEqualsAndHashCodeContract(a, b, false, $"float3 differing at {index}");
|
||||
AssertElementwiseEqualityOperators<float3, float3, bool3>(
|
||||
a, b, 3, i => i != index, (x, y) => x == y, (x, y) => x != y, (v, i) => v[i], "float3 vector==vector");
|
||||
|
||||
var scalar = a[index];
|
||||
AssertElementwiseEqualityOperators<float3, float, bool3>(
|
||||
a, scalar, 3, i => a[i] == scalar, (x, y) => x == y, (x, y) => x != y, (v, i) => v[i], "float3 vector==scalar");
|
||||
AssertElementwiseEqualityOperators<float, float3, bool3>(
|
||||
scalar, a, 3, i => a[i] == scalar, (x, y) => x == y, (x, y) => x != y, (v, i) => v[i], "float3 scalar==vector");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Float3ComparisonOperatorsDifferPerComponent()
|
||||
{
|
||||
// No component is tied: a tie makes strict < and strict > agree (both false), which
|
||||
// would hide a direction-flip mutation (< swapped for >) at that component.
|
||||
var left = new float3(1.0f, 5.0f, 7.0f);
|
||||
var right = new float3(2.0f, 3.0f, 4.0f);
|
||||
var leftVals = new double[] { 1.0, 5.0, 7.0 };
|
||||
var rightVals = new double[] { 2.0, 3.0, 4.0 };
|
||||
|
||||
AssertElementwiseComparisonOperators<float3, float3, bool3>(
|
||||
left, right, leftVals, rightVals,
|
||||
(x, y) => x < y, (x, y) => x > y, (x, y) => x <= y, (x, y) => x >= y, (v, i) => v[i], "float3 vector-vector");
|
||||
|
||||
const float scalar = 4.0f;
|
||||
AssertElementwiseComparisonOperators<float3, float, bool3>(
|
||||
left, scalar, leftVals, new double[] { scalar, scalar, scalar },
|
||||
(x, y) => x < y, (x, y) => x > y, (x, y) => x <= y, (x, y) => x >= y, (v, i) => v[i], "float3 vector-scalar");
|
||||
AssertElementwiseComparisonOperators<float, float3, bool3>(
|
||||
scalar, left, new double[] { scalar, scalar, scalar }, leftVals,
|
||||
(x, y) => x < y, (x, y) => x > y, (x, y) => x <= y, (x, y) => x >= y, (v, i) => v[i], "float3 scalar-vector");
|
||||
|
||||
// Tied against itself (see the float2 tied-fixture comment above).
|
||||
AssertElementwiseComparisonOperators<float3, float3, bool3>(
|
||||
left, left, leftVals, leftVals,
|
||||
(x, y) => x < y, (x, y) => x > y, (x, y) => x <= y, (x, y) => x >= y, (v, i) => v[i], "float3 tied vector-vector");
|
||||
|
||||
// The scalar overloads need their own tie (see the float2 uniform-vector comment above).
|
||||
var uniform = new float3(4.0f, 4.0f, 4.0f);
|
||||
var uniformVals = new double[] { 4.0, 4.0, 4.0 };
|
||||
AssertElementwiseComparisonOperators<float3, float, bool3>(
|
||||
uniform, 4.0f, uniformVals, uniformVals,
|
||||
(x, y) => x < y, (x, y) => x > y, (x, y) => x <= y, (x, y) => x >= y, (v, i) => v[i], "float3 tied vector-scalar");
|
||||
AssertElementwiseComparisonOperators<float, float3, bool3>(
|
||||
4.0f, uniform, uniformVals, uniformVals,
|
||||
(x, y) => x < y, (x, y) => x > y, (x, y) => x <= y, (x, y) => x >= y, (v, i) => v[i], "float3 tied scalar-vector");
|
||||
}
|
||||
|
||||
// ---- float4 -----------------------------------------------------------------------------
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(1)]
|
||||
[InlineData(2)]
|
||||
[InlineData(3)]
|
||||
public void Float4EqualsHashCodeAndOperatorsDetectSingleComponentDifference(int index)
|
||||
{
|
||||
var a = new float4(3.0f, -7.0f, 4.5f, 9.0f);
|
||||
var b = a;
|
||||
b[index] += 5.0f;
|
||||
|
||||
AssertEqualsAndHashCodeContract(a, a, true, "float4 self");
|
||||
AssertEqualsAndHashCodeContract(a, b, false, $"float4 differing at {index}");
|
||||
AssertElementwiseEqualityOperators<float4, float4, bool4>(
|
||||
a, b, 4, i => i != index, (x, y) => x == y, (x, y) => x != y, (v, i) => v[i], "float4 vector==vector");
|
||||
|
||||
var scalar = a[index];
|
||||
AssertElementwiseEqualityOperators<float4, float, bool4>(
|
||||
a, scalar, 4, i => a[i] == scalar, (x, y) => x == y, (x, y) => x != y, (v, i) => v[i], "float4 vector==scalar");
|
||||
AssertElementwiseEqualityOperators<float, float4, bool4>(
|
||||
scalar, a, 4, i => a[i] == scalar, (x, y) => x == y, (x, y) => x != y, (v, i) => v[i], "float4 scalar==vector");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Float4ComparisonOperatorsDifferPerComponent()
|
||||
{
|
||||
// No component is tied (see the float3 comment above for why a tie hides a
|
||||
// direction-flip mutation at that component).
|
||||
var left = new float4(1.0f, 5.0f, 7.0f, 2.0f);
|
||||
var right = new float4(2.0f, 3.0f, 4.0f, 9.0f);
|
||||
var leftVals = new double[] { 1.0, 5.0, 7.0, 2.0 };
|
||||
var rightVals = new double[] { 2.0, 3.0, 4.0, 9.0 };
|
||||
|
||||
AssertElementwiseComparisonOperators<float4, float4, bool4>(
|
||||
left, right, leftVals, rightVals,
|
||||
(x, y) => x < y, (x, y) => x > y, (x, y) => x <= y, (x, y) => x >= y, (v, i) => v[i], "float4 vector-vector");
|
||||
|
||||
const float scalar = 4.0f;
|
||||
AssertElementwiseComparisonOperators<float4, float, bool4>(
|
||||
left, scalar, leftVals, new double[] { scalar, scalar, scalar, scalar },
|
||||
(x, y) => x < y, (x, y) => x > y, (x, y) => x <= y, (x, y) => x >= y, (v, i) => v[i], "float4 vector-scalar");
|
||||
AssertElementwiseComparisonOperators<float, float4, bool4>(
|
||||
scalar, left, new double[] { scalar, scalar, scalar, scalar }, leftVals,
|
||||
(x, y) => x < y, (x, y) => x > y, (x, y) => x <= y, (x, y) => x >= y, (v, i) => v[i], "float4 scalar-vector");
|
||||
|
||||
// Tied against itself (see the float2 tied-fixture comment above).
|
||||
AssertElementwiseComparisonOperators<float4, float4, bool4>(
|
||||
left, left, leftVals, leftVals,
|
||||
(x, y) => x < y, (x, y) => x > y, (x, y) => x <= y, (x, y) => x >= y, (v, i) => v[i], "float4 tied vector-vector");
|
||||
|
||||
// The scalar overloads need their own tie (see the float2 uniform-vector comment above).
|
||||
var uniform = new float4(4.0f, 4.0f, 4.0f, 4.0f);
|
||||
var uniformVals = new double[] { 4.0, 4.0, 4.0, 4.0 };
|
||||
AssertElementwiseComparisonOperators<float4, float, bool4>(
|
||||
uniform, 4.0f, uniformVals, uniformVals,
|
||||
(x, y) => x < y, (x, y) => x > y, (x, y) => x <= y, (x, y) => x >= y, (v, i) => v[i], "float4 tied vector-scalar");
|
||||
AssertElementwiseComparisonOperators<float, float4, bool4>(
|
||||
4.0f, uniform, uniformVals, uniformVals,
|
||||
(x, y) => x < y, (x, y) => x > y, (x, y) => x <= y, (x, y) => x >= y, (v, i) => v[i], "float4 tied scalar-vector");
|
||||
}
|
||||
|
||||
// ---- double2 ----------------------------------------------------------------------------
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(1)]
|
||||
public void Double2EqualsHashCodeAndOperatorsDetectSingleComponentDifference(int index)
|
||||
{
|
||||
var a = new double2(3.0, -7.0);
|
||||
var b = a;
|
||||
b[index] += 5.0;
|
||||
|
||||
AssertEqualsAndHashCodeContract(a, a, true, "double2 self");
|
||||
AssertEqualsAndHashCodeContract(a, b, false, $"double2 differing at {index}");
|
||||
AssertElementwiseEqualityOperators<double2, double2, bool2>(
|
||||
a, b, 2, i => i != index, (x, y) => x == y, (x, y) => x != y, (v, i) => v[i], "double2 ==");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Double2ComparisonOperatorsDifferPerComponent()
|
||||
{
|
||||
var left = new double2(1.0, 5.0);
|
||||
var right = new double2(2.0, 3.0);
|
||||
var leftVals = new double[] { 1.0, 5.0 };
|
||||
var rightVals = new double[] { 2.0, 3.0 };
|
||||
|
||||
AssertElementwiseComparisonOperators<double2, double2, bool2>(
|
||||
left, right, leftVals, rightVals,
|
||||
(x, y) => x < y, (x, y) => x > y, (x, y) => x <= y, (x, y) => x >= y, (v, i) => v[i], "double2 vector-vector");
|
||||
|
||||
// Tied against itself (see the float2 tied-fixture comment above).
|
||||
AssertElementwiseComparisonOperators<double2, double2, bool2>(
|
||||
left, left, leftVals, leftVals,
|
||||
(x, y) => x < y, (x, y) => x > y, (x, y) => x <= y, (x, y) => x >= y, (v, i) => v[i], "double2 tied vector-vector");
|
||||
}
|
||||
|
||||
// ---- double3 ----------------------------------------------------------------------------
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(1)]
|
||||
[InlineData(2)]
|
||||
public void Double3EqualsHashCodeAndOperatorsDetectSingleComponentDifference(int index)
|
||||
{
|
||||
var a = new double3(3.0, -7.0, 4.5);
|
||||
var b = a;
|
||||
b[index] += 5.0;
|
||||
|
||||
AssertEqualsAndHashCodeContract(a, a, true, "double3 self");
|
||||
AssertEqualsAndHashCodeContract(a, b, false, $"double3 differing at {index}");
|
||||
AssertElementwiseEqualityOperators<double3, double3, bool3>(
|
||||
a, b, 3, i => i != index, (x, y) => x == y, (x, y) => x != y, (v, i) => v[i], "double3 ==");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Double3ComparisonOperatorsDifferPerComponent()
|
||||
{
|
||||
// No component is tied (see the float3 comment above).
|
||||
var left = new double3(1.0, 5.0, 7.0);
|
||||
var right = new double3(2.0, 3.0, 4.0);
|
||||
var leftVals = new double[] { 1.0, 5.0, 7.0 };
|
||||
var rightVals = new double[] { 2.0, 3.0, 4.0 };
|
||||
|
||||
AssertElementwiseComparisonOperators<double3, double3, bool3>(
|
||||
left, right, leftVals, rightVals,
|
||||
(x, y) => x < y, (x, y) => x > y, (x, y) => x <= y, (x, y) => x >= y, (v, i) => v[i], "double3 vector-vector");
|
||||
|
||||
// Tied against itself (see the float2 tied-fixture comment above).
|
||||
AssertElementwiseComparisonOperators<double3, double3, bool3>(
|
||||
left, left, leftVals, leftVals,
|
||||
(x, y) => x < y, (x, y) => x > y, (x, y) => x <= y, (x, y) => x >= y, (v, i) => v[i], "double3 tied vector-vector");
|
||||
}
|
||||
|
||||
// ---- int2 -------------------------------------------------------------------------------
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(1)]
|
||||
public void Int2EqualsHashCodeAndOperatorsDetectSingleComponentDifference(int index)
|
||||
{
|
||||
var a = new int2(3, -7);
|
||||
var b = a;
|
||||
b[index] += 5;
|
||||
|
||||
AssertEqualsAndHashCodeContract(a, a, true, "int2 self");
|
||||
AssertEqualsAndHashCodeContract(a, b, false, $"int2 differing at {index}");
|
||||
AssertElementwiseEqualityOperators<int2, int2, bool2>(
|
||||
a, b, 2, i => i != index, (x, y) => x == y, (x, y) => x != y, (v, i) => v[i], "int2 ==");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Int2ComparisonOperatorsDifferPerComponent()
|
||||
{
|
||||
var left = new int2(1, 5);
|
||||
var right = new int2(2, 3);
|
||||
var leftVals = new double[] { 1, 5 };
|
||||
var rightVals = new double[] { 2, 3 };
|
||||
|
||||
AssertElementwiseComparisonOperators<int2, int2, bool2>(
|
||||
left, right, leftVals, rightVals,
|
||||
(x, y) => x < y, (x, y) => x > y, (x, y) => x <= y, (x, y) => x >= y, (v, i) => v[i], "int2 vector-vector");
|
||||
|
||||
// Tied against itself (see the float2 tied-fixture comment above); integers make an
|
||||
// exact tie the routine case, not the rare one.
|
||||
AssertElementwiseComparisonOperators<int2, int2, bool2>(
|
||||
left, left, leftVals, leftVals,
|
||||
(x, y) => x < y, (x, y) => x > y, (x, y) => x <= y, (x, y) => x >= y, (v, i) => v[i], "int2 tied vector-vector");
|
||||
}
|
||||
|
||||
// ---- int3 -------------------------------------------------------------------------------
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(1)]
|
||||
[InlineData(2)]
|
||||
public void Int3EqualsHashCodeAndOperatorsDetectSingleComponentDifference(int index)
|
||||
{
|
||||
var a = new int3(3, -7, 4);
|
||||
var b = a;
|
||||
b[index] += 5;
|
||||
|
||||
AssertEqualsAndHashCodeContract(a, a, true, "int3 self");
|
||||
AssertEqualsAndHashCodeContract(a, b, false, $"int3 differing at {index}");
|
||||
AssertElementwiseEqualityOperators<int3, int3, bool3>(
|
||||
a, b, 3, i => i != index, (x, y) => x == y, (x, y) => x != y, (v, i) => v[i], "int3 ==");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Int3ComparisonOperatorsDifferPerComponent()
|
||||
{
|
||||
// No component is tied (see the float3 comment above).
|
||||
var left = new int3(1, 5, 7);
|
||||
var right = new int3(2, 3, 4);
|
||||
var leftVals = new double[] { 1, 5, 7 };
|
||||
var rightVals = new double[] { 2, 3, 4 };
|
||||
|
||||
AssertElementwiseComparisonOperators<int3, int3, bool3>(
|
||||
left, right, leftVals, rightVals,
|
||||
(x, y) => x < y, (x, y) => x > y, (x, y) => x <= y, (x, y) => x >= y, (v, i) => v[i], "int3 vector-vector");
|
||||
|
||||
// Tied against itself (see the int2 tied-fixture comment above).
|
||||
AssertElementwiseComparisonOperators<int3, int3, bool3>(
|
||||
left, left, leftVals, leftVals,
|
||||
(x, y) => x < y, (x, y) => x > y, (x, y) => x <= y, (x, y) => x >= y, (v, i) => v[i], "int3 tied vector-vector");
|
||||
}
|
||||
|
||||
// ---- int4 -------------------------------------------------------------------------------
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(1)]
|
||||
[InlineData(2)]
|
||||
[InlineData(3)]
|
||||
public void Int4EqualsHashCodeAndOperatorsDetectSingleComponentDifference(int index)
|
||||
{
|
||||
var a = new int4(3, -7, 4, 9);
|
||||
var b = a;
|
||||
b[index] += 5;
|
||||
|
||||
AssertEqualsAndHashCodeContract(a, a, true, "int4 self");
|
||||
AssertEqualsAndHashCodeContract(a, b, false, $"int4 differing at {index}");
|
||||
AssertElementwiseEqualityOperators<int4, int4, bool4>(
|
||||
a, b, 4, i => i != index, (x, y) => x == y, (x, y) => x != y, (v, i) => v[i], "int4 ==");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Int4ComparisonOperatorsDifferPerComponent()
|
||||
{
|
||||
// No component is tied (see the float3 comment above).
|
||||
var left = new int4(1, 5, 7, 2);
|
||||
var right = new int4(2, 3, 4, 9);
|
||||
var leftVals = new double[] { 1, 5, 7, 2 };
|
||||
var rightVals = new double[] { 2, 3, 4, 9 };
|
||||
|
||||
AssertElementwiseComparisonOperators<int4, int4, bool4>(
|
||||
left, right, leftVals, rightVals,
|
||||
(x, y) => x < y, (x, y) => x > y, (x, y) => x <= y, (x, y) => x >= y, (v, i) => v[i], "int4 vector-vector");
|
||||
|
||||
// Tied against itself (see the int2 tied-fixture comment above).
|
||||
AssertElementwiseComparisonOperators<int4, int4, bool4>(
|
||||
left, left, leftVals, leftVals,
|
||||
(x, y) => x < y, (x, y) => x > y, (x, y) => x <= y, (x, y) => x >= y, (v, i) => v[i], "int4 tied vector-vector");
|
||||
}
|
||||
|
||||
// ---- bool2/3/4 (no ordering operators) ---------------------------------------------------
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(1)]
|
||||
public void Bool2EqualsHashCodeAndOperatorsDetectSingleComponentDifference(int index)
|
||||
{
|
||||
var a = new bool2(true, false);
|
||||
var b = a;
|
||||
b[index] = !b[index];
|
||||
|
||||
AssertEqualsAndHashCodeContract(a, a, true, "bool2 self");
|
||||
AssertEqualsAndHashCodeContract(a, b, false, $"bool2 differing at {index}");
|
||||
AssertElementwiseEqualityOperators<bool2, bool2, bool2>(
|
||||
a, b, 2, i => i != index, (x, y) => x == y, (x, y) => x != y, (v, i) => v[i], "bool2 ==");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(1)]
|
||||
[InlineData(2)]
|
||||
public void Bool3EqualsHashCodeAndOperatorsDetectSingleComponentDifference(int index)
|
||||
{
|
||||
var a = new bool3(true, false, true);
|
||||
var b = a;
|
||||
b[index] = !b[index];
|
||||
|
||||
AssertEqualsAndHashCodeContract(a, a, true, "bool3 self");
|
||||
AssertEqualsAndHashCodeContract(a, b, false, $"bool3 differing at {index}");
|
||||
AssertElementwiseEqualityOperators<bool3, bool3, bool3>(
|
||||
a, b, 3, i => i != index, (x, y) => x == y, (x, y) => x != y, (v, i) => v[i], "bool3 ==");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(1)]
|
||||
[InlineData(2)]
|
||||
[InlineData(3)]
|
||||
public void Bool4EqualsHashCodeAndOperatorsDetectSingleComponentDifference(int index)
|
||||
{
|
||||
var a = new bool4(true, false, true, false);
|
||||
var b = a;
|
||||
b[index] = !b[index];
|
||||
|
||||
AssertEqualsAndHashCodeContract(a, a, true, "bool4 self");
|
||||
AssertEqualsAndHashCodeContract(a, b, false, $"bool4 differing at {index}");
|
||||
AssertElementwiseEqualityOperators<bool4, bool4, bool4>(
|
||||
a, b, 4, i => i != index, (x, y) => x == y, (x, y) => x != y, (v, i) => v[i], "bool4 ==");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BoolVectorFalseAndTrueConstantsHaveEveryComponentSet()
|
||||
{
|
||||
// bool2.@false/@true and bool4.@false/bool3.@true have no other consumer in this suite
|
||||
// (bool3.@false and bool4.@true are read by HlslSemanticsTests), so a wrong component in
|
||||
// their field initializers would otherwise go unnoticed.
|
||||
Assert.Equal(new bool2(false, false), bool2.@false);
|
||||
Assert.Equal(new bool2(true, true), bool2.@true);
|
||||
Assert.Equal(new bool3(false, false, false), bool3.@false);
|
||||
Assert.Equal(new bool3(true, true, true), bool3.@true);
|
||||
Assert.Equal(new bool4(false, false, false, false), bool4.@false);
|
||||
Assert.Equal(new bool4(true, true, true, true), bool4.@true);
|
||||
}
|
||||
|
||||
// ---- quaternion (== and != return a plain bool, not a component vector) -----------------
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(1)]
|
||||
[InlineData(2)]
|
||||
[InlineData(3)]
|
||||
public void QuaternionEqualsHashCodeAndOperatorsDetectSingleComponentDifference(int index)
|
||||
{
|
||||
var a = new quaternion(1.0f, 2.0f, 3.0f, 4.0f);
|
||||
float4 asFloat4 = a;
|
||||
asFloat4[index] += 5.0f;
|
||||
quaternion b = asFloat4;
|
||||
|
||||
AssertEqualsAndHashCodeContract(a, a, true, "quaternion self");
|
||||
AssertEqualsAndHashCodeContract(a, b, false, $"quaternion differing at {index}");
|
||||
|
||||
// quaternion's == / != are scalar bool, defined in terms of Equals: confirm they
|
||||
// actually delegate rather than reimplementing (and weakening) the comparison.
|
||||
var aCopy = new quaternion(a.x, a.y, a.z, a.w);
|
||||
Assert.Equal(a.Equals(aCopy), a == aCopy);
|
||||
Assert.Equal(!a.Equals(aCopy), a != aCopy);
|
||||
Assert.Equal(a.Equals(b), a == b);
|
||||
Assert.Equal(!a.Equals(b), a != b);
|
||||
}
|
||||
|
||||
// ---- float2x2 / float3x3 (Equals/GetHashCode only, row granularity) ---------------------
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(1)]
|
||||
public void Float2x2EqualsAndHashCodeDetectSingleRowDifference(int row)
|
||||
{
|
||||
var row0 = new float2(1.0f, 2.0f);
|
||||
var row1 = new float2(3.0f, 4.0f);
|
||||
var a = new float2x2(row0, row1);
|
||||
|
||||
var differingRow0 = row == 0 ? row0 + new float2(10.0f, 0.0f) : row0;
|
||||
var differingRow1 = row == 1 ? row1 + new float2(0.0f, 10.0f) : row1;
|
||||
var b = new float2x2(differingRow0, differingRow1);
|
||||
|
||||
AssertEqualsAndHashCodeContract(a, a, true, "float2x2 self");
|
||||
AssertEqualsAndHashCodeContract(a, b, false, $"float2x2 differing at row {row}");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(1)]
|
||||
[InlineData(2)]
|
||||
public void Float3x3EqualsAndHashCodeDetectSingleRowDifference(int row)
|
||||
{
|
||||
var row0 = new float3(1.0f, 2.0f, 3.0f);
|
||||
var row1 = new float3(4.0f, 5.0f, 6.0f);
|
||||
var row2 = new float3(7.0f, 8.0f, 9.0f);
|
||||
var a = new float3x3(row0, row1, row2);
|
||||
|
||||
var differingRow0 = row == 0 ? row0 + new float3(10.0f, 0.0f, 0.0f) : row0;
|
||||
var differingRow1 = row == 1 ? row1 + new float3(0.0f, 10.0f, 0.0f) : row1;
|
||||
var differingRow2 = row == 2 ? row2 + new float3(0.0f, 0.0f, 10.0f) : row2;
|
||||
var b = new float3x3(differingRow0, differingRow1, differingRow2);
|
||||
|
||||
AssertEqualsAndHashCodeContract(a, a, true, "float3x3 self");
|
||||
AssertEqualsAndHashCodeContract(a, b, false, $"float3x3 differing at row {row}");
|
||||
}
|
||||
}
|
||||
14
packages/cultmath/tests/CultMath.Tests/stryker-config.json
Normal file
14
packages/cultmath/tests/CultMath.Tests/stryker-config.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"stryker-config": {
|
||||
"project": "CultMath.csproj",
|
||||
"mutate": [
|
||||
"**/packages/cultmath/src/CultMath/**/*.cs",
|
||||
"!**/packages/cultmath/src/CultMath/Swizzles.g.cs",
|
||||
"!**/packages/cultmath/src/CultMath/*.meta"
|
||||
],
|
||||
"mutation-level": "Standard",
|
||||
"reporters": [ "progress", "html", "json" ],
|
||||
"thresholds": { "high": 80, "low": 60, "break": 0 },
|
||||
"test-runner": "mtp"
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue