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