import Foundation import Testing @testable import RunnerCore /// Tests for ``LabelSet`` matching against a job's bare `runs-on` labels. /// /// The semantics under test are exactly: a job matches iff its label array is /// non-empty and every entry is one of ours. That is a subset test with an /// explicit carve-out for the empty set, which subset semantics would otherwise /// accept. @Suite("LabelSet") struct LabelsTests { // MARK: - Construction @Test("bare names are kept verbatim") func bareNamesAreKept() { let set = LabelSet(["macos-arm64", "macos"]) #expect(set.names == ["macos-arm64", "macos"]) } @Test("a ':schema' suffix is stripped at construction") func schemaSuffixIsStripped() { // Safe to hand this the same array the config file holds, even if an // operator wrote the registration form by mistake. let set = LabelSet(["macos-arm64:host", "macos:docker"]) #expect(set.names == ["macos-arm64", "macos"]) } @Test("empty and whitespace-only names are dropped") func emptyNamesAreDropped() { let set = LabelSet(["macos-arm64", "", " ", ":host"]) #expect(set.names == ["macos-arm64"]) } @Test("bareName strips at the first colon only") func bareNameStripsAtFirstColon() { #expect(LabelSet.bareName("macos-arm64") == "macos-arm64") #expect(LabelSet.bareName("macos-arm64:host") == "macos-arm64") #expect(LabelSet.bareName("a:b:c") == "a") #expect(LabelSet.bareName(" macos:host") == "macos") } // MARK: - Matching @Test("an exact single-label match succeeds") func exactMatch() { #expect(LabelSet(["macos-arm64"]).matches(jobLabels: ["macos-arm64"])) } @Test("a job asking for a subset of our labels matches") func subsetMatches() { // The runner is a superset: it advertises more than the job needs. let set = LabelSet(["macos-arm64", "macos", "self-hosted"]) #expect(set.matches(jobLabels: ["macos-arm64"])) #expect(set.matches(jobLabels: ["macos-arm64", "self-hosted"])) #expect(set.matches(jobLabels: ["macos-arm64", "macos", "self-hosted"])) } @Test("a job asking for anything we lack does not match") func supersetDoesNotMatch() { let set = LabelSet(["macos-arm64"]) #expect(!set.matches(jobLabels: ["ubuntu-latest"])) // One unknown label is enough to disqualify the whole job. #expect(!set.matches(jobLabels: ["macos-arm64", "xcode-16"])) } @Test("an empty job label array never matches") func emptyJobLabelsDoNotMatch() { // Subset semantics alone would say yes — the empty set is a subset of // everything. A job that declares no requirement must not consume one of // two scarce macOS VMs. #expect(!LabelSet(["macos-arm64"]).matches(jobLabels: [])) #expect(!LabelSet([]).matches(jobLabels: [])) } @Test("an empty runner label set matches nothing") func emptyRunnerLabelsMatchNothing() { #expect(!LabelSet([]).matches(jobLabels: ["macos-arm64"])) } @Test("matching is case-sensitive") func matchingIsCaseSensitive() { // Gitea compares labels case-sensitively, so `macOS-ARM64` in a workflow // is a different label from `macos-arm64` and must not be claimed. let set = LabelSet(["macos-arm64"]) #expect(!set.matches(jobLabels: ["macOS-ARM64"])) #expect(!set.matches(jobLabels: ["MACOS-ARM64"])) #expect(set.matches(jobLabels: ["macos-arm64"])) } @Test("a schema suffix on the job side is stripped before matching") func jobLabelsAreNormalized() { // The server stores bare names, so this is unusual — but a workflow that // writes `runs-on: [macos-arm64:host]` would otherwise be skipped // silently, with no log line to explain why. let set = LabelSet(["macos-arm64", "macos"]) #expect(set.matches(jobLabels: ["macos-arm64:host"])) #expect(set.matches(jobLabels: ["macos-arm64:host", "macos"])) #expect(!set.matches(jobLabels: ["ubuntu-latest:host"])) // A label that is nothing but a schema separator is not a match. #expect(!set.matches(jobLabels: [":host"])) } @Test("duplicate job labels are harmless") func duplicateJobLabels() { #expect(LabelSet(["macos-arm64"]).matches(jobLabels: ["macos-arm64", "macos-arm64"])) } // MARK: - Registration argument @Test("the registration argument appends the schema to every name") func registrationArgumentAppendsSchema() { #expect(LabelSet(["macos-arm64"]).registrationArgument() == "macos-arm64:host") #expect( LabelSet(["macos-arm64", "macos"]).registrationArgument() == "macos:host,macos-arm64:host") } @Test("the registration argument is stable across calls") func registrationArgumentIsStable() { // `names` is a Set; sorting is what keeps the guest command line — and // therefore its logs — reproducible. let set = LabelSet(["zulu", "alpha", "mike"]) #expect(set.registrationArgument() == set.registrationArgument()) #expect(set.registrationArgument() == "alpha:host,mike:host,zulu:host") } @Test("a non-default schema is honoured") func registrationArgumentWithCustomSchema() { #expect(LabelSet(["ubuntu"]).registrationArgument(schema: "docker") == "ubuntu:docker") } @Test("an empty label set produces an empty registration argument") func registrationArgumentOfEmptySet() { #expect(LabelSet([]).registrationArgument() == "") } } /// Tests for ``RunnerNaming``. @Suite("RunnerNaming") struct RunnerNamingTests { @Test("a generated name carries the prefix and a lowercase UUID") func generatedNameShape() throws { let name = RunnerNaming.makeRunnerName(prefix: "macos-vm-") #expect(name.hasPrefix("macos-vm-")) let suffix = String(name.dropFirst("macos-vm-".count)) #expect(suffix == suffix.lowercased()) #expect(UUID(uuidString: suffix) != nil) } @Test("generated names are unique") func generatedNamesAreUnique() { // Uniqueness is what lets the reconcile loop decide a row is ours and // unbacked; a collision would make that decision unsound. let names = Set((0..<200).map { _ in RunnerNaming.makeRunnerName(prefix: "macos-vm-") }) #expect(names.count == 200) } @Test("prefix detection matches only our names") func prefixDetection() { #expect(RunnerNaming.hasPrefix("macos-vm-abc", prefix: "macos-vm-")) #expect(!RunnerNaming.hasPrefix("linux-runner-1", prefix: "macos-vm-")) #expect(!RunnerNaming.hasPrefix("MACOS-VM-abc", prefix: "macos-vm-")) #expect(!RunnerNaming.hasPrefix("x-macos-vm-abc", prefix: "macos-vm-")) } @Test("an empty prefix matches nothing") func emptyPrefixMatchesNothing() { // Otherwise the reconcile loop would consider every runner on the // instance — including other hosts' — a deletion candidate. #expect(!RunnerNaming.hasPrefix("anything", prefix: "")) } @Test("a generated name is recognized by its own prefix") func roundTrip() { let name = RunnerNaming.makeRunnerName(prefix: "macos-vm-") #expect(RunnerNaming.hasPrefix(name, prefix: "macos-vm-")) } }