m323/Sources/MiniProject/RunnerCoreTests.swift
cediackermann 13876d51c2
test(MiniProject): extract RunnerCore library with Swift Testing suite
Split UI-free logic into a RunnerCore swift_library and cover it with a
swift_test suite using the Swift Testing library (import Testing / @Test /
#expect), matching Tests/m323Tests. Runs via `bazel test`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 08:47:33 +02:00

216 lines
7.3 KiB
Swift

import Foundation
import Testing
import RunnerCore
struct RunnerCoreTests {
// MARK: - AgeGroup
@Test func ageGroupDescription() {
#expect(AgeGroup.junior.description == "Junior")
#expect(AgeGroup.adult.description == "Adult")
#expect(AgeGroup.senior.description == "Senior")
}
@Test func ageGroupRawValueInit() {
#expect(AgeGroup(rawValue: 1) == .junior)
#expect(AgeGroup(rawValue: 3) == .senior)
#expect(AgeGroup(rawValue: 0) == nil)
#expect(AgeGroup(rawValue: 4) == nil)
}
// MARK: - SortKey.sort
private func runner(_ name: String, _ group: AgeGroup, _ time: String, _ nr: Int) -> Runner {
Runner(name: name, ageGroup: group, time: time, number: nr)
}
@Test func sortKeyByName() {
let a = runner("Bob", .adult, "10:00", 2)
let b = runner("Ann", .senior, "09:00", 1)
#expect(SortKey.name.sort(b, a))
#expect(!SortKey.name.sort(a, b))
}
@Test func sortKeyByAgeGroup() {
let junior = runner("X", .junior, "10:00", 5)
let senior = runner("A", .senior, "09:00", 1)
#expect(SortKey.ageGroup.sort(junior, senior))
#expect(!SortKey.ageGroup.sort(senior, junior))
}
@Test func sortKeyByTime() {
let fast = runner("X", .adult, "09:00", 2)
let slow = runner("A", .adult, "10:00", 1)
#expect(SortKey.time.sort(fast, slow))
}
@Test func sortKeyByNumber() {
let first = runner("Z", .adult, "10:00", 1)
let second = runner("A", .adult, "09:00", 2)
#expect(SortKey.number.sort(first, second))
}
// MARK: - readFile
@Test func readFileReturnsContents() throws {
let dir = FileManager.default.temporaryDirectory
let url = dir.appendingPathComponent("runnercore-test-\(UUID().uuidString).txt")
try "hello\nworld".write(to: url, atomically: true, encoding: .utf8)
defer { try? FileManager.default.removeItem(at: url) }
#expect(readFile(url.path) == "hello\nworld")
}
@Test func readFileMissingReturnsEmpty() {
#expect(readFile("/no/such/path/\(UUID().uuidString)") == "")
}
// MARK: - parseResults
@Test func parseResultsValidLines() {
let content = "1\t09:30\n2\t10:15"
let results = parseResults(content, separator: "\t")
#expect(results == [1: "09:30", 2: "10:15"])
}
@Test func parseResultsSkipsMalformed() {
let content = "1\t09:30\nnope\n\n3" // "nope" non-int, empty, "3" has <2 cols
let results = parseResults(content, separator: "\t")
#expect(results == [1: "09:30"])
}
@Test func parseResultsDuplicateKeyKeepsLast() {
let content = "1\tfirst\n1\tsecond"
let results = parseResults(content, separator: "\t")
#expect(results == [1: "second"])
}
// MARK: - parseStarters
@Test func parseStartersValidLines() {
let content = "1\t2\tAlice\n7\t3\tBob"
let starters = parseStarters(content, separator: "\t")
#expect(starters.count == 2)
#expect(starters[0].nr == 1)
#expect(starters[0].ageGroup == 2)
#expect(starters[0].name == "Alice")
#expect(starters[1].name == "Bob")
}
@Test func parseStartersSkipsMalformed() {
let content = "1\t2\tAlice\nx\t2\tBad\n5\t2" // non-int nr, too few cols
let starters = parseStarters(content, separator: "\t")
#expect(starters.count == 1)
#expect(starters[0].name == "Alice")
}
// MARK: - assembleRunners
@Test func assembleRunnersMatchesResults() {
let starters = [(nr: 1, ageGroup: 2, name: "Alice"), (nr: 2, ageGroup: 3, name: "Bob")]
let results = [1: "09:30", 2: "10:00"]
let runners = assembleRunners(starters: starters, results: results)
#expect(runners.count == 2)
#expect(runners[0] == Runner(name: "Alice", ageGroup: .adult, time: "09:30", number: 1))
}
@Test func assembleRunnersSkipsMissingResultAndBadGroup() {
let starters = [
(nr: 1, ageGroup: 2, name: "Alice"),
(nr: 2, ageGroup: 9, name: "Bob"),
]
let results = [2: "10:00"]
let runners = assembleRunners(starters: starters, results: results)
#expect(runners.isEmpty)
}
// MARK: - nonParticipants
@Test func nonParticipantsGetDNS() {
let starters = [(nr: 1, ageGroup: 2, name: "Alice"), (nr: 2, ageGroup: 3, name: "Bob")]
let results = [2: "10:00"] // only Bob finished
let absent = nonParticipants(starters: starters, results: results)
#expect(absent == [Runner(name: "Alice", ageGroup: .adult, time: "DNS", number: 1)])
}
@Test func nonParticipantsSkipsBadGroup() {
let starters = [(nr: 1, ageGroup: 9, name: "Ghost")]
let absent = nonParticipants(starters: starters, results: [:])
#expect(absent.isEmpty)
}
// MARK: - visibleRunners
private var sample: [Runner] {
[
runner("Alice", .junior, "09:00", 1),
runner("Bob", .adult, "10:00", 2),
runner("Charlie", .senior, "08:00", 3),
]
}
@Test func visibleRunnersFiltersByGroup() {
let visible = visibleRunners(
sample, query: "", includedGroups: [.adult], sortKey: .number)
#expect(visible.map(\.name) == ["Bob"])
}
@Test func visibleRunnersQueryIsCaseInsensitive() {
let visible = visibleRunners(
sample, query: "aLiC", includedGroups: Set(AgeGroup.allCases), sortKey: .number)
#expect(visible.map(\.name) == ["Alice"])
}
@Test func visibleRunnersEmptyQueryReturnsAllSorted() {
let visible = visibleRunners(
sample, query: "", includedGroups: Set(AgeGroup.allCases), sortKey: .time)
#expect(visible.map(\.name) == ["Charlie", "Alice", "Bob"])
}
@Test func visibleRunnersEmptyWhenNoGroups() {
let visible = visibleRunners(
sample, query: "", includedGroups: [], sortKey: .number)
#expect(visible.isEmpty)
}
// MARK: - toggling
@Test func togglingAddsWhenAbsent() {
#expect(toggling([.junior], .adult) == [.junior, .adult])
}
@Test func togglingRemovesWhenPresent() {
#expect(toggling([.junior, .adult], .adult) == [.junior])
}
// MARK: - menuLabel
@Test func menuLabelOn() {
#expect(menuLabel("Name", on: true) == "[Name]")
}
@Test func menuLabelOff() {
#expect(menuLabel("Name", on: false) == " Name ")
}
// MARK: - formatRow
@Test func formatRowPadsColumns() {
let row = formatRow("#1", "Alice", "Junior", "09:00")
#expect(row == "#1 Alice Junior 09:00")
#expect(row == "#1".padding(toLength: 6, withPad: " ", startingAt: 0)
+ "Alice".padding(toLength: 22, withPad: " ", startingAt: 0)
+ "Junior".padding(toLength: 9, withPad: " ", startingAt: 0)
+ "09:00")
}
@Test func formatRowTruncatesLongFields() {
// padding(toLength:) truncates when the source is longer than the target.
let longName = String(repeating: "x", count: 30)
let row = formatRow("#1", longName, "Junior", "09:00")
#expect(row.contains(String(repeating: "x", count: 22)))
#expect(!row.contains(String(repeating: "x", count: 23)))
}
}