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/ObservingLogTests/ObservingLogTests.swift

6c4b28384d65e09486e0ad62f2ac9d10401cc80c
moonrise/Packages/MoonriseKit/Tests/ObservingLogTests/ObservingLogTests.swift history · blame · raw

144 lines · 7164 bytes

  1import Foundation
  2import Testing
  3@testable import ObservingLog
  4
  5let when = Date(timeIntervalSince1970: 1_792_202_400) // 2026-10-17T02:00:00Z
  6
  7@Test func orgRoundTrip() throws {
  8    let o = Observation(object: "Saturn", date: when, place: "Chicago", latitude: 41.8781, longitude: -87.6298, equipment: "10x50 binoculars", seeing: 3, notes: "Rings nearly edge-on.\n\nTitan visible to the east.")
  9    let text = o.org
 10    #expect(text.hasPrefix("* Saturn\n["))
 11    #expect(text.contains(":EQUIPMENT: 10x50 binoculars"))
 12    #expect(text.contains(":SEEING: 3/5"))
 13    let back = try #require(Observation(org: text))
 14    #expect(back.id == o.id)
 15    #expect(back.object == "Saturn")
 16    #expect(abs(back.date.timeIntervalSince(when)) < 60)
 17    #expect(back.latitude == 41.8781)
 18    #expect(back.seeing == 3)
 19    #expect(back.notes == o.notes)
 20}
 21
 22@Test func minimalEntryRoundTrips() throws {
 23    let o = Observation(object: "M31", date: when, place: "Backyard")
 24    let back = try #require(Observation(org: o.org))
 25    #expect(back.equipment == "" && back.seeing == nil && back.notes == "")
 26}
 27
 28@Test func folderSaveListDelete() throws {
 29    let dir = FileManager.default.temporaryDirectory.appending(path: "obs-\(UUID().uuidString)")
 30    defer { try? FileManager.default.removeItem(at: dir) }
 31    let log = ObservingLog(folder: dir)
 32    var a = Observation(object: "Moon", date: when, place: "Chicago")
 33    let b = Observation(object: "Jupiter", date: when.addingTimeInterval(3600), place: "Chicago")
 34    try log.save(a)
 35    try log.save(b)
 36    #expect(log.all().map(\.object) == ["Jupiter", "Moon"])
 37    a.notes = "Earthshine."
 38    try log.save(a)
 39    #expect(log.all().count == 2)
 40    #expect(log.all().first { $0.id == a.id }?.notes == "Earthshine.")
 41    try log.delete(id: b.id)
 42    #expect(log.all().map(\.object) == ["Moon"])
 43    #expect(log.fileURL(for: a).lastPathComponent.hasSuffix("-moon.org"))
 44}
 45
 46@Test func equipmentIDsRoundTrip() throws {
 47    let o = Observation(object: "M13", date: when, place: "Backyard", equipment: "8-inch Dobsonian, 25 mm Plössl", equipmentIDs: ["scope-1", "ep-2"])
 48    #expect(o.org.contains(":EQUIPMENT: 8-inch Dobsonian, 25 mm Plössl\n:EQUIPMENT_IDS: scope-1 ep-2\n"))
 49    let back = try #require(Observation(org: o.org))
 50    #expect(back.equipment == o.equipment)
 51    #expect(back.equipmentIDs == ["scope-1", "ep-2"])
 52}
 53
 54@Test func freeTextEquipmentStillParses() throws {
 55    let text = "* Saturn\n[2026-10-16 Fri 21:00]\n:PROPERTIES:\n:ID: abc\n:EQUIPMENT: 10x50 binoculars\n:END:\n"
 56    let o = try #require(Observation(org: text))
 57    #expect(o.equipment == "10x50 binoculars")
 58    #expect(o.equipmentIDs.isEmpty)
 59}
 60
 61@Test func equipmentOrgRoundTrip() throws {
 62    let scope = Equipment(kind: .telescope, name: "8-inch Dobsonian", aperture: 203, focalLength: 1200, notes: "Collimated in May.")
 63    let text = scope.org
 64    #expect(text.hasPrefix("* 8-inch Dobsonian\n:PROPERTIES:\n"))
 65    #expect(text.contains(":TYPE: telescope\n"))
 66    #expect(text.contains(":APERTURE: 203 mm\n"))
 67    #expect(text.contains(":FOCAL_LENGTH: 1200 mm\n"))
 68    #expect(text.contains(":FOCAL_RATIO: f/5.9\n"))
 69    #expect(!text.contains(":RETIRED:"))
 70    let back = try #require(Equipment(org: text))
 71    #expect(back == scope)
 72
 73    let binos = Equipment(kind: .binoculars, name: "10x50", aperture: 50, magnification: 10, fieldOfView: 6.5, retired: true)
 74    #expect(binos.org.contains(":MAGNIFICATION: 10x\n"))
 75    #expect(binos.org.contains(":FIELD_OF_VIEW: 6.5°\n"))
 76    #expect(binos.org.contains(":RETIRED: t\n"))
 77    #expect(Equipment(org: binos.org) == binos)
 78
 79    let cam = Equipment(kind: .camera, name: "ZWO", sensor: "IMX585")
 80    #expect(Equipment(org: cam.org) == cam)
 81}
 82
 83@Test func inventorySaveListDelete() throws {
 84    let file = FileManager.default.temporaryDirectory.appending(path: "eq-\(UUID().uuidString)/Equipment.org")
 85    defer { try? FileManager.default.removeItem(at: file.deletingLastPathComponent()) }
 86    let inv = EquipmentInventory(file: file)
 87    #expect(inv.all().isEmpty)
 88    var scope = Equipment(kind: .telescope, name: "Dob", aperture: 203, notes: "Line one.\n\nLine two.")
 89    let ep = Equipment(kind: .eyepiece, name: "25 mm Plössl", focalLength: 25, fieldOfView: 52)
 90    try inv.save(scope)
 91    try inv.save(ep)
 92    #expect(inv.all() == [scope, ep])
 93    scope.retired = true
 94    try inv.save(scope)
 95    #expect(inv.all() == [scope, ep])
 96    #expect(try String(contentsOf: file, encoding: .utf8).hasPrefix("#+title: Equipment\n"))
 97    try inv.delete(id: scope.id)
 98    #expect(inv.all() == [ep])
 99}
