krz/moonrise

Night-sky companion for iPhone, Apple Watch and Vision Pro: what's up tonight, the Moon, planets, launches and missions, and a Library of the solar system. SwiftUI app with a Cloudflare Workers backend. astronomy cloudflare-workers ios night-sky space swiftui visionos watchos

Packages/MoonriseKit/Tests/SkyEngineTests/EclipseTimelineTests.swift

623ba8919c3beb07ea7cc2bd76bb989ab0e93c25
moonrise/Packages/MoonriseKit/Tests/SkyEngineTests/EclipseTimelineTests.swift history · blame · raw

84 lines · 3781 bytes

 1import Foundation
 2import Testing
 3@testable import SkyEngine
 4
 5private let peak2027 = Date(timeIntervalSince1970: 1_817_201_220)
 6private let tarifa = Observer(latitude: 36.0143, longitude: -5.6044)
 7
 8private func timeline(_ observer: Observer, accuracy: Double?) throws -> EclipseTimeline {
 9    try EclipseTimeline(try #require(LocalEclipse.at(observer, near: peak2027)), observer: observer, accuracy: accuracy)
10}
11
12private func offCues(_ t: EclipseTimeline) -> [EclipseTimeline.Cue] { t.cues.filter { $0.text.localizedCaseInsensitiveContains("glasses off") } }
13
14@Test func glassesOffOnlyAtSecondContactWithAPreciseFix() throws {
15    let t = try timeline(tarifa, accuracy: 5)
16    #expect(t.glassesOffWithheld == nil)
17    let off = offCues(t)
18    #expect(off.count == 1)
19    #expect(off.first?.date == t.eclipse.centralBegin)
20    #expect(off.first?.text.contains("last bead") == true)
21}
22
23@Test func glassesOnTenSecondsBeforeThirdContact() throws {
24    for accuracy in [5.0, 500, nil] {
25        let t = try timeline(tarifa, accuracy: accuracy)
26        let c3 = try #require(t.eclipse.centralEnd)
27        let on = t.cues.filter { $0.text.hasPrefix("Glasses on") }
28        #expect(on.map(\.date) == [c3.addingTimeInterval(-10)])
29        #expect(on.first?.warning == true)
30    }
31}
32
33@Test func noGlassesOffWithoutAPreciseFix() throws {
34    let coarse = try timeline(tarifa, accuracy: 100)
35    #expect(offCues(coarse).isEmpty)
36    #expect(coarse.glassesOffWithheld == .imprecise)
37    let city = try timeline(tarifa, accuracy: nil)
38    #expect(offCues(city).isEmpty)
39    #expect(city.glassesOffWithheld == .imprecise)
40}
41
42@Test func noGlassesOffForShortTotality() throws {
43    var e = try #require(LocalEclipse.at(tarifa, near: peak2027))
44    e.centralEnd = e.centralBegin?.addingTimeInterval(29.9)
45    let t = try EclipseTimeline(e, observer: tarifa, accuracy: 5)
46    #expect(offCues(t).isEmpty)
47    #expect(t.glassesOffWithheld == .short)
48}
49
50@Test func partialSiteKeepsGlassesOnThroughout() throws {
51    let seville = Observer(latitude: 37.3891, longitude: -5.9845) // 98% covered, no totality
52    let t = try timeline(seville, accuracy: 5)
53    #expect(t.eclipse.kind == "Partial")
54    #expect(offCues(t).isEmpty)
55    #expect(t.glassesOffWithheld == .partial)
56    #expect(t.cues.first?.text.contains("keep your glasses on the whole time") == true)
57}
58
59@Test func scriptHasShadowBandAndBeadCuesInOrder() throws {
60    let t = try timeline(tarifa, accuracy: 5)
61    let c2 = try #require(t.eclipse.centralBegin)
62    #expect(t.cues.contains { $0.date == c2.addingTimeInterval(-120) && $0.text.contains("shadow bands") })
63    #expect(t.cues.contains { $0.date == c2.addingTimeInterval(-15) && $0.text.contains("Baily's beads") })
64    #expect(t.cues.map(\.date) == t.cues.map(\.date).sorted())
65    #expect(t.cues.first?.date == t.eclipse.partialBegin && t.cues.last?.date == t.eclipse.partialEnd)
66}
67
68@Test func rehearsalStartsThreeMinutesBeforeTotalityOrMaximum() throws {
69    let t = try timeline(tarifa, accuracy: 5)
70    #expect(t.rehearsalStart == t.eclipse.centralBegin?.addingTimeInterval(-180))
71    let madrid = try timeline(Observer(latitude: 40.42, longitude: -3.70), accuracy: 5)
72    #expect(madrid.rehearsalStart == madrid.eclipse.peak.addingTimeInterval(-180))
73}
74
75@Test func sunAtEachContact() throws {
76    let t = try timeline(tarifa, accuracy: 5)
77    #expect(t.contacts.map(\.name) == ["C1", "C2", "Max", "C3", "C4"])
78    let c2 = t.contacts[1].sun
79    // Tarifa at totality: hour angle −56°, declination +17.8°, so about 38° up and just south of east.
80    #expect(abs(c2.altitude - 38) < 2)
81    #expect(c2.azimuth > 92 && c2.azimuth < 98)
82    let path = try t.sunPath(every: 300)
83    #expect(path.first?.date == t.eclipse.partialBegin && path.last?.date == t.eclipse.partialEnd)
84}