Skip to content

Commit 7440cfb

Browse files
Merge pull request #66 from ktsu-dev/work/issue-54-55
test: cover IVector*/Magnitude/Dot/Cross and semantic overload conversions
2 parents 85e6b25 + f7b4ad7 commit 7440cfb

2 files changed

Lines changed: 363 additions & 0 deletions

File tree

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
// Copyright (c) ktsu.dev
2+
// All rights reserved.
3+
// Licensed under the MIT license.
4+
5+
namespace ktsu.Semantics.Test.Quantities;
6+
7+
using ktsu.Semantics.Quantities;
8+
using Microsoft.VisualStudio.TestTools.UnitTesting;
9+
10+
/// <summary>
11+
/// Covers semantic overload conversions and metadata-driven relationships.
12+
/// Issue #55.
13+
/// </summary>
14+
[TestClass]
15+
public sealed class SemanticOverloadTests
16+
{
17+
private const double Tolerance = 1e-10;
18+
19+
// ----------------------------------------- Implicit widening to base
20+
21+
[TestMethod]
22+
public void Weight_Widens_Implicitly_To_ForceMagnitude()
23+
{
24+
Weight<double> w = Weight<double>.FromNewton(686.0);
25+
ForceMagnitude<double> baseValue = w; // implicit conversion
26+
Assert.AreEqual(686.0, baseValue.Value, Tolerance);
27+
}
28+
29+
[TestMethod]
30+
public void Distance_Widens_Implicitly_To_Length()
31+
{
32+
Distance<double> d = Distance<double>.FromMeter(42.0);
33+
Length<double> len = d;
34+
Assert.AreEqual(42.0, len.Value, Tolerance);
35+
}
36+
37+
[TestMethod]
38+
public void Diameter_Widens_Implicitly_To_Length()
39+
{
40+
Diameter<double> diam = Diameter<double>.FromMeter(10.0);
41+
Length<double> len = diam;
42+
Assert.AreEqual(10.0, len.Value, Tolerance);
43+
}
44+
45+
// ---------------------------------------- Explicit narrowing from base
46+
47+
[TestMethod]
48+
public void ForceMagnitude_Narrows_Explicitly_To_Weight()
49+
{
50+
ForceMagnitude<double> fm = ForceMagnitude<double>.FromNewton(686.0);
51+
Weight<double> w = (Weight<double>)fm;
52+
Assert.AreEqual(686.0, w.Value, Tolerance);
53+
}
54+
55+
[TestMethod]
56+
public void Length_Narrows_Explicitly_To_Distance()
57+
{
58+
Length<double> len = Length<double>.FromMeter(42.0);
59+
Distance<double> d = (Distance<double>)len;
60+
Assert.AreEqual(42.0, d.Value, Tolerance);
61+
}
62+
63+
// --------------------------------------------- From(base) factory
64+
65+
[TestMethod]
66+
public void Weight_From_ForceMagnitude_Constructs()
67+
{
68+
ForceMagnitude<double> fm = ForceMagnitude<double>.FromNewton(100.0);
69+
Weight<double> w = Weight<double>.From(fm);
70+
Assert.AreEqual(100.0, w.Value, Tolerance);
71+
}
72+
73+
[TestMethod]
74+
public void Distance_From_Length_Constructs()
75+
{
76+
Length<double> len = Length<double>.FromMeter(7.0);
77+
Distance<double> d = Distance<double>.From(len);
78+
Assert.AreEqual(7.0, d.Value, Tolerance);
79+
}
80+
81+
// -------------------- Round-trip widen/narrow preserves value
82+
83+
[TestMethod]
84+
public void Weight_RoundTrip_Through_ForceMagnitude_Preserves_Value()
85+
{
86+
Weight<double> original = Weight<double>.FromNewton(123.456);
87+
ForceMagnitude<double> widened = original;
88+
Weight<double> narrowed = (Weight<double>)widened;
89+
Assert.AreEqual(original.Value, narrowed.Value, Tolerance);
90+
}
91+
92+
// ------------------ Metadata-defined relationship: Diameter <-> Radius
93+
94+
[TestMethod]
95+
public void Diameter_ToRadius_Halves_Value()
96+
{
97+
Diameter<double> d = Diameter<double>.FromMeter(10.0);
98+
Radius<double> r = d.ToRadius();
99+
Assert.AreEqual(5.0, r.Value, Tolerance);
100+
}
101+
102+
[TestMethod]
103+
public void Diameter_FromRadius_Doubles_Value()
104+
{
105+
Radius<double> r = Radius<double>.FromMeter(5.0);
106+
Diameter<double> d = Diameter<double>.FromRadius(r);
107+
Assert.AreEqual(10.0, d.Value, Tolerance);
108+
}
109+
110+
[TestMethod]
111+
public void Diameter_RoundTrip_Through_Radius_Preserves_Value()
112+
{
113+
Diameter<double> d = Diameter<double>.FromMeter(20.0);
114+
Radius<double> r = d.ToRadius();
115+
Diameter<double> back = Diameter<double>.FromRadius(r);
116+
Assert.AreEqual(d.Value, back.Value, Tolerance);
117+
}
118+
119+
// ----------------- V0 overload subtraction
120+
// Locked in #52: V0 - V0 returns the same V0 of T.Abs(a - b).
121+
// Generator currently emits a Force1D-returning subtraction for Weight - Weight,
122+
// which violates that rule. The current behaviour is documented here so the fix
123+
// in #52 can replace this test with the correct shape.
124+
125+
[TestMethod]
126+
public void Weight_Minus_Weight_Currently_Returns_Force1D_PendingFix52()
127+
{
128+
Weight<double> a = Weight<double>.FromNewton(100.0);
129+
Weight<double> b = Weight<double>.FromNewton(150.0);
130+
Force1D<double> diff = a - b; // current generator behaviour; #52 plans Weight of |a - b|.
131+
Assert.AreEqual(-50.0, diff.Value, Tolerance);
132+
}
133+
134+
// ----------------- Storage-type genericity sanity
135+
136+
[TestMethod]
137+
public void Diameter_ToRadius_Works_With_Float_Storage()
138+
{
139+
Diameter<float> d = Diameter<float>.FromMeter(10.0f);
140+
Radius<float> r = d.ToRadius();
141+
Assert.AreEqual(5.0f, r.Value, 1e-6f);
142+
}
143+
144+
[TestMethod]
145+
public void Diameter_ToRadius_Works_With_Decimal_Storage()
146+
{
147+
Diameter<decimal> d = Diameter<decimal>.FromMeter(10m);
148+
Radius<decimal> r = d.ToRadius();
149+
Assert.AreEqual(5m, r.Value);
150+
}
151+
}
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
// Copyright (c) ktsu.dev
2+
// All rights reserved.
3+
// Licensed under the MIT license.
4+
5+
namespace ktsu.Semantics.Test.Quantities;
6+
7+
using ktsu.Semantics.Quantities;
8+
using Microsoft.VisualStudio.TestTools.UnitTesting;
9+
10+
/// <summary>
11+
/// Covers <see cref="IVector0{TSelf, T}"/>..<see cref="IVector4{TSelf, T}"/> contracts:
12+
/// magnitude extraction, typed dot/cross products, vector arithmetic, and V0 invariants.
13+
/// Issue #54.
14+
/// </summary>
15+
[TestClass]
16+
public sealed class VectorQuantityTests
17+
{
18+
private const double Tolerance = 1e-10;
19+
20+
// -------------------------------------------------------------- Magnitude
21+
22+
[TestMethod]
23+
public void Velocity3D_Magnitude_Of_3_4_0_Is_Speed_5()
24+
{
25+
Velocity3D<double> v = new() { X = 3.0, Y = 4.0, Z = 0.0 };
26+
Speed<double> s = v.Magnitude();
27+
Assert.AreEqual(5.0, s.Value, Tolerance);
28+
}
29+
30+
[TestMethod]
31+
public void Force3D_Magnitude_Is_Always_NonNegative_Even_With_Negative_Components()
32+
{
33+
Force3D<double> f = new() { X = -3.0, Y = -4.0, Z = 0.0 };
34+
ForceMagnitude<double> m = f.Magnitude();
35+
Assert.AreEqual(5.0, m.Value, Tolerance);
36+
}
37+
38+
[TestMethod]
39+
public void Velocity3D_Magnitude_Returns_Speed_Type_Statically()
40+
{
41+
Velocity3D<double> v = new() { X = 1.0, Y = 0.0, Z = 0.0 };
42+
Speed<double> s = v.Magnitude();
43+
Assert.IsInstanceOfType<Speed<double>>(s);
44+
}
45+
46+
[TestMethod]
47+
public void Velocity3D_Magnitude_Of_Zero_Vector_Is_Zero()
48+
{
49+
Velocity3D<double> zero = Velocity3D<double>.Zero;
50+
Speed<double> s = zero.Magnitude();
51+
Assert.AreEqual(0.0, s.Value, Tolerance);
52+
}
53+
54+
// ------------------------------------------------------ Typed dot product
55+
56+
[TestMethod]
57+
public void Force3D_Dot_Displacement3D_Returns_Energy_Aligned()
58+
{
59+
Force3D<double> f = new() { X = 10.0, Y = 0.0, Z = 0.0 };
60+
Displacement3D<double> r = new() { X = 2.0, Y = 0.0, Z = 0.0 };
61+
Energy<double> work = f.Dot(r);
62+
Assert.AreEqual(20.0, work.Value, Tolerance);
63+
}
64+
65+
[TestMethod]
66+
public void Force3D_Dot_Displacement3D_Is_Zero_For_Perpendicular()
67+
{
68+
Force3D<double> f = new() { X = 10.0, Y = 0.0, Z = 0.0 };
69+
Displacement3D<double> r = new() { X = 0.0, Y = 5.0, Z = 0.0 };
70+
Energy<double> work = f.Dot(r);
71+
Assert.AreEqual(0.0, work.Value, Tolerance);
72+
}
73+
74+
// ---------------------------------------------------- Typed cross product
75+
76+
[TestMethod]
77+
public void Force3D_Cross_Displacement3D_Returns_Torque3D()
78+
{
79+
Force3D<double> f = new() { X = 0.0, Y = 10.0, Z = 0.0 };
80+
Displacement3D<double> r = new() { X = 0.5, Y = 0.0, Z = 0.0 };
81+
Torque3D<double> t = f.Cross(r);
82+
// (Y*rZ - Z*rY, Z*rX - X*rZ, X*rY - Y*rX) = (0, 0, -5)
83+
Assert.AreEqual(0.0, t.X, Tolerance);
84+
Assert.AreEqual(0.0, t.Y, Tolerance);
85+
Assert.AreEqual(-5.0, t.Z, Tolerance);
86+
}
87+
88+
[TestMethod]
89+
public void Force3D_Cross_Self_Is_Zero_Vector()
90+
{
91+
Force3D<double> f = new() { X = 1.0, Y = 2.0, Z = 3.0 };
92+
// Same-dimension structural cross returns Force3D (the dimension itself, not a typed dimensional product).
93+
Force3D<double> c = f.Cross(f);
94+
Assert.AreEqual(0.0, c.X, Tolerance);
95+
Assert.AreEqual(0.0, c.Y, Tolerance);
96+
Assert.AreEqual(0.0, c.Z, Tolerance);
97+
}
98+
99+
// --------------------------------------------- Same-dimension dot product
100+
101+
[TestMethod]
102+
public void Velocity3D_Dot_Velocity3D_Returns_Raw_Storage_Scalar()
103+
{
104+
// Same-dimension Dot is structural and returns the raw storage type; it isn't
105+
// a typed dimensional product (no "Speed²" exists in the type system).
106+
Velocity3D<double> a = new() { X = 1.0, Y = 2.0, Z = 3.0 };
107+
Velocity3D<double> b = new() { X = 4.0, Y = 5.0, Z = 6.0 };
108+
double dot = a.Dot(b);
109+
Assert.AreEqual(32.0, dot, Tolerance);
110+
}
111+
112+
// ------------------------------------------------ Vector form arithmetic
113+
114+
[TestMethod]
115+
public void Force3D_Plus_Force3D_Stays_Force3D_Componentwise()
116+
{
117+
Force3D<double> a = new() { X = 1.0, Y = 2.0, Z = 3.0 };
118+
Force3D<double> b = new() { X = 4.0, Y = 5.0, Z = 6.0 };
119+
Force3D<double> sum = a + b;
120+
Assert.AreEqual(5.0, sum.X, Tolerance);
121+
Assert.AreEqual(7.0, sum.Y, Tolerance);
122+
Assert.AreEqual(9.0, sum.Z, Tolerance);
123+
}
124+
125+
[TestMethod]
126+
public void Force3D_Minus_Force3D_Componentwise()
127+
{
128+
Force3D<double> a = new() { X = 5.0, Y = 7.0, Z = 9.0 };
129+
Force3D<double> b = new() { X = 1.0, Y = 2.0, Z = 3.0 };
130+
Force3D<double> diff = a - b;
131+
Assert.AreEqual(4.0, diff.X, Tolerance);
132+
Assert.AreEqual(5.0, diff.Y, Tolerance);
133+
Assert.AreEqual(6.0, diff.Z, Tolerance);
134+
}
135+
136+
[TestMethod]
137+
public void Force3D_Negation_Inverts_Each_Component()
138+
{
139+
Force3D<double> f = new() { X = 1.0, Y = -2.0, Z = 3.0 };
140+
Force3D<double> n = -f;
141+
Assert.AreEqual(-1.0, n.X, Tolerance);
142+
Assert.AreEqual(2.0, n.Y, Tolerance);
143+
Assert.AreEqual(-3.0, n.Z, Tolerance);
144+
}
145+
146+
// ------------------------------------------------------------- V0 + V0
147+
148+
[TestMethod]
149+
public void Mass_Plus_Mass_Returns_Mass()
150+
{
151+
Mass<double> a = Mass<double>.FromKilogram(3.0);
152+
Mass<double> b = Mass<double>.FromKilogram(5.0);
153+
Mass<double> sum = a + b;
154+
Assert.AreEqual(8.0, sum.Value, Tolerance);
155+
Assert.IsInstanceOfType<Mass<double>>(sum);
156+
}
157+
158+
[TestMethod]
159+
public void Speed_Plus_Speed_Returns_Speed()
160+
{
161+
Speed<double> a = Speed<double>.FromMetersPerSecond(3.0);
162+
Speed<double> b = Speed<double>.FromMetersPerSecond(5.0);
163+
Speed<double> sum = a + b;
164+
Assert.AreEqual(8.0, sum.Value, Tolerance);
165+
}
166+
167+
// ------------------------------------------------------------- V0 - V0
168+
// Locked design decision in #52: V0 - V0 should return the same V0 of T.Abs(a - b).
169+
// Generator currently emits unsigned subtraction via the SemanticQuantity base, which
170+
// can produce a negative magnitude. Tracked as a follow-up.
171+
172+
[TestMethod]
173+
[Ignore("Locked in #52: V0 - V0 should return the same V0 of T.Abs(a - b). Generator currently emits unsigned subtraction.")]
174+
public void Mass_Minus_Mass_Returns_Absolute_Difference_Pending52()
175+
{
176+
Mass<double> a = Mass<double>.FromKilogram(3.0);
177+
Mass<double> b = Mass<double>.FromKilogram(5.0);
178+
Mass<double> diff = a - b;
179+
Assert.AreEqual(2.0, diff.Value, Tolerance);
180+
}
181+
182+
// ---------------------------------------------------- V0 non-negativity
183+
// Tracked in #50: factories on Vector0 quantities should reject negative inputs
184+
// with ArgumentException. The current generator does not emit guards.
185+
186+
[TestMethod]
187+
[Ignore("Tracked in #50: V0 factories should reject negative inputs.")]
188+
public void Speed_From_Negative_Throws_Pending50()
189+
{
190+
_ = Assert.ThrowsExactly<System.ArgumentException>(
191+
() => Speed<double>.FromMetersPerSecond(-1.0));
192+
}
193+
194+
[TestMethod]
195+
[Ignore("Tracked in #50: V0 factories should reject negative inputs.")]
196+
public void Mass_From_Negative_Throws_Pending50()
197+
{
198+
_ = Assert.ThrowsExactly<System.ArgumentException>(
199+
() => Mass<double>.FromKilogram(-1.0));
200+
}
201+
202+
// -------------------------------------------------- Magnitude on V1
203+
// Velocity1D.Magnitude() should return Speed of T.Abs(value).
204+
205+
[TestMethod]
206+
public void Velocity1D_Magnitude_Of_Negative_Is_Positive_Speed()
207+
{
208+
Velocity1D<double> v = Velocity1D<double>.FromMetersPerSecond(-3.5);
209+
Speed<double> s = v.Magnitude();
210+
Assert.AreEqual(3.5, s.Value, Tolerance);
211+
}
212+
}

0 commit comments

Comments
 (0)