Skip to content

Commit f07fad6

Browse files
committed
unit testing: networking
1 parent 5a55232 commit f07fad6

21 files changed

+1343
-1
lines changed
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
name: Run Tests with Coverage
2+
3+
on:
4+
pull_request:
5+
types: ["opened", "reopened", "ready_for_review", "synchronize"]
6+
branches:
7+
- main
8+
9+
concurrency:
10+
group: ${{ github.workflow }}-${{ github.ref }}
11+
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
12+
13+
jobs:
14+
test-and-coverage:
15+
if: ${{ github.event.pull_request.draft == false }}
16+
runs-on: macos-15
17+
env:
18+
COVERAGE_HTML_DIR: coverage-html
19+
steps:
20+
- name: Checkout
21+
uses: actions/checkout@v4
22+
23+
- name: Show toolchain info
24+
run: |
25+
swift --version
26+
27+
- name: Build & Test (SwiftPM, with coverage)
28+
run: |
29+
set -eo pipefail
30+
swift build --configuration debug
31+
swift test --parallel --enable-code-coverage
32+
33+
- name: Generate coverage summary and HTML
34+
run: |
35+
set -eo pipefail
36+
BIN_PATH=$(swift build --show-bin-path)
37+
PROF_DATA="${BIN_PATH}/codecov/default.profdata"
38+
39+
# Fallback: merge any .profraw into default.profdata if needed
40+
if [[ ! -f "${PROF_DATA}" ]]; then
41+
mkdir -p "${BIN_PATH}/codecov"
42+
RAWS=$(find "${BIN_PATH}/codecov" -name "*.profraw" -print || true)
43+
if [[ -n "$RAWS" ]]; then
44+
xcrun llvm-profdata merge -sparse $RAWS -o "${BIN_PATH}/codecov/default.profdata"
45+
fi
46+
fi
47+
48+
if [[ ! -f "${PROF_DATA}" ]]; then
49+
echo "Coverage profile not found at ${PROF_DATA}" >&2
50+
exit 1
51+
fi
52+
53+
# Collect test binaries (.xctest bundles)
54+
TEST_BINS=()
55+
while IFS= read -r b; do
56+
name=$(basename "$b" .xctest)
57+
TEST_BINS+=("$b/Contents/MacOS/$name")
58+
done < <(find "${BIN_PATH}" -type d -name "*.xctest" -print)
59+
60+
if [[ ${#TEST_BINS[@]} -eq 0 ]]; then
61+
echo "No test binaries found in ${BIN_PATH}" >&2
62+
exit 1
63+
fi
64+
65+
# Human-readable summary
66+
xcrun llvm-cov report \
67+
-instr-profile "${PROF_DATA}" \
68+
"${TEST_BINS[@]}" | sed -E 's/\x1b\[[0-9;]*m//g' > coverage-summary.txt
69+
70+
# HTML report
71+
mkdir -p "${COVERAGE_HTML_DIR}"
72+
xcrun llvm-cov show \
73+
-format=html \
74+
-instr-profile "${PROF_DATA}" \
75+
-ignore-filename-regex '\\.(build|Build)|Tests' \
76+
"${TEST_BINS[@]}" \
77+
-output-dir "${COVERAGE_HTML_DIR}"
78+
79+
- name: Upload HTML coverage artifact
80+
uses: actions/upload-artifact@v4
81+
with:
82+
name: coverage-html
83+
path: ${{ env.COVERAGE_HTML_DIR }}/
84+
85+
- name: Print coverage summary
86+
if: always()
87+
run: |
88+
echo "=== llvm-cov summary ==="
89+
cat coverage-summary.txt || true

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@ DerivedData/
77
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
88
.netrc
99
.claude/settings.local.json
10+
.cursor
11+
.swiftpm

Sources/Hume/Services/Networking/AccessTokenResolver.swift

Whitespace-only changes.

Sources/Hume/Services/Networking/HTTPMethod.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import Foundation
99

10-
enum HTTPMethod: String {
10+
enum HTTPMethod: String, CaseIterable {
1111
case get = "GET"
1212
case post = "POST"
1313
case put = "PUT"
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//
2+
// AVAdditionsTest.swift
3+
// Hume
4+
//
5+
6+
import AVFoundation
7+
import Foundation
8+
import Testing
9+
10+
@testable import Hume
11+
12+
struct AVAdditionsTest {
13+
#if os(iOS)
14+
@Test func avAudioSession_recordPermission_maps_to_MicrophonePermission() async throws {
15+
// Assert direct mapping values compile and map correctly
16+
#expect(AVAudioSession.RecordPermission.undetermined.asMicrophonePermission == .undetermined)
17+
#expect(AVAudioSession.RecordPermission.denied.asMicrophonePermission == .denied)
18+
#expect(AVAudioSession.RecordPermission.granted.asMicrophonePermission == .granted)
19+
}
20+
21+
@Test func avAudioApplication_recordPermission_maps_to_MicrophonePermission() async throws {
22+
if #available(iOS 17.0, *) {
23+
#expect(AVAudioApplication.recordPermission.undetermined.asMicrophonePermission == .undetermined)
24+
#expect(AVAudioApplication.recordPermission.denied.asMicrophonePermission == .denied)
25+
#expect(AVAudioApplication.recordPermission.granted.asMicrophonePermission == .granted)
26+
} else {
27+
// Can't reference AVAudioApplication on earlier iOS; ensure test doesn't run
28+
#expect(true)
29+
}
30+
}
31+
#endif
32+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//
2+
// AssistantInputExtensionsTest.swift
3+
// Hume
4+
//
5+
6+
import Foundation
7+
import Testing
8+
9+
@testable import Hume
10+
11+
struct AssistantInputExtensionsTest {
12+
13+
@Test func init_text_sets_nil_sessionId_and_text_matches() async throws {
14+
// Arrange
15+
let inputText = "Hello, Assistant!"
16+
17+
// Act
18+
let sut = AssistantInput(text: inputText)
19+
20+
// Assert
21+
#expect(sut.customSessionId == nil)
22+
#expect(sut.text == inputText)
23+
}
24+
25+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
//
2+
// AudioOutputExtensionsTest.swift
3+
// Hume
4+
//
5+
6+
import Foundation
7+
import Testing
8+
9+
@testable import Hume
10+
11+
struct AudioOutputExtensionsTest {
12+
13+
@Test func asBase64EncodedData_returns_data_for_valid_base64() async throws {
14+
// Arrange
15+
let bytes = Data([0x61, 0x62, 0x63]) // "abc"
16+
let base64 = bytes.base64EncodedString() // "YWJj"
17+
let model = AudioOutput(
18+
type: "audio_output",
19+
customSessionId: nil,
20+
id: "id1",
21+
index: 0,
22+
data: base64
23+
)
24+
25+
// Act
26+
let decoded = model.asBase64EncodedData
27+
28+
// Assert
29+
#expect(decoded == bytes)
30+
}
31+
32+
@Test func asBase64EncodedData_returns_nil_for_invalid_base64() async throws {
33+
// Arrange
34+
let model = AudioOutput(
35+
type: "audio_output",
36+
customSessionId: nil,
37+
id: "id2",
38+
index: 1,
39+
data: "not-base64!!"
40+
)
41+
42+
// Act
43+
let decoded = model.asBase64EncodedData
44+
45+
// Assert
46+
#expect(decoded == nil)
47+
}
48+
49+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
//
2+
// DataAdditionsTest.swift
3+
// Hume
4+
//
5+
6+
import Foundation
7+
import Testing
8+
9+
@testable import Hume
10+
11+
struct DataAdditionsTest {
12+
#if os(iOS)
13+
@Test func parseWAVHeader_parses_minimal_valid_header_and_maps_to_avformat() async throws {
14+
// Arrange: Build 44-byte RIFF/WAVE header (little-endian) matching WAVHeader expectations
15+
// Fields we care about: chunkID "RIFF", format "WAVE", subchunk1ID "fmt ",
16+
// audioFormat=1 (PCM), numChannels=2, sampleRate=48000, byteRate, blockAlign, bitsPerSample=16
17+
var bytes = [UInt8](repeating: 0, count: 44)
18+
19+
func putString(_ s: String, at offset: Int) {
20+
let data = s.data(using: .ascii)!
21+
for (i, b) in data.enumerated() { bytes[offset + i] = b }
22+
}
23+
func putUInt16(_ v: UInt16, at offset: Int) {
24+
let le = withUnsafeBytes(of: v.littleEndian) { Array($0) }
25+
bytes[offset] = le[0]; bytes[offset + 1] = le[1]
26+
}
27+
func putUInt32(_ v: UInt32, at offset: Int) {
28+
let le = withUnsafeBytes(of: v.littleEndian) { Array($0) }
29+
bytes[offset] = le[0]; bytes[offset + 1] = le[1]; bytes[offset + 2] = le[2]; bytes[offset + 3] = le[3]
30+
}
31+
32+
// RIFF header
33+
putString("RIFF", at: 0)
34+
putUInt32(36, at: 4) // chunk size (unused by parser)
35+
putString("WAVE", at: 8)
36+
// fmt subchunk
37+
putString("fmt ", at: 12)
38+
putUInt32(16, at: 16) // subchunk1 size (PCM)
39+
putUInt16(1, at: 20) // audioFormat = PCM
40+
putUInt16(2, at: 22) // numChannels = 2
41+
putUInt32(48000, at: 24) // sampleRate
42+
putUInt32(48000 * 2 * 16 / 8, at: 28) // byteRate = sampleRate * channels * blitsPerSample/8
43+
putUInt16(2 * 16 / 8, at: 32) // blockAlign = channels * bitsPerSample/8
44+
putUInt16(16, at: 34) // bitsPerSample
45+
// data subchunk header (not read by parser but include to make header realistic)
46+
putString("data", at: 36)
47+
putUInt32(0, at: 40)
48+
49+
let data = Data(bytes)
50+
51+
// Act
52+
let header = data.parseWAVHeader()
53+
54+
// Assert header parsed
55+
#expect(header != nil)
56+
#expect(header!.isValid)
57+
58+
// Verify fields and mapping to AVAudioFormat
59+
let asFormat = header!.asAVAudioFormat
60+
#expect(asFormat != nil)
61+
#expect(asFormat!.sampleRate == 48000)
62+
#expect(asFormat!.channelCount == 2)
63+
}
64+
#endif
65+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//
2+
// DecodingErrorAdditionsTest.swift
3+
// Hume
4+
//
5+
6+
import Foundation
7+
import Testing
8+
9+
@testable import Hume
10+
11+
struct DecodingErrorAdditionsTest {
12+
13+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
//
2+
// EmotionScoresExtensionsTest.swift
3+
// Hume
4+
//
5+
6+
import Foundation
7+
import Testing
8+
9+
@testable import Hume
10+
11+
struct EmotionScoresExtensionsTest {
12+
@Test func topThree_returns_top3_sorted_desc() async throws {
13+
// Arrange
14+
let scores: EmotionScores = [
15+
"Joy": 0.9,
16+
"Sadness": 0.1,
17+
"Anger": 0.7,
18+
"Calmness": 0.8,
19+
"Boredom": 0.05,
20+
]
21+
22+
// Act
23+
let top = scores.topThree
24+
25+
// Assert
26+
#expect(top.count == 3)
27+
#expect(top[0].name == "Joy" && top[0].value == 0.9)
28+
#expect(top[1].name == "Calmness" && top[1].value == 0.8)
29+
#expect(top[2].name == "Anger" && top[2].value == 0.7)
30+
}
31+
32+
@Test func topThree_handles_fewer_than_three_entries() async throws {
33+
// Arrange
34+
let scores: EmotionScores = ["Joy": 0.2, "Calmness": 0.3]
35+
36+
// Act
37+
let top = scores.topThree
38+
39+
// Assert
40+
#expect(top.count == 2)
41+
#expect(Set(top.map { $0.name }) == Set(["Calmness", "Joy"]))
42+
#expect(top[0].value >= top[1].value)
43+
}
44+
45+
@Test func topThree_on_empty_returns_empty() async throws {
46+
// Arrange
47+
let scores: EmotionScores = [:]
48+
49+
// Act
50+
let top = scores.topThree
51+
52+
// Assert
53+
#expect(top.isEmpty)
54+
}
55+
}

0 commit comments

Comments
 (0)