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>
216 lines
7.3 KiB
Swift
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)))
|
|
}
|
|
}
|