Skip to content

Commit b2cca42

Browse files
author
Vincent Herbst
committed
test+ci: add AetherEngineTests target and GitHub Actions CI
Tests/AetherEngineTests/ covers the pure-function surface that doesn't need FFmpeg fixtures or device hardware: - FrameRateSnap: standard rates, film-cadence shortcut, tolerance edges, invalid inputs - VideoFormat: case distinctness under Equatable, Sendable - LoadOptions: documented defaults, Equatable Uses Swift Testing (the modern @suite / @test framework that ships with the Swift 6.0 toolchain the package already requires). .github/workflows/ci.yml runs three jobs on macos-15: - swift build + swift test (verifies the package + tests) - xcodebuild build for tvOS Simulator - xcodebuild build for iOS Simulator CI gates pushes to main and PRs against main. Local swift test passes (12/12) and tvOS xcodebuild succeeds.
1 parent 22c4e8c commit b2cca42

5 files changed

Lines changed: 219 additions & 0 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
workflow_dispatch:
9+
10+
concurrency:
11+
group: ${{ github.workflow }}-${{ github.ref }}
12+
cancel-in-progress: true
13+
14+
jobs:
15+
test-macos:
16+
name: swift test (macOS)
17+
runs-on: macos-15
18+
timeout-minutes: 30
19+
steps:
20+
- uses: actions/checkout@v4
21+
22+
- uses: maxim-lobanov/setup-xcode@v1
23+
with:
24+
xcode-version: latest-stable
25+
26+
- name: Show toolchain
27+
run: |
28+
swift --version
29+
xcodebuild -version
30+
31+
- name: swift build
32+
run: swift build
33+
34+
- name: swift test
35+
run: swift test
36+
37+
build-tvos:
38+
name: xcodebuild (tvOS Simulator)
39+
runs-on: macos-15
40+
timeout-minutes: 30
41+
steps:
42+
- uses: actions/checkout@v4
43+
44+
- uses: maxim-lobanov/setup-xcode@v1
45+
with:
46+
xcode-version: latest-stable
47+
48+
- name: Build for tvOS Simulator
49+
run: |
50+
xcodebuild build \
51+
-scheme AetherEngine \
52+
-destination 'generic/platform=tvOS Simulator' \
53+
-derivedDataPath .build/xcode-tvos \
54+
CODE_SIGNING_ALLOWED=NO
55+
56+
build-ios:
57+
name: xcodebuild (iOS Simulator)
58+
runs-on: macos-15
59+
timeout-minutes: 30
60+
steps:
61+
- uses: actions/checkout@v4
62+
63+
- uses: maxim-lobanov/setup-xcode@v1
64+
with:
65+
xcode-version: latest-stable
66+
67+
- name: Build for iOS Simulator
68+
run: |
69+
xcodebuild build \
70+
-scheme AetherEngine \
71+
-destination 'generic/platform=iOS Simulator' \
72+
-derivedDataPath .build/xcode-ios \
73+
CODE_SIGNING_ALLOWED=NO

