Nucleic: Gitea Runner macOS VM Support
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
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-"))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user