Packages/MoonriseKit/Tests/FeedsTests/SunSummaryTests.swift
121 lines · 5512 bytes
1import Foundation
2import Testing
3@testable import Feeds
4
5private func at(_ s: String) -> Date { try! Date(s, strategy: .iso8601) }
6
7private let now = at("2026-10-19T15:00:00Z")
8private let night = DateInterval(start: at("2026-10-19T23:00:00Z"), end: at("2026-10-20T11:00:00Z"))
9/// Chicago is at about 50.7° geomagnetic.
10private let chicago = 50.7
11
12private func scales(r: Int = 0, s: Int = 0, g: Int = 0) -> NOAAScales {
13 NOAAScales(asOf: at("2026-10-19T14:55:00Z"), current: .init(r: r, s: s, g: g), past24h: .init(r: r, s: s, g: g))
14}
15
16private let probabilities = SolarProbabilities(
17 issued: at("2026-10-19T00:00:00Z"),
18 days: [.init(c: 85, m: 25, x: 5, proton: 5), .init(c: 80, m: 20, x: 5, proton: 5), .init(c: 80, m: 20, x: 5, proton: 5)]
19)
20
21private let quietKp = [
22 KpBar(start: at("2026-10-19T12:00:00Z"), kp: 2.33, predicted: false),
23 KpBar(start: at("2026-10-20T00:00:00Z"), kp: 3.0, predicted: true),
24]
25
26private let quietXRay = [XRaySample(time: at("2026-10-19T14:00:00Z"), flux: 3e-6)]
27
28private func summary(
29 scales s: NOAAScales? = scales(), kp: [KpBar] = quietKp, xray: [XRaySample] = quietXRay,
30 latitude: Double = chicago, night n: DateInterval = night, at date: Date = now
31) -> String? {
32 SunSummary.text(
33 scales: s, kp: kp, probabilities: probabilities, xray: xray,
34 geomagneticLatitude: latitude, night: n, at: date,
35 timeZone: TimeZone(identifier: "UTC")!, locale: Locale(identifier: "en_GB")
36 )
37}
38
39@Test func quietNamesTodaysFlareChance() {
40 #expect(summary() == "Quiet. 25% chance of an M-class flare today.")
41}
42
43@Test func quietWithoutProbabilities() {
44 let text = SunSummary.text(
45 scales: scales(), kp: quietKp, probabilities: nil, xray: quietXRay,
46 geomagneticLatitude: chicago, night: night, at: now,
47 timeZone: TimeZone(identifier: "UTC")!, locale: Locale(identifier: "en_GB")
48 )
49 #expect(text == "Quiet. No storms, and no flare of class M or above in the last 6 hours.")
50}
51
52@Test func nothingLoadedGivesNoSummary() {
53 #expect(summary(scales: nil, kp: [], xray: []) == nil)
54}
55
56@Test func stormForecastAfterDarkAtYourLatitude() {
57 let kp = quietKp + [KpBar(start: at("2026-10-20T03:00:00Z"), kp: 5.33, predicted: true)]
58 #expect(summary(kp: kp) == "Minor storm (G1) forecast for 3:00–6:00, Kp 5.3. Aurora possible low in the northern sky.")
59}
60
61@Test func stormNowAndDark() {
62 let kp = [KpBar(start: at("2026-10-20T00:00:00Z"), kp: 6.67, predicted: true)]
63 let text = summary(scales: scales(g: 2), kp: kp, at: at("2026-10-20T01:00:00Z"))
64 #expect(text == "Moderate storm (G2) now, Kp 6.7. Aurora possible low in the northern sky.")
65}
66
67@Test func stormUnderwayBeforeDarkRunsIntoTheNight() {
68 let kp = [KpBar(start: at("2026-10-19T21:00:00Z"), kp: 5.0, predicted: true)]
69 let text = summary(kp: kp, at: at("2026-10-19T21:30:00Z"))
70 #expect(text == "Minor storm (G1) forecast until 0:00, Kp 5.0. Aurora possible low in the northern sky.")
71}
72
73@Test func southernObserverLooksSouth() {
74 let kp = [KpBar(start: at("2026-10-20T03:00:00Z"), kp: 5.33, predicted: true)]
75 #expect(summary(kp: kp, latitude: -55)?.hasSuffix("Aurora possible low in the southern sky.") == true)
76}
77
78@Test func stormTooFarFromThePoleIsASecondClause() {
79 // At 40° geomagnetic, Kp 5.3 does not reach: the oval's visible edge is at 49.6°.
80 let kp = quietKp + [KpBar(start: at("2026-10-20T03:00:00Z"), kp: 5.33, predicted: true)]
81 #expect(summary(kp: kp, latitude: 40) == "Quiet now. Minor storm (G1) forecast from 3:00, Kp 5.3.")
82}
83
84@Test func daytimeStormIsASecondClause() {
85 let kp = quietKp + [KpBar(start: at("2026-10-19T18:00:00Z"), kp: 5.33, predicted: true)]
86 #expect(summary(kp: kp) == "Quiet now. Minor storm (G1) forecast from 18:00, Kp 5.3.")
87}
88
89@Test func stormNowOutOfReach() {
90 #expect(summary(scales: scales(g: 1), latitude: 40) == "Minor storm (G1) as of 14:55. Aurora at your latitude would need more than Kp 9.")
91 #expect(summary(scales: scales(g: 1), latitude: 45) == "Minor storm (G1) as of 14:55. Aurora at your latitude needs Kp 7.6.")
92}
93
94@Test func stormNowInReachButOverBeforeDark() {
95 #expect(summary(scales: scales(g: 1)) == "Minor storm (G1) as of 14:55. Kp is forecast below 5 after dark.")
96}
97
98@Test func radiationStorm() {
99 #expect(summary(scales: scales(s: 1)) == "Minor radiation storm (S1) as of 14:55. It affects polar flights and satellites, not people on the ground.")
100}
101
102@Test func recentFlare() {
103 let xray = quietXRay + [XRaySample(time: at("2026-10-19T14:05:00Z"), flux: 1.2e-4)]
104 #expect(summary(xray: xray) == "Strong flare (X1.2) at 14:05. Brief radio blackout on Earth's day side.")
105}
106
107@Test func flareOlderThanSixHoursIsQuiet() {
108 let xray = quietXRay + [XRaySample(time: at("2026-10-19T08:30:00Z"), flux: 2e-5)]
109 #expect(summary(xray: xray) == "Quiet. 25% chance of an M-class flare today.")
110}
111
112@Test func radioBlackoutWithoutXRayData() {
113 #expect(summary(scales: scales(r: 3), xray: []) == "Strong radio blackout (R3) as of 14:55. It affects radio on Earth's day side.")
114}
115
116@Test func stormOutranksRadiationAndFlare() {
117 let xray = [XRaySample(time: at("2026-10-19T14:05:00Z"), flux: 5e-5)]
118 #expect(summary(scales: scales(r: 2, s: 1, g: 1), xray: xray)?.hasPrefix("Minor storm (G1)") == true)
119 #expect(summary(scales: scales(r: 2, s: 1), xray: xray)?.hasPrefix("Minor radiation storm (S1)") == true)
120 #expect(summary(scales: scales(r: 2), xray: xray)?.hasPrefix("Moderate flare (M5.0)") == true)
121}