diff --git a/Semantics.Test/Quantities/SemanticOverloadTests.cs b/Semantics.Test/Quantities/SemanticOverloadTests.cs new file mode 100644 index 0000000..0d256af --- /dev/null +++ b/Semantics.Test/Quantities/SemanticOverloadTests.cs @@ -0,0 +1,151 @@ +// Copyright (c) ktsu.dev +// All rights reserved. +// Licensed under the MIT license. + +namespace ktsu.Semantics.Test.Quantities; + +using ktsu.Semantics.Quantities; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// Covers semantic overload conversions and metadata-driven relationships. +/// Issue #55. +/// +[TestClass] +public sealed class SemanticOverloadTests +{ + private const double Tolerance = 1e-10; + + // ----------------------------------------- Implicit widening to base + + [TestMethod] + public void Weight_Widens_Implicitly_To_ForceMagnitude() + { + Weight w = Weight.FromNewton(686.0); + ForceMagnitude baseValue = w; // implicit conversion + Assert.AreEqual(686.0, baseValue.Value, Tolerance); + } + + [TestMethod] + public void Distance_Widens_Implicitly_To_Length() + { + Distance d = Distance.FromMeter(42.0); + Length len = d; + Assert.AreEqual(42.0, len.Value, Tolerance); + } + + [TestMethod] + public void Diameter_Widens_Implicitly_To_Length() + { + Diameter diam = Diameter.FromMeter(10.0); + Length len = diam; + Assert.AreEqual(10.0, len.Value, Tolerance); + } + + // ---------------------------------------- Explicit narrowing from base + + [TestMethod] + public void ForceMagnitude_Narrows_Explicitly_To_Weight() + { + ForceMagnitude fm = ForceMagnitude.FromNewton(686.0); + Weight w = (Weight)fm; + Assert.AreEqual(686.0, w.Value, Tolerance); + } + + [TestMethod] + public void Length_Narrows_Explicitly_To_Distance() + { + Length len = Length.FromMeter(42.0); + Distance d = (Distance)len; + Assert.AreEqual(42.0, d.Value, Tolerance); + } + + // --------------------------------------------- From(base) factory + + [TestMethod] + public void Weight_From_ForceMagnitude_Constructs() + { + ForceMagnitude fm = ForceMagnitude.FromNewton(100.0); + Weight w = Weight.From(fm); + Assert.AreEqual(100.0, w.Value, Tolerance); + } + + [TestMethod] + public void Distance_From_Length_Constructs() + { + Length len = Length.FromMeter(7.0); + Distance d = Distance.From(len); + Assert.AreEqual(7.0, d.Value, Tolerance); + } + + // -------------------- Round-trip widen/narrow preserves value + + [TestMethod] + public void Weight_RoundTrip_Through_ForceMagnitude_Preserves_Value() + { + Weight original = Weight.FromNewton(123.456); + ForceMagnitude widened = original; + Weight narrowed = (Weight)widened; + Assert.AreEqual(original.Value, narrowed.Value, Tolerance); + } + + // ------------------ Metadata-defined relationship: Diameter <-> Radius + + [TestMethod] + public void Diameter_ToRadius_Halves_Value() + { + Diameter d = Diameter.FromMeter(10.0); + Radius r = d.ToRadius(); + Assert.AreEqual(5.0, r.Value, Tolerance); + } + + [TestMethod] + public void Diameter_FromRadius_Doubles_Value() + { + Radius r = Radius.FromMeter(5.0); + Diameter d = Diameter.FromRadius(r); + Assert.AreEqual(10.0, d.Value, Tolerance); + } + + [TestMethod] + public void Diameter_RoundTrip_Through_Radius_Preserves_Value() + { + Diameter d = Diameter.FromMeter(20.0); + Radius r = d.ToRadius(); + Diameter back = Diameter.FromRadius(r); + Assert.AreEqual(d.Value, back.Value, Tolerance); + } + + // ----------------- V0 overload subtraction + // Locked in #52: V0 - V0 returns the same V0 of T.Abs(a - b). + // Generator currently emits a Force1D-returning subtraction for Weight - Weight, + // which violates that rule. The current behaviour is documented here so the fix + // in #52 can replace this test with the correct shape. + + [TestMethod] + public void Weight_Minus_Weight_Currently_Returns_Force1D_PendingFix52() + { + Weight a = Weight.FromNewton(100.0); + Weight b = Weight.FromNewton(150.0); + Force1D diff = a - b; // current generator behaviour; #52 plans Weight of |a - b|. + Assert.AreEqual(-50.0, diff.Value, Tolerance); + } + + // ----------------- Storage-type genericity sanity + + [TestMethod] + public void Diameter_ToRadius_Works_With_Float_Storage() + { + Diameter d = Diameter.FromMeter(10.0f); + Radius r = d.ToRadius(); + Assert.AreEqual(5.0f, r.Value, 1e-6f); + } + + [TestMethod] + public void Diameter_ToRadius_Works_With_Decimal_Storage() + { + Diameter d = Diameter.FromMeter(10m); + Radius r = d.ToRadius(); + Assert.AreEqual(5m, r.Value); + } +} diff --git a/Semantics.Test/Quantities/VectorQuantityTests.cs b/Semantics.Test/Quantities/VectorQuantityTests.cs new file mode 100644 index 0000000..492952d --- /dev/null +++ b/Semantics.Test/Quantities/VectorQuantityTests.cs @@ -0,0 +1,212 @@ +// Copyright (c) ktsu.dev +// All rights reserved. +// Licensed under the MIT license. + +namespace ktsu.Semantics.Test.Quantities; + +using ktsu.Semantics.Quantities; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// Covers .. contracts: +/// magnitude extraction, typed dot/cross products, vector arithmetic, and V0 invariants. +/// Issue #54. +/// +[TestClass] +public sealed class VectorQuantityTests +{ + private const double Tolerance = 1e-10; + + // -------------------------------------------------------------- Magnitude + + [TestMethod] + public void Velocity3D_Magnitude_Of_3_4_0_Is_Speed_5() + { + Velocity3D v = new() { X = 3.0, Y = 4.0, Z = 0.0 }; + Speed s = v.Magnitude(); + Assert.AreEqual(5.0, s.Value, Tolerance); + } + + [TestMethod] + public void Force3D_Magnitude_Is_Always_NonNegative_Even_With_Negative_Components() + { + Force3D f = new() { X = -3.0, Y = -4.0, Z = 0.0 }; + ForceMagnitude m = f.Magnitude(); + Assert.AreEqual(5.0, m.Value, Tolerance); + } + + [TestMethod] + public void Velocity3D_Magnitude_Returns_Speed_Type_Statically() + { + Velocity3D v = new() { X = 1.0, Y = 0.0, Z = 0.0 }; + Speed s = v.Magnitude(); + Assert.IsInstanceOfType>(s); + } + + [TestMethod] + public void Velocity3D_Magnitude_Of_Zero_Vector_Is_Zero() + { + Velocity3D zero = Velocity3D.Zero; + Speed s = zero.Magnitude(); + Assert.AreEqual(0.0, s.Value, Tolerance); + } + + // ------------------------------------------------------ Typed dot product + + [TestMethod] + public void Force3D_Dot_Displacement3D_Returns_Energy_Aligned() + { + Force3D f = new() { X = 10.0, Y = 0.0, Z = 0.0 }; + Displacement3D r = new() { X = 2.0, Y = 0.0, Z = 0.0 }; + Energy work = f.Dot(r); + Assert.AreEqual(20.0, work.Value, Tolerance); + } + + [TestMethod] + public void Force3D_Dot_Displacement3D_Is_Zero_For_Perpendicular() + { + Force3D f = new() { X = 10.0, Y = 0.0, Z = 0.0 }; + Displacement3D r = new() { X = 0.0, Y = 5.0, Z = 0.0 }; + Energy work = f.Dot(r); + Assert.AreEqual(0.0, work.Value, Tolerance); + } + + // ---------------------------------------------------- Typed cross product + + [TestMethod] + public void Force3D_Cross_Displacement3D_Returns_Torque3D() + { + Force3D f = new() { X = 0.0, Y = 10.0, Z = 0.0 }; + Displacement3D r = new() { X = 0.5, Y = 0.0, Z = 0.0 }; + Torque3D t = f.Cross(r); + // (Y*rZ - Z*rY, Z*rX - X*rZ, X*rY - Y*rX) = (0, 0, -5) + Assert.AreEqual(0.0, t.X, Tolerance); + Assert.AreEqual(0.0, t.Y, Tolerance); + Assert.AreEqual(-5.0, t.Z, Tolerance); + } + + [TestMethod] + public void Force3D_Cross_Self_Is_Zero_Vector() + { + Force3D f = new() { X = 1.0, Y = 2.0, Z = 3.0 }; + // Same-dimension structural cross returns Force3D (the dimension itself, not a typed dimensional product). + Force3D c = f.Cross(f); + Assert.AreEqual(0.0, c.X, Tolerance); + Assert.AreEqual(0.0, c.Y, Tolerance); + Assert.AreEqual(0.0, c.Z, Tolerance); + } + + // --------------------------------------------- Same-dimension dot product + + [TestMethod] + public void Velocity3D_Dot_Velocity3D_Returns_Raw_Storage_Scalar() + { + // Same-dimension Dot is structural and returns the raw storage type; it isn't + // a typed dimensional product (no "Speed²" exists in the type system). + Velocity3D a = new() { X = 1.0, Y = 2.0, Z = 3.0 }; + Velocity3D b = new() { X = 4.0, Y = 5.0, Z = 6.0 }; + double dot = a.Dot(b); + Assert.AreEqual(32.0, dot, Tolerance); + } + + // ------------------------------------------------ Vector form arithmetic + + [TestMethod] + public void Force3D_Plus_Force3D_Stays_Force3D_Componentwise() + { + Force3D a = new() { X = 1.0, Y = 2.0, Z = 3.0 }; + Force3D b = new() { X = 4.0, Y = 5.0, Z = 6.0 }; + Force3D sum = a + b; + Assert.AreEqual(5.0, sum.X, Tolerance); + Assert.AreEqual(7.0, sum.Y, Tolerance); + Assert.AreEqual(9.0, sum.Z, Tolerance); + } + + [TestMethod] + public void Force3D_Minus_Force3D_Componentwise() + { + Force3D a = new() { X = 5.0, Y = 7.0, Z = 9.0 }; + Force3D b = new() { X = 1.0, Y = 2.0, Z = 3.0 }; + Force3D diff = a - b; + Assert.AreEqual(4.0, diff.X, Tolerance); + Assert.AreEqual(5.0, diff.Y, Tolerance); + Assert.AreEqual(6.0, diff.Z, Tolerance); + } + + [TestMethod] + public void Force3D_Negation_Inverts_Each_Component() + { + Force3D f = new() { X = 1.0, Y = -2.0, Z = 3.0 }; + Force3D n = -f; + Assert.AreEqual(-1.0, n.X, Tolerance); + Assert.AreEqual(2.0, n.Y, Tolerance); + Assert.AreEqual(-3.0, n.Z, Tolerance); + } + + // ------------------------------------------------------------- V0 + V0 + + [TestMethod] + public void Mass_Plus_Mass_Returns_Mass() + { + Mass a = Mass.FromKilogram(3.0); + Mass b = Mass.FromKilogram(5.0); + Mass sum = a + b; + Assert.AreEqual(8.0, sum.Value, Tolerance); + Assert.IsInstanceOfType>(sum); + } + + [TestMethod] + public void Speed_Plus_Speed_Returns_Speed() + { + Speed a = Speed.FromMetersPerSecond(3.0); + Speed b = Speed.FromMetersPerSecond(5.0); + Speed sum = a + b; + Assert.AreEqual(8.0, sum.Value, Tolerance); + } + + // ------------------------------------------------------------- V0 - V0 + // Locked design decision in #52: V0 - V0 should return the same V0 of T.Abs(a - b). + // Generator currently emits unsigned subtraction via the SemanticQuantity base, which + // can produce a negative magnitude. Tracked as a follow-up. + + [TestMethod] + [Ignore("Locked in #52: V0 - V0 should return the same V0 of T.Abs(a - b). Generator currently emits unsigned subtraction.")] + public void Mass_Minus_Mass_Returns_Absolute_Difference_Pending52() + { + Mass a = Mass.FromKilogram(3.0); + Mass b = Mass.FromKilogram(5.0); + Mass diff = a - b; + Assert.AreEqual(2.0, diff.Value, Tolerance); + } + + // ---------------------------------------------------- V0 non-negativity + // Tracked in #50: factories on Vector0 quantities should reject negative inputs + // with ArgumentException. The current generator does not emit guards. + + [TestMethod] + [Ignore("Tracked in #50: V0 factories should reject negative inputs.")] + public void Speed_From_Negative_Throws_Pending50() + { + _ = Assert.ThrowsExactly( + () => Speed.FromMetersPerSecond(-1.0)); + } + + [TestMethod] + [Ignore("Tracked in #50: V0 factories should reject negative inputs.")] + public void Mass_From_Negative_Throws_Pending50() + { + _ = Assert.ThrowsExactly( + () => Mass.FromKilogram(-1.0)); + } + + // -------------------------------------------------- Magnitude on V1 + // Velocity1D.Magnitude() should return Speed of T.Abs(value). + + [TestMethod] + public void Velocity1D_Magnitude_Of_Negative_Is_Positive_Speed() + { + Velocity1D v = Velocity1D.FromMetersPerSecond(-3.5); + Speed s = v.Magnitude(); + Assert.AreEqual(3.5, s.Value, Tolerance); + } +}