100
101@Test func equipmentUsage() throws {
102    var cal = Calendar(identifier: .gregorian)
103    cal.timeZone = TimeZone(identifier: "UTC")!
104    func day(_ y: Int, _ m: Int, _ d: Int) -> Date { cal.date(from: DateComponents(year: y, month: m, day: d, hour: 3))! }
105    let log = [
106        Observation(object: "Saturn", date: day(2026, 1, 10), place: "", equipmentIDs: ["scope", "ep25"], seeing: 4),
107        Observation(object: "saturn ", date: day(2026, 7, 10), place: "", equipmentIDs: ["scope", "ep25"], seeing: 2),
108        Observation(object: "M31", date: day(2026, 10, 10), place: "", equipmentIDs: ["scope", "ep10"]),
109        Observation(object: "Moon", date: day(2026, 10, 11), place: "", latitude: -33.9, longitude: 18.4, equipmentIDs: ["scope"], seeing: 3),
110        Observation(object: "Jupiter", date: day(2026, 3, 1), place: "", equipmentIDs: ["binos"]),
111        Observation(object: "Mars", date: day(2026, 3, 2), place: "", equipment: "Telescope"),
112    ]
113    let u = EquipmentUsage(of: "scope", in: log, calendar: cal)
114    #expect(u.observations == 4)
115    #expect(u.first == day(2026, 1, 10))
116    #expect(u.last == day(2026, 10, 11))
117    #expect(u.distinctObjects == 3)
118    #expect(u.topObjects.first == .init(key: "Saturn", count: 2))
119    #expect(u.byMonth == [1, 0, 0, 0, 0, 0, 1, 0, 0, 2, 0, 0])
120    #expect(u.bySeason == [.winter: 1, .summer: 1, .autumn: 1, .spring: 1])
121    #expect(u.averageSeeing == 3)
122    #expect(u.usedWith == [.init(key: "ep25", count: 2), .init(key: "ep10", count: 1)])
123
124    let none = EquipmentUsage(of: "camera", in: log, calendar: cal)
125    #expect(none.observations == 0 && none.first == nil && none.averageSeeing == nil && none.usedWith.isEmpty)
126}
127
128@Test func observationKeepsHandAddedProperties() throws {
129    let text = "* Saturn\n[2026-10-16 Fri 21:00]\n:PROPERTIES:\n:ID: abc\n:TRANSPARENCY: 4/5\n:PLACE: Chicago\n:CATEGORY: planet\n:BORTLE:\n:END:\n\nRings.\n"
130    var o = try #require(Observation(org: text))
131    o.notes = "Rings and Titan."
132    let out = o.org
133    #expect(out.contains(":PLACE: Chicago\n:TRANSPARENCY: 4/5\n:CATEGORY: planet\n:BORTLE:\n:END:\n"))
134    #expect(Observation(org: out) == o)
135}
136
137@Test func equipmentKeepsHandAddedProperties() throws {
138    let text = "* Dob\n:PROPERTIES:\n:ID: scope-1\n:TYPE: telescope\n:PURCHASED: 2019-05-02\n:APERTURE: 203 mm\n:FOCAL_RATIO: f/5.9\n:SERIAL: X123\n:END:\n"
139    var scope = try #require(Equipment(org: text))
140    scope.focalLength = 1200
141    let out = scope.org
142    #expect(out.contains(":FOCAL_RATIO: f/5.9\n:PURCHASED: 2019-05-02\n:SERIAL: X123\n:END:\n"))
143    #expect(Equipment(org: out) == scope)
144}