Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions .github/workflows/run-unit-tests-with-coverage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
name: Run Tests with Coverage

on:
pull_request:
types: ["opened", "reopened", "ready_for_review", "synchronize"]
branches:
- main

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}

jobs:
test-and-coverage:
runs-on: macos-15
env:
COVERAGE_HTML_DIR: coverage-html
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Show toolchain info
run: |
swift --version

- name: Build & Test (SwiftPM, with coverage)
run: |
set -eo pipefail
swift build --configuration debug
swift test --parallel --enable-code-coverage

- name: Generate coverage summary and HTML
run: |
set -eo pipefail
BIN_PATH=$(swift build --show-bin-path)
PROF_DATA="${BIN_PATH}/codecov/default.profdata"

# Fallback: merge any .profraw into default.profdata if needed
if [[ ! -f "${PROF_DATA}" ]]; then
mkdir -p "${BIN_PATH}/codecov"
RAWS=$(find "${BIN_PATH}/codecov" -name "*.profraw" -print || true)
if [[ -n "$RAWS" ]]; then
xcrun llvm-profdata merge -sparse $RAWS -o "${BIN_PATH}/codecov/default.profdata"
fi
fi

if [[ ! -f "${PROF_DATA}" ]]; then
echo "Coverage profile not found at ${PROF_DATA}" >&2
exit 1
fi

# Collect test binaries (.xctest bundles)
TEST_BINS=()
while IFS= read -r b; do
name=$(basename "$b" .xctest)
TEST_BINS+=("$b/Contents/MacOS/$name")
done < <(find "${BIN_PATH}" -type d -name "*.xctest" -print)