‎Package.swift‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,10 @@ let package = Package(
4747
dependencies: ["AetherEngine"],
4848
path: "Sources/aetherctl"
4949
),
50+
.testTarget(
51+
name: "AetherEngineTests",
52+
dependencies: ["AetherEngine"],
53+
path: "Tests/AetherEngineTests"
54+
),
5055
]
5156
)
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import Testing
2+
@testable import AetherEngine
3+
4+
@Suite("FrameRateSnap")
5+
struct FrameRateSnapTests {
6+
7+
@Test("Standard rates pass through unchanged")
8+
func standardRatesIdentity() {
9+
#expect(FrameRateSnap.snap(25) == 25)
10+
#expect(FrameRateSnap.snap(29.97) == 29.97)
11+
#expect(FrameRateSnap.snap(30) == 30)
12+
#expect(FrameRateSnap.snap(50) == 50)
13+
#expect(FrameRateSnap.snap(59.94) == 59.94)
14+
#expect(FrameRateSnap.snap(60) == 60)
15+
#expect(FrameRateSnap.snap(48) == 48)
16+
}
17+
18+
@Test("Film cadence shortcut: 23.5...24.05 → 23.976")
19+
func filmCadencePrefers23976() {
20+
// Film-cadence shortcut beats the 24.0 nearest-match because
21+
// panels that support 24 also support 23.976; reverse isn't
22+
// guaranteed.
23+
#expect(FrameRateSnap.snap(23.976) == 23.976)
24+
#expect(FrameRateSnap.snap(24.000) == 23.976)
25+
#expect(FrameRateSnap.snap(23.97) == 23.976)
26+
#expect(FrameRateSnap.snap(23.98) == 23.976)
27+
#expect(FrameRateSnap.snap(23.5) == 23.976)
28+
#expect(FrameRateSnap.snap(24.05) == 23.976)
29+
}
30+
31+
@Test("Nearby probes snap to standard within ±0.5")
32+
func nearestMatchWithinTolerance() {
33+
#expect(FrameRateSnap.snap(24.99) == 25)
34+
#expect(FrameRateSnap.snap(25.4) == 25)
35+
#expect(FrameRateSnap.snap(29.5) == 29.97)
36+
#expect(FrameRateSnap.snap(60.3) == 60)
37+
}
38+
39+
@Test("Out-of-tolerance probes return nil")
40+
func outOfToleranceReturnsNil() {
41+
// 35 is more than 0.5 away from any standard rate.
42+
#expect(FrameRateSnap.snap(35) == nil)
43+
// 45 is between 30 and 48, more than 0.5 from each.
44+
#expect(FrameRateSnap.snap(45) == nil)
45+
// 120 is far above any standard rate.
46+
#expect(FrameRateSnap.snap(120) == nil)
47+
}
48+
49+
@Test("Invalid inputs return nil")
50+
func invalidInputsReturnNil() {
51+
#expect(FrameRateSnap.snap(0) == nil)
52+
#expect(FrameRateSnap.snap(-1) == nil)
53+
#expect(FrameRateSnap.snap(-23.976) == nil)
54+
#expect(FrameRateSnap.snap(.nan) == nil)
55+
#expect(FrameRateSnap.snap(.infinity) == nil)
56+
}
57+
58+
@Test("Standard set is the documented contract")
59+
func standardSetMatchesDocumentation() {
60+
// Locks the documented contract: changes here are intentional
61+
// API breaks for hosts that depend on a specific set.
62+
#expect(FrameRateSnap.standard == [23.976, 24, 25, 29.97, 30, 48, 50, 59.94, 60])
63+
}
64+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import Testing
2+
@testable import AetherEngine
3+
4+
@Suite("LoadOptions")
5+
struct LoadOptionsTests {
6+
7+
@Test("Default values match documented defaults")
8+
func defaultsMatchDocs() {
9+
let opts = LoadOptions()
10+
#expect(opts.omitCriteriaColorExtensions == false)
11+
#expect(opts.suppressDisplayCriteria == false)
12+
#expect(opts.httpHeaders.isEmpty)
13+
#expect(opts.keepDvh1TagWithoutDV == false)
14+
// Default matchContentEnabled MUST be true; flipping this would
15+
// silently regress HDR routing for non-tvOS callers that don't
16+
// query the AVDisplayManager flag.
17+
#expect(opts.matchContentEnabled == true)
18+
// Default panelIsInHDRMode MUST be false (conservative branch).
19+
#expect(opts.panelIsInHDRMode == false)
20+
#expect(opts.audioBridgeMode == .surroundCompat)
21+
#expect(opts.isLive == false)
22+
}
23+
24+
@Test("Equatable holds for identical inputs")
25+
func equatableForIdenticalInputs() {
26+
let a = LoadOptions(httpHeaders: ["Auth": "x"], matchContentEnabled: false)
27+
let b = LoadOptions(httpHeaders: ["Auth": "x"], matchContentEnabled: false)
28+
#expect(a == b)
29+
}
30+
31+
@Test("Equatable distinguishes different inputs")
32+
func equatableDistinguishesDifferentInputs() {
33+
let a = LoadOptions(matchContentEnabled: true)
34+
let b = LoadOptions(matchContentEnabled: false)
35+
#expect(a != b)
36+
37+
let c = LoadOptions(panelIsInHDRMode: true)
38+
let d = LoadOptions(panelIsInHDRMode: false)
39+
#expect(c != d)
40+
}
41+
42+
@Test("audioBridgeMode is preserved through init")
43+
func audioBridgeModePreserved() {
44+
let surround = LoadOptions(audioBridgeMode: .surroundCompat)
45+
let lossless = LoadOptions(audioBridgeMode: .lossless)
46+
#expect(surround.audioBridgeMode == .surroundCompat)
47+
#expect(lossless.audioBridgeMode == .lossless)
48+
#expect(surround != lossless)
49+
}
50+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import Testing
2+
@testable import AetherEngine
3+
4+
@Suite("VideoFormat")
5+
struct VideoFormatTests {
6+
7+
@Test("All five cases are distinct under Equatable")
8+
func allCasesAreDistinct() {
9+
let all: [VideoFormat] = [.sdr, .hdr10, .hdr10Plus, .dolbyVision, .hlg]
10+
for (i, a) in all.enumerated() {
11+
for (j, b) in all.enumerated() {
12+
if i == j {
13+
#expect(a == b)
14+
} else {
15+
#expect(a != b)
16+
}
17+
}
18+
}
19+
}
20+
21+
@Test("Sendable across actor boundary")
22+
func sendableCrossesActorBoundary() async {
23+
let format: VideoFormat = .dolbyVision
24+
let received = await Task.detached { format }.value
25+
#expect(received == .dolbyVision)
26+
}
27+
}

0 commit comments

Comments
 (0)