diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 555e236..b219a92 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,11 +2,14 @@ # # Category A (tools-version 6.2 + macOS 26 min): macos-26 + Swift 6.2 # Category B (tools-version 6.2 + older macOS): macos-15 + 6.2, macos-26 + 6.2 -# Category C (tools-version 6.0): macos-15 + 6.0, macos-15 + 6.2, macos-26 + 6.2 +# Category C (tools-version 6.3): macos-15 + 6.3, macos-26 + 6.3 # Linux: ubuntu + Swift 6.3 # -# When Swift 6.3 ships: bump 6.0→6.1 and 6.2→6.3 in Category C -# When bumping tools-version to 6.2: drop 6.0/6.1, move to Category A or B +# This package is Category A, plus a Linux leg on Swift 6.3. Test names are raw +# identifiers (SE-0451), so a leg must be on Swift 6.2 or newer to run `swift test`; +# a leg on an older compiler is limited to `swift build -v`. +# +# When Swift 6.4 ships: add 6.4 legs alongside 6.2 name: CI diff --git a/Package.swift b/Package.swift index 8eb943e..2701820 100644 --- a/Package.swift +++ b/Package.swift @@ -3,9 +3,13 @@ import PackageDescription -let approachableConcurrency: [SwiftSetting] = [ +let upcomingFeatures: [SwiftSetting] = [ .enableUpcomingFeature("NonisolatedNonsendingByDefault"), - .enableUpcomingFeature("InferIsolatedConformances") + .enableUpcomingFeature("InferIsolatedConformances"), + .enableUpcomingFeature("ImmutableWeakCaptures"), + .enableUpcomingFeature("MemberImportVisibility"), + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("InternalImportsByDefault") ] let package = Package( @@ -31,12 +35,12 @@ let package = Package( .target( name: "SwiftDOF", resources: [.process("Resources")], - swiftSettings: approachableConcurrency + swiftSettings: upcomingFeatures ), .testTarget( name: "SwiftDOFTests", dependencies: ["SwiftDOF"], - swiftSettings: approachableConcurrency + swiftSettings: upcomingFeatures ) ], swiftLanguageModes: [.v5, .v6] @@ -51,6 +55,6 @@ package.targets.append( .product(name: "ZIPFoundation", package: "ZIPFoundation"), .product(name: "Progress", package: "Progress.swift") ], - swiftSettings: approachableConcurrency + swiftSettings: upcomingFeatures ) ) diff --git a/Sources/SwiftDOF/Cycle.swift b/Sources/SwiftDOF/Cycle.swift index 234ad72..e087ea9 100644 --- a/Sources/SwiftDOF/Cycle.swift +++ b/Sources/SwiftDOF/Cycle.swift @@ -1,4 +1,4 @@ -import Foundation +public import Foundation /// Represents a DOF publication cycle. /// diff --git a/Sources/SwiftDOF/DOF.swift b/Sources/SwiftDOF/DOF.swift index 49c4533..2bc231b 100644 --- a/Sources/SwiftDOF/DOF.swift +++ b/Sources/SwiftDOF/DOF.swift @@ -1,4 +1,4 @@ -import Foundation +public import Foundation /// Container for DOF obstacle data. /// @@ -36,7 +36,7 @@ public struct DOF: Sendable, Codable { public init( data: Data, progressHandler: @Sendable (Progress) -> Void = { _ in }, - errorCallback: ((Error, Int) -> Void)? = nil + errorCallback: ((any Error, Int) -> Void)? = nil ) throws { var obstacles: [String: Obstacle] = [:] obstacles.reserveCapacity(Self.estimatedObstacleCount) @@ -83,7 +83,7 @@ public struct DOF: Sendable, Codable { public init( url: URL, progressHandler: @Sendable (Progress) -> Void = { _ in }, - errorCallback: ((Error, Int) -> Void)? = nil + errorCallback: ((any Error, Int) -> Void)? = nil ) async throws { var obstacles: [String: Obstacle] = [:] obstacles.reserveCapacity(Self.estimatedObstacleCount) @@ -143,7 +143,7 @@ public struct DOF: Sendable, Codable { bytes: S, totalBytes: Int64? = nil, progressHandler: @Sendable (Progress) -> Void = { _ in }, - errorCallback: ((Error, Int) -> Void)? = nil + errorCallback: ((any Error, Int) -> Void)? = nil ) async throws where S.Element == UInt8, S: Sendable { var obstacles: [String: Obstacle] = [:] obstacles.reserveCapacity(Self.estimatedObstacleCount) @@ -186,7 +186,7 @@ public struct DOF: Sendable, Codable { lineNumber: Int, cycle: inout Cycle?, obstacles: inout [String: Obstacle], - errorCallback: ((Error, Int) -> Void)? + errorCallback: ((any Error, Int) -> Void)? ) throws { // Line 1: Parse currency date if lineNumber == 1 { @@ -225,7 +225,7 @@ public struct DOF: Sendable, Codable { public static func from( filePath: URL, progressHandler: @Sendable (Progress) -> Void = { _ in }, - errorCallback: ((Error, Int) -> Void)? = nil + errorCallback: ((any Error, Int) -> Void)? = nil ) throws -> Self { let data = try Data(contentsOf: filePath) return try Self(data: data, progressHandler: progressHandler, errorCallback: errorCallback) @@ -243,7 +243,7 @@ public struct DOF: Sendable, Codable { public static func from( data: Data, progressHandler: @Sendable (Progress) -> Void = { _ in }, - errorCallback: ((Error, Int) -> Void)? = nil + errorCallback: ((any Error, Int) -> Void)? = nil ) throws -> Self { try Self(data: data, progressHandler: progressHandler, errorCallback: errorCallback) } @@ -260,7 +260,7 @@ public struct DOF: Sendable, Codable { public static func from( url: URL, progressHandler: @Sendable (Progress) -> Void = { _ in }, - errorCallback: ((Error, Int) -> Void)? = nil + errorCallback: ((any Error, Int) -> Void)? = nil ) async throws -> Self { try await Self(url: url, progressHandler: progressHandler, errorCallback: errorCallback) } diff --git a/Sources/SwiftDOF/Obstacle.swift b/Sources/SwiftDOF/Obstacle.swift index 15455a6..f657e9d 100644 --- a/Sources/SwiftDOF/Obstacle.swift +++ b/Sources/SwiftDOF/Obstacle.swift @@ -1,7 +1,7 @@ -import Foundation +public import Foundation #if canImport(CoreLocation) - import CoreLocation + public import CoreLocation #endif /// Represents a single obstacle from the FAA Digital Obstacle File. diff --git a/Sources/SwiftDOF/Parser/ByteParsing.swift b/Sources/SwiftDOF/Parser/ByteParsing.swift index 289aa4f..5fd37ce 100644 --- a/Sources/SwiftDOF/Parser/ByteParsing.swift +++ b/Sources/SwiftDOF/Parser/ByteParsing.swift @@ -1,4 +1,4 @@ -import Foundation +public import Foundation /// Extensions for parsing numeric values directly from ASCII byte sequences. extension RandomAccessCollection where Element == UInt8, Index == Int { diff --git a/Sources/SwiftDOF/Parser/DOFError.swift b/Sources/SwiftDOF/Parser/DOFError.swift index 43ad06e..a1ee3de 100644 --- a/Sources/SwiftDOF/Parser/DOFError.swift +++ b/Sources/SwiftDOF/Parser/DOFError.swift @@ -1,4 +1,4 @@ -import Foundation +public import Foundation /// Specific format errors that can occur during DOF parsing. public enum DOFFormatError: Sendable { @@ -36,7 +36,7 @@ public enum DOFError: Error, LocalizedError, Sendable { case fileNotFound(URL) /// An error occurred while reading the stream. - case streamError(Error) + case streamError(any Error) /// The line is too short to parse. case lineTooShort(expected: Int, actual: Int, line: Int) diff --git a/Sources/SwiftDOF/Types/AccuracyCategory.swift b/Sources/SwiftDOF/Types/AccuracyCategory.swift index aee1e22..164ca93 100644 --- a/Sources/SwiftDOF/Types/AccuracyCategory.swift +++ b/Sources/SwiftDOF/Types/AccuracyCategory.swift @@ -1,4 +1,4 @@ -import Foundation +public import Foundation /// FAA horizontal accuracy category for obstacle position data. /// diff --git a/Sources/SwiftDOF_E2E/DOFDataLoader.swift b/Sources/SwiftDOF_E2E/DOFDataLoader.swift index 2ddf162..3a5cae7 100644 --- a/Sources/SwiftDOF_E2E/DOFDataLoader.swift +++ b/Sources/SwiftDOF_E2E/DOFDataLoader.swift @@ -13,7 +13,7 @@ protocol DOFDataLoader { /// - Returns: The parsed DOF data. func load( progressHandler: @Sendable (Progress) -> Void, - errorCallback: @escaping (Error, Int) -> Void + errorCallback: @escaping (any Error, Int) -> Void ) async throws -> DOF } @@ -25,7 +25,7 @@ struct FileDataLoader: DOFDataLoader { func load( progressHandler: @Sendable (Progress) -> Void, - errorCallback: @escaping (Error, Int) -> Void + errorCallback: @escaping (any Error, Int) -> Void ) throws -> DOF { let data: Data if url.pathExtension.lowercased() == "zip" { @@ -46,7 +46,7 @@ struct URLZipLoader: DOFDataLoader { func load( progressHandler: @Sendable (Progress) -> Void, - errorCallback: @escaping (Error, Int) -> Void + errorCallback: @escaping (any Error, Int) -> Void ) async throws -> DOF { let (downloadedData, response) = try await URLSession.shared.data(from: url) @@ -84,7 +84,7 @@ struct URLStreamLoader: DOFDataLoader { #else func load( progressHandler: @Sendable (Progress) -> Void, - errorCallback: @escaping (Error, Int) -> Void + errorCallback: @escaping (any Error, Int) -> Void ) async throws -> DOF { let (bytes, response) = try await URLSession.shared.bytes(from: url) diff --git a/Sources/SwiftDOF_E2E/SwiftDOF_E2E.swift b/Sources/SwiftDOF_E2E/SwiftDOF_E2E.swift index 614ede8..9d87e91 100644 --- a/Sources/SwiftDOF_E2E/SwiftDOF_E2E.swift +++ b/Sources/SwiftDOF_E2E/SwiftDOF_E2E.swift @@ -71,7 +71,7 @@ struct SwiftDOF_E2E: AsyncParsableCommand { errorCallback: { error, line in errorCount += 1 var message = "Error at line \(line): \(error.localizedDescription)" - if let reason = (error as? LocalizedError)?.failureReason { + if let reason = (error as? (any LocalizedError))?.failureReason { message += "\n - \(reason)" } FileHandle.standardError.write(Data("\(message)\n".utf8)) @@ -95,7 +95,7 @@ struct SwiftDOF_E2E: AsyncParsableCommand { try formatter.format(dof: dof, errorCount: errorCount, elapsed: elapsed, to: stdout) } - private func makeLoader(for url: URL) -> DOFDataLoader { + private func makeLoader(for url: URL) -> any DOFDataLoader { if url.isFileURL { return FileDataLoader(url: url) } @@ -105,7 +105,7 @@ struct SwiftDOF_E2E: AsyncParsableCommand { return URLStreamLoader(url: url) } - private func makeFormatter(for format: OutputFormat) -> OutputFormatter { + private func makeFormatter(for format: OutputFormat) -> any OutputFormatter { switch format { case .summary: return SummaryOutputFormatter() case .json: return JSONOutputFormatter() diff --git a/Tests/SwiftDOFTests/CycleTests.swift b/Tests/SwiftDOFTests/CycleTests.swift index 67db9af..a91cd40 100644 --- a/Tests/SwiftDOFTests/CycleTests.swift +++ b/Tests/SwiftDOFTests/CycleTests.swift @@ -4,44 +4,44 @@ import Foundation struct CycleTests { - @Test("Effective cycle is valid") - func testEffectiveCycleIsValid() { + @Test + func `reports a plausible year, month, and day for the effective cycle`() { let cycle = Cycle.effective #expect(cycle.year >= 2025) #expect(cycle.month >= 1 && cycle.month <= 12) #expect(cycle.day >= 1 && cycle.day <= 31) } - @Test("Datum cycle (Sep 1, 2025) is valid") - func testDatumCycleIsValid() { + @Test + func `treats the datum cycle of Sep 1, 2025 as valid`() { let cycle = Cycle(year: 2025, month: 9, day: 1) #expect(cycle.isValid) #expect(cycle.id == "20250901") } - @Test("Second cycle (Oct 27, 2025) is valid") - func testSecondCycleIsValid() { + @Test + func `treats the second cycle of Oct 27, 2025 as valid`() { let cycle = Cycle(year: 2025, month: 10, day: 27) #expect(cycle.isValid) #expect(cycle.id == "20251027") } - @Test("Third cycle (Dec 22, 2025) is valid") - func testThirdCycleIsValid() { + @Test + func `treats the third cycle of Dec 22, 2025 as valid`() { let cycle = Cycle(year: 2025, month: 12, day: 22) #expect(cycle.isValid) #expect(cycle.id == "20251222") } - @Test("Non-boundary date is invalid") - func testNonBoundaryDateIsInvalid() { + @Test + func `treats a date that is not a cycle boundary as invalid`() { // Sep 15 is not a cycle boundary let cycle = Cycle(year: 2025, month: 9, day: 15) #expect(!cycle.isValid) } - @Test("Previous cycle calculation") - func testPreviousCycle() throws { + @Test + func `returns the preceding cycle from previous`() throws { let cycle = Cycle(year: 2025, month: 10, day: 27) let previous = try #require(cycle.previous) #expect(previous.year == 2025) @@ -49,8 +49,8 @@ struct CycleTests { #expect(previous.day == 1) } - @Test("Next cycle calculation") - func testNextCycle() throws { + @Test + func `returns the following cycle from next`() throws { let cycle = Cycle(year: 2025, month: 9, day: 1) let next = try #require(cycle.next) #expect(next.year == 2025) @@ -58,53 +58,53 @@ struct CycleTests { #expect(next.day == 27) } - @Test("Cycle comparison - less than") - func testCycleComparisonLessThan() { + @Test + func `orders an earlier cycle before a later one`() { let older = Cycle(year: 2025, month: 9, day: 1) let newer = Cycle(year: 2025, month: 10, day: 27) #expect(older < newer) #expect(!(newer < older)) } - @Test("Cycle comparison - greater than") - func testCycleComparisonGreaterThan() { + @Test + func `orders a later cycle after an earlier one`() { let older = Cycle(year: 2025, month: 9, day: 1) let newer = Cycle(year: 2025, month: 10, day: 27) #expect(newer > older) } - @Test("Cycle equality") - func testCycleEquality() { + @Test + func `considers two cycles with the same date equal`() { let cycle1 = Cycle(year: 2025, month: 9, day: 1) let cycle2 = Cycle(year: 2025, month: 9, day: 1) #expect(cycle1 == cycle2) } - @Test("RawRepresentable round-trip") - func testRawRepresentableRoundTrip() { + @Test + func `round-trips a cycle through its raw value`() { let original = Cycle(year: 2025, month: 9, day: 1) let rawValue = original.rawValue let restored = Cycle(rawValue: rawValue) #expect(restored == original) } - @Test("RawRepresentable initialization with valid string") - func testRawRepresentableValidString() throws { + @Test + func `parses a valid raw value into year, month, and day`() throws { let cycle = try #require(Cycle(rawValue: "20251027")) #expect(cycle.year == 2025) #expect(cycle.month == 10) #expect(cycle.day == 27) } - @Test("RawRepresentable initialization with invalid string") - func testRawRepresentableInvalidString() { + @Test + func `returns nil for a malformed raw value`() { #expect(Cycle(rawValue: "invalid") == nil) #expect(Cycle(rawValue: "2025") == nil) #expect(Cycle(rawValue: "202509011") == nil) } - @Test("Cycle covering arbitrary date") - func testCycleCoveringArbitraryDate() throws { + @Test + func `finds the cycle covering a date mid-cycle`() throws { // Sep 15, 2025 should be covered by Sep 1, 2025 cycle let components = DateComponents(timeZone: .gmt, year: 2025, month: 9, day: 15) let cycle = try #require(Cycle(covering: components)) @@ -113,8 +113,8 @@ struct CycleTests { #expect(cycle.day == 1) } - @Test("Cycle covering date in second cycle") - func testCycleCoveringDateInSecondCycle() throws { + @Test + func `finds the cycle covering a date in the second cycle`() throws { // Nov 1, 2025 should be covered by Oct 27, 2025 cycle let components = DateComponents(timeZone: .gmt, year: 2025, month: 11, day: 1) let cycle = try #require(Cycle(covering: components)) @@ -123,8 +123,8 @@ struct CycleTests { #expect(cycle.day == 27) } - @Test("Cycle covering date before datum") - func testCycleCoveringDateBeforeDatum() throws { + @Test + func `finds the cycle covering a date before the datum`() throws { // Aug 15, 2025 is before datum (Sep 1), should be covered by Jul 7, 2025 (56 days before datum) let components = DateComponents(timeZone: .gmt, year: 2025, month: 8, day: 15) let cycle = try #require(Cycle(covering: components)) @@ -133,8 +133,8 @@ struct CycleTests { #expect(cycle.day == 7) } - @Test("Cycle covering date exactly on pre-datum boundary") - func testCycleCoveringDateExactlyOnPreDatumBoundary() throws { + @Test + func `finds a valid cycle for a date exactly on a pre-datum boundary`() throws { // Jul 7, 2025 is exactly 56 days before datum let components = DateComponents(timeZone: .gmt, year: 2025, month: 7, day: 7) let cycle = try #require(Cycle(covering: components)) @@ -144,8 +144,8 @@ struct CycleTests { #expect(cycle.isValid) } - @Test("Cycle ID format") - func testCycleIdFormat() { + @Test + func `formats the ID as a zero-padded year, month, and day`() { let cycle = Cycle(year: 2025, month: 9, day: 1) #expect(cycle.id == "20250901") @@ -153,14 +153,14 @@ struct CycleTests { #expect(cycle2.id == "20261215") } - @Test("Cycle description equals ID") - func testCycleDescriptionEqualsId() { + @Test + func `describes a cycle by its ID`() { let cycle = Cycle(year: 2025, month: 9, day: 1) #expect(cycle.description == cycle.id) } - @Test("isEffective returns true for effective cycle") - func testIsEffective() { + @Test + func `reports the current cycle as effective`() { let effective = Cycle.effective #expect(effective.isEffective) @@ -171,8 +171,8 @@ struct CycleTests { _ = past.isEffective } - @Test("Cycle date property returns valid date") - func testCycleDateProperty() throws { + @Test + func `returns the first date of the cycle in GMT`() throws { let cycle = Cycle(year: 2025, month: 9, day: 1) let date = try #require(cycle.firstDate) @@ -185,8 +185,8 @@ struct CycleTests { #expect(components.day == 1) } - @Test("dateRange covers full cycle") - func testDateRange() throws { + @Test + func `spans 56 days from the effective date to the expiration date`() throws { let cycle = Cycle(year: 2025, month: 9, day: 1) let dateRange = try #require(cycle.dateRange) @@ -201,8 +201,8 @@ struct CycleTests { #expect(dateRange.end == cycle.expirationDate) } - @Test("contains returns true for date within cycle") - func testContainsDateWithinCycle() throws { + @Test + func `contains a date inside the cycle`() throws { let cycle = Cycle(year: 2025, month: 9, day: 1) // Create a date in the middle of the cycle (Sep 15, 2025) @@ -215,8 +215,8 @@ struct CycleTests { #expect(cycle.contains(midCycleDate)) } - @Test("contains returns false for date outside cycle") - func testContainsDateOutsideCycle() throws { + @Test + func `excludes dates before and after the cycle`() throws { let cycle = Cycle(year: 2025, month: 9, day: 1) // Create a date before the cycle (Aug 15, 2025) @@ -236,8 +236,8 @@ struct CycleTests { #expect(!cycle.contains(afterDate)) } - @Test("cycle(for:) returns correct cycle") - func testCycleForDate() throws { + @Test + func `returns the cycle covering a given date`() throws { var calendar = Calendar(identifier: .gregorian) calendar.timeZone = .gmt @@ -252,8 +252,8 @@ struct CycleTests { #expect(cycle.day == 1) } - @Test("expirationDate returns exact expiration moment") - func testExpirationDateExactMoment() throws { + @Test + func `expires 56 days after the effective date, when the next cycle begins`() throws { let cycle = Cycle(year: 2025, month: 9, day: 1) let effectiveDate = try #require(cycle.effectiveDate) let expirationDate = try #require(cycle.expirationDate) diff --git a/Tests/SwiftDOFTests/DOFTests.swift b/Tests/SwiftDOFTests/DOFTests.swift index ab89209..1c1ad44 100644 --- a/Tests/SwiftDOFTests/DOFTests.swift +++ b/Tests/SwiftDOFTests/DOFTests.swift @@ -23,8 +23,8 @@ struct DOFTests { sampleDOFContent.data(using: .utf8)! } - @Test("Parse DOF data") - func testParseDOFData() throws { + @Test + func `parses obstacles and the currency date from DOF data`() throws { let dof = try DOF(data: sampleDOFData) #expect(dof.count == 3) @@ -33,8 +33,8 @@ struct DOFTests { #expect(dof.cycle.day == 21) } - @Test("Lookup obstacle by ID") - func testLookupByID() throws { + @Test + func `looks up an obstacle by its OAS number`() throws { let dof = try DOF(data: sampleDOFData) let obstacle = try #require(dof.obstacle(for: "01-001307")) @@ -42,30 +42,30 @@ struct DOFTests { #expect(obstacle.type == "RIG") } - @Test("Lookup non-existent obstacle returns nil") - func testLookupNonExistent() throws { + @Test + func `returns nil for an unknown OAS number`() throws { let dof = try DOF(data: sampleDOFData) let obstacle = dof.obstacle(for: "99-999999") #expect(obstacle == nil) } - @Test("All property returns all obstacles") - func testAllProperty() throws { + @Test + func `returns every obstacle from all`() throws { let dof = try DOF(data: sampleDOFData) let all = dof.all #expect(all.count == 3) } - @Test("Count property") - func testCountProperty() throws { + @Test + func `counts the parsed obstacles`() throws { let dof = try DOF(data: sampleDOFData) #expect(dof.count == 3) } - @Test("Sequence conformance - iteration") - func testSequenceIteration() throws { + @Test + func `iterates over every obstacle as a sequence`() throws { let dof = try DOF(data: sampleDOFData) var count = 0 @@ -75,16 +75,16 @@ struct DOFTests { #expect(count == 3) } - @Test("Collection conformance") - func testCollectionConformance() throws { + @Test + func `exposes obstacles as a non-empty collection`() throws { let dof = try DOF(data: sampleDOFData) #expect(!dof.isEmpty) #expect(dof.startIndex != dof.endIndex) } - @Test("Filter obstacles by state") - func testFilterByState() throws { + @Test + func `filters obstacles by state`() throws { let dof = try DOF(data: sampleDOFData) let alObstacles = dof.obstacles(in: "AL") @@ -94,8 +94,8 @@ struct DOFTests { #expect(caObstacles.isEmpty) } - @Test("DOF is Codable") - func testCodable() throws { + @Test + func `round-trips a DOF through JSON`() throws { let dof = try DOF(data: sampleDOFData) let encoder = JSONEncoder() @@ -108,8 +108,8 @@ struct DOFTests { #expect(decoded.cycle == dof.cycle) } - @Test("Parse currency date") - func testParseCurrencyDate() throws { + @Test + func `parses the currency date header into a cycle`() throws { let bytes: [UInt8] = Array(" CURRENCY DATE = 12/21/25".utf8) let cycle = try DOFByteParser.parseCurrencyDate(bytes[...]) @@ -118,8 +118,8 @@ struct DOFTests { #expect(cycle.day == 21) } - @Test("Error callback is invoked for malformed lines") - func testErrorCallback() throws { + @Test + func `invokes the error callback for a malformed line and skips it`() throws { let contentWithError = """ CURRENCY DATE = 12/21/25 LATITUDE LONGITUDE OBSTACLE AGL \ @@ -146,15 +146,15 @@ struct DOFTests { #expect(errorCount == 1) // 1 error for the invalid line } - @Test("Empty data throws error") - func testEmptyData() { + @Test + func `throws when the data is empty`() { #expect(throws: DOFError.self) { try DOF(data: Data()) } } - @Test("Data with only header produces empty DOF") - func testOnlyHeader() throws { + @Test + func `parses a header-only file into an empty DOF`() throws { let headerOnly = """ CURRENCY DATE = 12/21/25 HEADER @@ -166,14 +166,14 @@ struct DOFTests { #expect(dof.cycle.year == 2025) } - @Test("From data factory method") - func testFromData() throws { + @Test + func `parses obstacles through the from(data:) factory`() throws { let dof = try DOF.from(data: sampleDOFData) #expect(dof.count == 3) } - @Test("From data with error callback") - func testFromDataWithErrorCallback() throws { + @Test + func `leaves the error callback uncalled for valid data`() throws { var errorCalled = false let dof = try DOF.from( diff --git a/Tests/SwiftDOFTests/ObstacleTests.swift b/Tests/SwiftDOFTests/ObstacleTests.swift index 02196eb..74ec3f1 100644 --- a/Tests/SwiftDOFTests/ObstacleTests.swift +++ b/Tests/SwiftDOFTests/ObstacleTests.swift @@ -15,8 +15,8 @@ struct ObstacleTests { .utf8 ) - @Test("Parse valid obstacle line") - func testParseValidLine() throws { + @Test + func `parses every field of a valid obstacle line`() throws { let obstacle = try DOFByteParser.parseLine(sampleLineBytes[...], lineNumber: 1) #expect(obstacle.oasNumber == "01-001307") @@ -36,8 +36,8 @@ struct ObstacleTests { #expect(obstacle.lastUpdatedComponents.dayOfYear == 138) } - @Test("Parse latitude (North)") - func testParseLatitudeNorth() throws { + @Test + func `converts a northern latitude to positive degrees`() throws { let obstacle = try DOFByteParser.parseLine(sampleLineBytes[...], lineNumber: 1) // 30 10 45.00N = 30 + 10/60 + 45/3600 = 30.179166... @@ -46,8 +46,8 @@ struct ObstacleTests { #expect(obstacle.latitudeDeg > 0) // North is positive } - @Test("Parse longitude (West)") - func testParseLongitudeWest() throws { + @Test + func `converts a western longitude to negative degrees`() throws { let obstacle = try DOFByteParser.parseLine(sampleLineBytes[...], lineNumber: 1) // 088 04 39.00W = -(88 + 4/60 + 39/3600) = -88.0775 @@ -56,14 +56,14 @@ struct ObstacleTests { #expect(obstacle.longitudeDeg < 0) // West is negative } - @Test("Obstacle Identifiable conformance") - func testIdentifiable() throws { + @Test + func `identifies an obstacle by its OAS number`() throws { let obstacle = try DOFByteParser.parseLine(sampleLineBytes[...], lineNumber: 1) #expect(obstacle.id == "01-001307") } - @Test("Obstacle Hashable conformance") - func testHashable() throws { + @Test + func `hashes two identical obstacles into one set element`() throws { let obstacle1 = try DOFByteParser.parseLine(sampleLineBytes[...], lineNumber: 1) let obstacle2 = try DOFByteParser.parseLine(sampleLineBytes[...], lineNumber: 1) @@ -74,16 +74,16 @@ struct ObstacleTests { #expect(set.count == 1) } - @Test("Obstacle Equatable conformance") - func testEquatable() throws { + @Test + func `considers two obstacles parsed from the same line equal`() throws { let obstacle1 = try DOFByteParser.parseLine(sampleLineBytes[...], lineNumber: 1) let obstacle2 = try DOFByteParser.parseLine(sampleLineBytes[...], lineNumber: 1) #expect(obstacle1 == obstacle2) } - @Test("Parse different obstacle types") - func testDifferentObstacleTypes() throws { + @Test + func `parses the obstacle type of a tower and a building`() throws { let towerBytes: [UInt8] = Array( "01-001173 O US AL DAUPHIN ISLAND 30 15 01.00N 088 04 45.00W TOWER 1 00201 00205 R 5 D M 1988ASO02440OE C 2014138 " .utf8 @@ -99,8 +99,8 @@ struct ObstacleTests { #expect(bldg.type == "BLDG") } - @Test("Parse under review verification status") - func testUnderReviewStatus() throws { + @Test + func `parses the under-review verification status`() throws { let bytes: [UInt8] = Array( "01-061332 U US AL GULF SHORES 30 14 43.32N 087 42 12.20W BLDG 1 00059 00067 N 4 D N 2018ASO25793OE A 2020230 " .utf8 @@ -109,8 +109,8 @@ struct ObstacleTests { #expect(obstacle.verificationStatus == .underReview) } - @Test("Parse active action code") - func testActiveActionCode() throws { + @Test + func `parses the active action code`() throws { let bytes: [UInt8] = Array( "01-061332 U US AL GULF SHORES 30 14 43.32N 087 42 12.20W BLDG 1 00059 00067 N 4 D N 2018ASO25793OE A 2020230 " .utf8 @@ -120,8 +120,8 @@ struct ObstacleTests { } #if canImport(CoreLocation) - @Test("CoreLocation extension") - func testCoreLocationExtension() throws { + @Test + func `exposes the obstacle position as a CoreLocation coordinate`() throws { let obstacle = try DOFByteParser.parseLine(sampleLineBytes[...], lineNumber: 1) let coordinate = obstacle.coreLocation @@ -130,8 +130,8 @@ struct ObstacleTests { } #endif - @Test("Measurement extension - latitude") - func testMeasurementLatitude() throws { + @Test + func `exposes the latitude as a measurement in degrees`() throws { let obstacle = try DOFByteParser.parseLine(sampleLineBytes[...], lineNumber: 1) let latitude = obstacle.latitude @@ -139,8 +139,8 @@ struct ObstacleTests { #expect(latitude.value == obstacle.latitudeDeg) } - @Test("Measurement extension - longitude") - func testMeasurementLongitude() throws { + @Test + func `exposes the longitude as a measurement in degrees`() throws { let obstacle = try DOFByteParser.parseLine(sampleLineBytes[...], lineNumber: 1) let longitude = obstacle.longitude @@ -148,8 +148,8 @@ struct ObstacleTests { #expect(longitude.value == obstacle.longitudeDeg) } - @Test("Measurement extension - height AGL") - func testMeasurementHeightAGL() throws { + @Test + func `exposes the height AGL as a measurement in feet`() throws { let obstacle = try DOFByteParser.parseLine(sampleLineBytes[...], lineNumber: 1) let height = obstacle.heightAGL @@ -157,8 +157,8 @@ struct ObstacleTests { #expect(height.value == Double(obstacle.heightFtAGL)) } - @Test("Measurement extension - height MSL") - func testMeasurementHeightMSL() throws { + @Test + func `exposes the height MSL as a measurement in feet`() throws { let obstacle = try DOFByteParser.parseLine(sampleLineBytes[...], lineNumber: 1) let height = obstacle.heightMSL @@ -166,23 +166,23 @@ struct ObstacleTests { #expect(height.value == Double(obstacle.heightFtMSL)) } - @Test("All lighting types are parseable") - func testAllLightingTypesAreParseable() { + @Test + func `parses every lighting type from its raw value`() { for type in LightingType.allCases { #expect(LightingType(rawValue: type.rawValue) == type) } } - @Test("Accuracy category values") - func testAccuracyCategoryValues() { + @Test + func `maps accuracy categories to their tolerances in feet`() { #expect(AccuracyCategory.category1.accuracy == Measurement(value: 20, unit: .feet)) #expect(AccuracyCategory.category5.accuracy == Measurement(value: 500, unit: .feet)) #expect(AccuracyCategory.category9.accuracy == nil) // Unknown #expect(AccuracyCategory.survey.accuracy == Measurement(value: 3, unit: .feet)) } - @Test("Parse line that is too short throws error") - func testLineTooShort() { + @Test + func `throws when the line is too short`() { let shortBytes: [UInt8] = Array("01-001307 O US AL".utf8) #expect(throws: DOFError.self) { @@ -190,8 +190,8 @@ struct ObstacleTests { } } - @Test("Obstacle is Codable") - func testCodable() throws { + @Test + func `round-trips an obstacle through JSON`() throws { let obstacle = try DOFByteParser.parseLine(sampleLineBytes[...], lineNumber: 1) let encoder = JSONEncoder()