if [[ ${#TEST_BINS[@]} -eq 0 ]]; then
echo "No test binaries found in ${BIN_PATH}" >&2
exit 1
fi

# Human-readable summary
xcrun llvm-cov report \
-instr-profile "${PROF_DATA}" \
"${TEST_BINS[@]}" | sed -E 's/\x1b\[[0-9;]*m//g' > coverage-summary.txt

# HTML report
mkdir -p "${COVERAGE_HTML_DIR}"
xcrun llvm-cov show \
-format=html \
-instr-profile "${PROF_DATA}" \
-ignore-filename-regex '\\.(build|Build)|Tests' \
"${TEST_BINS[@]}" \
-output-dir "${COVERAGE_HTML_DIR}"

- name: Upload HTML coverage artifact
uses: actions/upload-artifact@v4
with:
name: coverage-html
path: ${{ env.COVERAGE_HTML_DIR }}/

- name: Print coverage summary
if: always()
run: |
echo "=== llvm-cov summary ==="
cat coverage-summary.txt || true
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ DerivedData/
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
.claude/settings.local.json
.cursor
.swiftpm
Empty file.
2 changes: 1 addition & 1 deletion Sources/Hume/Services/Networking/HTTPMethod.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import Foundation

enum HTTPMethod: String {
enum HTTPMethod: String, CaseIterable {
case get = "GET"
case post = "POST"
case put = "PUT"
Expand Down
33 changes: 33 additions & 0 deletions Tests/HumeTests/Extensions/AVAdditionsTest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//
// AVAdditionsTest.swift
// Hume
//

import AVFoundation
import Foundation
import Testing

@testable import Hume

struct AVAdditionsTest {
#if os(iOS)
@Test func avAudioSession_recordPermission_maps_to_MicrophonePermission() async throws {
// Assert direct mapping values compile and map correctly
#expect(AVAudioSession.RecordPermission.undetermined.asMicrophonePermission == .undetermined)
#expect(AVAudioSession.RecordPermission.denied.asMicrophonePermission == .denied)
#expect(AVAudioSession.RecordPermission.granted.asMicrophonePermission == .granted)
}

@Test func avAudioApplication_recordPermission_maps_to_MicrophonePermission() async throws {
if #available(iOS 17.0, *) {
#expect(
AVAudioApplication.recordPermission.undetermined.asMicrophonePermission == .undetermined)
#expect(AVAudioApplication.recordPermission.denied.asMicrophonePermission == .denied)
#expect(AVAudioApplication.recordPermission.granted.asMicrophonePermission == .granted)
} else {
// Can't reference AVAudioApplication on earlier iOS; ensure test doesn't run
#expect(true)
}
}
#endif
}
25 changes: 25 additions & 0 deletions Tests/HumeTests/Extensions/AssistantInputExtensionsTest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// AssistantInputExtensionsTest.swift
// Hume
//

import Foundation
import Testing

@testable import Hume

struct AssistantInputExtensionsTest {

@Test func init_text_sets_nil_sessionId_and_text_matches() async throws {
// Arrange
let inputText = "Hello, Assistant!"

// Act
let sut = AssistantInput(text: inputText)

// Assert
#expect(sut.customSessionId == nil)
#expect(sut.text == inputText)
}

}
49 changes: 49 additions & 0 deletions Tests/HumeTests/Extensions/AudioOutputExtensionsTest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//
// AudioOutputExtensionsTest.swift
// Hume
//

import Foundation
import Testing

@testable import Hume

struct AudioOutputExtensionsTest {

@Test func asBase64EncodedData_returns_data_for_valid_base64() async throws {
// Arrange
let bytes = Data([0x61, 0x62, 0x63]) // "abc"
let base64 = bytes.base64EncodedString() // "YWJj"
let model = AudioOutput(
type: "audio_output",
customSessionId: nil,
id: "id1",
index: 0,
data: base64
)

// Act
let decoded = model.asBase64EncodedData

// Assert
#expect(decoded == bytes)
}

@Test func asBase64EncodedData_returns_nil_for_invalid_base64() async throws {
// Arrange
let model = AudioOutput(
type: "audio_output",
customSessionId: nil,
id: "id2",
index: 1,
data: "not-base64!!"
)

// Act
let decoded = model.asBase64EncodedData

// Assert
#expect(decoded == nil)
}

}
69 changes: 69 additions & 0 deletions Tests/HumeTests/Extensions/DataAdditionsTest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//
// DataAdditionsTest.swift
// Hume
//

import Foundation
import Testing

@testable import Hume

struct DataAdditionsTest {
#if os(iOS)
@Test func parseWAVHeader_parses_minimal_valid_header_and_maps_to_avformat() async throws {
// Arrange: Build 44-byte RIFF/WAVE header (little-endian) matching WAVHeader expectations
// Fields we care about: chunkID "RIFF", format "WAVE", subchunk1ID "fmt ",
// audioFormat=1 (PCM), numChannels=2, sampleRate=48000, byteRate, blockAlign, bitsPerSample=16
var bytes = [UInt8](repeating: 0, count: 44)

func putString(_ s: String, at offset: Int) {
let data = s.data(using: .ascii)!
for (i, b) in data.enumerated() { bytes[offset + i] = b }
}
func putUInt16(_ v: UInt16, at offset: Int) {
let le = withUnsafeBytes(of: v.littleEndian) { Array($0) }
bytes[offset] = le[0]
bytes[offset + 1] = le[1]
}
func putUInt32(_ v: UInt32, at offset: Int) {
let le = withUnsafeBytes(of: v.littleEndian) { Array($0) }
bytes[offset] = le[0]
bytes[offset + 1] = le[1]
bytes[offset + 2] = le[2]
bytes[offset + 3] = le[3]
}

// RIFF header
putString("RIFF", at: 0)
putUInt32(36, at: 4) // chunk size (unused by parser)
putString("WAVE", at: 8)
// fmt subchunk
putString("fmt ", at: 12)
putUInt32(16, at: 16) // subchunk1 size (PCM)
putUInt16(1, at: 20) // audioFormat = PCM
putUInt16(2, at: 22) // numChannels = 2
putUInt32(48000, at: 24) // sampleRate
putUInt32(48000 * 2 * 16 / 8, at: 28) // byteRate = sampleRate * channels * blitsPerSample/8
putUInt16(2 * 16 / 8, at: 32) // blockAlign = channels * bitsPerSample/8
putUInt16(16, at: 34) // bitsPerSample
// data subchunk header (not read by parser but include to make header realistic)
putString("data", at: 36)
putUInt32(0, at: 40)

let data = Data(bytes)

// Act
let header = data.parseWAVHeader()

// Assert header parsed
#expect(header != nil)
#expect(header!.isValid)

// Verify fields and mapping to AVAudioFormat
let asFormat = header!.asAVAudioFormat
#expect(asFormat != nil)
#expect(asFormat!.sampleRate == 48000)
#expect(asFormat!.channelCount == 2)
}
#endif
}
13 changes: 13 additions & 0 deletions Tests/HumeTests/Extensions/DecodingErrorAdditionsTest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// DecodingErrorAdditionsTest.swift
// Hume
//

import Foundation
import Testing

@testable import Hume

struct DecodingErrorAdditionsTest {

}
55 changes: 55 additions & 0 deletions Tests/HumeTests/Extensions/EmotionScoresExtensionsTest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//
// EmotionScoresExtensionsTest.swift
// Hume
//

import Foundation
import Testing

@testable import Hume

struct EmotionScoresExtensionsTest {
@Test func topThree_returns_top3_sorted_desc() async throws {
// Arrange
let scores: EmotionScores = [
"Joy": 0.9,
"Sadness": 0.1,
"Anger": 0.7,
"Calmness": 0.8,
"Boredom": 0.05,
]

// Act
let top = scores.topThree

// Assert
#expect(top.count == 3)
#expect(top[0].name == "Joy" && top[0].value == 0.9)
#expect(top[1].name == "Calmness" && top[1].value == 0.8)
#expect(top[2].name == "Anger" && top[2].value == 0.7)
}

@Test func topThree_handles_fewer_than_three_entries() async throws {
// Arrange
let scores: EmotionScores = ["Joy": 0.2, "Calmness": 0.3]

// Act
let top = scores.topThree

// Assert
#expect(top.count == 2)
#expect(Set(top.map { $0.name }) == Set(["Calmness", "Joy"]))
#expect(top[0].value >= top[1].value)
}

@Test func topThree_on_empty_returns_empty() async throws {
// Arrange
let scores: EmotionScores = [:]

// Act
let top = scores.topThree

// Assert
#expect(top.isEmpty)
}
}
Loading
Loading