Skip to content

Commit 20333fe

Browse files
committed
Add tests for geometry formulas and calculations
1 parent 821992d commit 20333fe

10 files changed

Lines changed: 274 additions & 0 deletions

.github/workflows/swift.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,6 @@ jobs:
2929
3030
- name: Build
3131
run: swift build -v
32+
33+
- name: Test
34+
run: swift test -v

Package.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ let package = Package(
2222
name: "Demo",
2323
dependencies: ["Cadova", "Helical"],
2424
swiftSettings: [.interoperabilityMode(.Cxx)]
25+
),
26+
.testTarget(
27+
name: "HelicalTests",
28+
dependencies: ["Helical", "Cadova"],
29+
swiftSettings: [.interoperabilityMode(.Cxx)]
2530
)
2631
]
2732
)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import Testing
2+
import Helical
3+
import Cadova
4+
5+
private func isClose(_ a: Double, _ b: Double, tolerance: Double = 1e-9) -> Bool { abs(a - b) < tolerance }
6+
7+
struct CountersunkHeadTests {
8+
@Test func `height is (topDiameter - boltDiameter) / 2 × tan(angle / 2)`() {
9+
let noLens = CountersunkBoltHeadShape(angle: 90°, topDiameter: 10, boltDiameter: 5)
10+
#expect(isClose(noLens.height, 2.5, tolerance: 1e-6))
11+
12+
// 82° uses angle/2 = 41°, not the full angle
13+
let angled = CountersunkBoltHeadShape(angle: 82°, topDiameter: 8, boltDiameter: 4)
14+
#expect(isClose(angled.height, 2.0 * tan(41°), tolerance: 1e-6))
15+
}
16+
17+
@Test func `lens height adds to total height but not consumed length`() {
18+
let shape = CountersunkBoltHeadShape(angle: 90°, topDiameter: 10, boltDiameter: 5, lensHeight: 1)
19+
#expect(isClose(shape.height, 3.5, tolerance: 1e-6))
20+
#expect(isClose(shape.consumedLength, 2.5, tolerance: 1e-6))
21+
}
22+
23+
@Test func `consumed length equals height minus lens height`() {
24+
let shape = CountersunkBoltHeadShape(angle: 90°, topDiameter: 12, boltDiameter: 6, lensHeight: 1.5)
25+
#expect(isClose(shape.consumedLength, shape.height - 1.5))
26+
}
27+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import Testing
2+
import Helical
3+
4+
private func isClose(_ a: Double, _ b: Double, tolerance: Double = 1e-9) -> Bool { abs(a - b) < tolerance }
5+
6+
struct EdisonScrewTests {
7+
// pitchDiameter is computed via the knuckle threadform arc formula —
8+
// several steps of non-trivial math over stored major/minor/pitch values.
9+
@Test func `pitch diameter is computed correctly from arc formula`() {
10+
// E27: h=1.25, pitch=3.62, radius≈0.96772 → pitchDiameter≈25.75
11+
#expect(isClose(ScrewThread.e27.pitchDiameter, 25.75, tolerance: 1e-3))
12+
13+
// E5.5: radius=17/60, d=9/60=0.15 → pitchDiameter = 5.5 - 0.3 = 5.2 (exact)
14+
#expect(isClose(ScrewThread.e5p5.pitchDiameter, 5.2, tolerance: 1e-6))
15+
}
16+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import Testing
2+
import Helical
3+
4+
private func isClose(_ a: Double, _ b: Double) -> Bool { abs(a - b) < 1e-9 }
5+
6+
struct ISOMetricThreadTests {
7+
// The minor diameter is derived from major and pitch via the V-thread formula,
8+
// not stored directly — worth verifying the formula constant is correct.
9+
@Test func `minor diameter is major - pitch × 1.082532`() {
10+
#expect(isClose(ScrewThread.m3.minorDiameter, 3.0 - 0.5 * 1.082532))
11+
#expect(isClose(ScrewThread.m8.minorDiameter, 8.0 - 1.25 * 1.082532))
12+
#expect(isClose(ScrewThread.m64.minorDiameter, 64.0 - 6.0 * 1.082532))
13+
}
14+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import Testing
2+
@testable import Helical
3+
import Cadova
4+
5+
private func isClose(_ a: Double, _ b: Double) -> Bool { abs(a - b) < 1e-9 }
6+
7+
struct LeadInTests {
8+
let m3 = ScrewThread.m3
9+
let m8 = ScrewThread.m8
10+
11+
@Test func `constant(depth:length:) returns exact values`() {
12+
let (depth, length) = LeadIn.constant(depth: 0.5, length: 1.0).resolved(for: m3)
13+
#expect(depth == 0.5)
14+
#expect(length == 1.0)
15+
}
16+
17+
@Test func `constant(depth:) produces depth == length`() {
18+
let (depth, length) = LeadIn.constant(depth: 0.3).resolved(for: m3)
19+
#expect(depth == 0.3)
20+
#expect(length == 0.3)
21+
}
22+
23+
@Test func `depth(multiple:) scales by thread depth`() {
24+
let (d1, l1) = LeadIn.depth(multiple: 1).resolved(for: m3)
25+
#expect(isClose(d1, m3.depth))
26+
#expect(isClose(l1, m3.depth))
27+
28+
let (d2, l2) = LeadIn.depth(multiple: 2).resolved(for: m8)
29+
#expect(isClose(d2, m8.depth * 2))
30+
#expect(isClose(l2, m8.depth * 2))
31+
}
32+
33+
@Test func `pitch(multiple:) scales by thread pitch`() {
34+
let (d1, l1) = LeadIn.pitch(multiple: 1).resolved(for: m3)
35+
#expect(d1 == 0.5)
36+
#expect(l1 == 0.5)
37+
38+
let (d2, l2) = LeadIn.pitch(multiple: 0.5).resolved(for: m8)
39+
#expect(d2 == 0.625)
40+
#expect(l2 == 0.625)
41+
}
42+
43+
@Test func `angle(90°) produces 45° chamfer: depth == length`() {
44+
let (depth, length) = LeadIn.angle(90°).resolved(for: m3)
45+
#expect(isClose(depth, m3.depth))
46+
#expect(isClose(length, depth))
47+
}
48+
49+
@Test func `angle(120°) produces length = depth × tan(30°)`() {
50+
let (depth, length) = LeadIn.angle(120°).resolved(for: m3)
51+
#expect(isClose(depth, m3.depth))
52+
#expect(isClose(length, depth * tan(30°)))
53+
}
54+
55+
@Test func `standard equals depth(multiple: 1)`() {
56+
let (d1, l1) = LeadIn.standard.resolved(for: m3)
57+
let (d2, l2) = LeadIn.depth(multiple: 1).resolved(for: m3)
58+
#expect(d1 == d2)
59+
#expect(l1 == l2)
60+
}
61+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import Testing
2+
@testable import Helical
3+
import Cadova
4+
5+
private func isClose(_ a: Double, _ b: Double) -> Bool { abs(a - b) < 1e-9 }
6+
7+
// PhillipsMetrics.depth applies a two-term trig formula to stored constants.
8+
// These tests verify that both the stored constants and the formula are correct
9+
// by computing the expected depth from the ISO 4757 spec values independently.
10+
// depth = bottomWidth/2 × tan(28°) + (fullWidth/2 − bottomWidth/2) / tan(26.5°)
11+
struct PhillipsTests {
12+
@Test func `PH0 depth matches formula with spec constants`() {
13+
let m = PhillipsMetrics(size: .ph0, width: 1.8)
14+
let expected = 0.81 / 2 * tan(28°) + (1.8 / 2 - 0.81 / 2) / tan(26.5°)
15+
#expect(isClose(m.depth, expected))
16+
}
17+
18+
@Test func `PH2 depth matches formula with spec constants`() {
19+
let m = PhillipsMetrics(size: .ph2, width: 4.6)
20+
let expected = 2.29 / 2 * tan(28°) + (4.6 / 2 - 2.29 / 2) / tan(26.5°)
21+
#expect(isClose(m.depth, expected))
22+
}
23+
24+
@Test func `PH4 depth matches formula with spec constants`() {
25+
let m = PhillipsMetrics(size: .ph4, width: 9)
26+
let expected = 5.08 / 2 * tan(28°) + (9.0 / 2 - 5.08 / 2) / tan(26.5°)
27+
#expect(isClose(m.depth, expected))
28+
}
29+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import Testing
2+
@testable import Helical
3+
import Cadova
4+
5+
private func isClose(_ a: Double, _ b: Double) -> Bool { abs(a - b) < 1e-9 }
6+
7+
struct ScrewThreadTests {
8+
@Test func `depth is (majorDiameter - minorDiameter) / 2`() {
9+
// M3: major=3.0, minor=3.0 - 0.5*1.082532 = 2.458734
10+
#expect(isClose(ScrewThread.m3.depth, 0.270633))
11+
12+
// Custom thread with exact integer values
13+
let t = ScrewThread(pitch: 1.5, majorDiameter: 10, minorDiameter: 8, form: .trapezoidal(angle: 60°, crestWidth: 0.125))
14+
#expect(t.depth == 1.0)
15+
}
16+
17+
@Test func `lead is starts × pitch`() {
18+
#expect(ScrewThread.m3.lead == 0.5)
19+
20+
// 3-start ACME thread: lead = 3 × 4 = 12
21+
let t = ScrewThread.acme(majorDiameter: 20, pitch: 4, starts: 3)
22+
#expect(t.lead == 12.0)
23+
}
24+
25+
@Test func `leftHanded reflects handedness`() {
26+
#expect(ScrewThread.m3.leftHanded == false)
27+
let left = ScrewThread(handedness: .left, pitch: 0.5, majorDiameter: 3, minorDiameter: 2.458734, form: .trapezoidal(angle: 60°, crestWidth: 0.0625))
28+
#expect(left.leftHanded == true)
29+
}
30+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import Testing
2+
@testable import Helical
3+
import Cadova
4+
5+
private func isClose(_ a: Double, _ b: Double, tolerance: Double = 1e-9) -> Bool { abs(a - b) < tolerance }
6+
7+
// These tests verify that factory methods wire geometry correctly:
8+
// the table-sourced topDiameter feeds through the countersink height formula,
9+
// so a wrong table value or a wrong boltDiameter argument produces a wrong height.
10+
11+
struct HexSocketCountersunkTests {
12+
// boltDiameter = thread.majorDiameter (not majorDiameter - depth)
13+
@Test func `head height uses major diameter as bolt diameter`() {
14+
let m4 = Bolt.hexSocketCountersunk(.m4, length: 20)
15+
let m10 = Bolt.hexSocketCountersunk(.m10, length: 30)
16+
// topDiameter=7.53, boltDiameter=4.0 → (7.53-4.0)/2 * tan(45°) ≈ 1.765
17+
#expect(isClose(m4.headShape.height, 1.765, tolerance: 1e-4))
18+
// topDiameter=19.22, boltDiameter=10.0 → (19.22-10.0)/2 ≈ 4.61
19+
#expect(isClose(m10.headShape.height, 4.61, tolerance: 1e-4))
20+
}
21+
}
22+
23+
struct TorxCountersunkTests {
24+
// boltDiameter = majorDiameter - depth (not majorDiameter)
25+
@Test func `head height uses reduced bolt diameter`() {
26+
let m3 = Bolt.torxCountersunk(.m3, length: 20)
27+
let m8 = Bolt.torxCountersunk(.m8, length: 30)
28+
#expect(isClose(m3.headShape.height, (5.5 - (ScrewThread.m3.majorDiameter - ScrewThread.m3.depth)) / 2, tolerance: 1e-6))
29+
#expect(isClose(m8.headShape.height, (15.8 - (ScrewThread.m8.majorDiameter - ScrewThread.m8.depth)) / 2, tolerance: 1e-6))
30+
}
31+
}
32+
33+
struct SlottedCountersunkTests {
34+
// Same reduced boltDiameter convention as Torx
35+
@Test func `head height uses reduced bolt diameter`() {
36+
let m3 = Bolt.slottedCountersunk(.m3, length: 20)
37+
let m10 = Bolt.slottedCountersunk(.m10, length: 30)
38+
#expect(isClose(m3.headShape.height, (5.6 - (ScrewThread.m3.majorDiameter - ScrewThread.m3.depth)) / 2, tolerance: 1e-6))
39+
#expect(isClose(m10.headShape.height, (18.0 - (ScrewThread.m10.majorDiameter - ScrewThread.m10.depth)) / 2, tolerance: 1e-6))
40+
}
41+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import Testing
2+
import Helical
3+
import Cadova
4+
5+
private func isClose(_ a: Double, _ b: Double, tolerance: Double = 1e-9) -> Bool { abs(a - b) < tolerance }
6+
7+
struct TrapezoidalThreadformTests {
8+
@Test func `angle is sum of both flank angles`() {
9+
let symmetric = TrapezoidalThreadform(angle: 60°, crestWidth: 0.0625)
10+
#expect(symmetric.angle == 60°)
11+
12+
let asymmetric = TrapezoidalThreadform(leadingFlankAngle: 7°, trailingFlankAngle: 45°, crestWidth: 0.1)
13+
#expect(asymmetric.angle == 52°)
14+
}
15+
16+
@Test func `pitch diameter formula`() {
17+
// M3: major=3.0 + 2*(crestWidth - pitch/2)/(2*tan30°) ≈ 2.6752
18+
let formM3 = TrapezoidalThreadform(angle: 60°, crestWidth: ScrewThread.m3.pitch / 8)
19+
#expect(isClose(formM3.pitchDiameter(for: .m3), 2.6752, tolerance: 1e-4))
20+
21+
// M8: same formula ≈ 7.1881
22+
let formM8 = TrapezoidalThreadform(angle: 60°, crestWidth: ScrewThread.m8.pitch / 8)
23+
#expect(isClose(formM8.pitchDiameter(for: .m8), 7.1881, tolerance: 1e-4))
24+
}
25+
26+
@Test func `minimum pitch formula`() {
27+
// M3: crestWidth + depth*(tan30°+tan30°) = 0.0625 + 0.270633*2/√3 ≈ 0.375
28+
let form = TrapezoidalThreadform(angle: 60°, crestWidth: ScrewThread.m3.pitch / 8)
29+
#expect(isClose(form.minimumPitch(for: .m3), 0.375, tolerance: 1e-6))
30+
}
31+
}
32+
33+
struct KnuckleThreadformTests {
34+
@Test func `minimum pitch is 2 × crest radius`() {
35+
// E27: h=1.25, pitch=3.62 → radius=(1.5625+3.2761)/5 ≈ 0.96772
36+
// minimumPitch = 2*radius ≈ 1.93544
37+
let e27 = ScrewThread.e27
38+
let h = (e27.majorDiameter - e27.minorDiameter) / 2
39+
let r = (h * h + e27.pitch * e27.pitch / 4) / (4 * h)
40+
let form: KnuckleThreadform = .knuckle(crestRadius: r, rootRadius: r)
41+
#expect(isClose(form.minimumPitch(for: e27), 2 * r))
42+
#expect(isClose(form.minimumPitch(for: e27), 1.93544, tolerance: 1e-4))
43+
}
44+
45+
@Test func `pitch diameter formula`() {
46+
#expect(isClose(ScrewThread.e27.pitchDiameter, 25.75, tolerance: 1e-3))
47+
}
48+
}

0 commit comments

Comments
 (0)