package buildrun import ( "context" "strings" "testing" "time" ) func TestParseCapacityReadsNodeEqualsCountPairs(t *testing.T) { c, err := ParseCapacity(" shanks=3, ace=2 ") if err != nil || c["shanks"] != 3 || c["ace"] != 2 || len(c) != 2 { t.Errorf("capacity = %v, %v", c, err) } if c, err := ParseCapacity(""); err != nil || len(c) != 0 { t.Errorf("empty: %v, %v", c, err) } for _, bad := range []string{"shanks", "shanks=x", "=2", "a=1,a=2", "shanks=-1"} { if _, err := ParseCapacity(bad); err == nil { t.Errorf("%q accepted", bad) } } } func acquireAsync(p *Places, node string) chan func() { got := make(chan func(), 1) go func() { release, err := p.Acquire(context.Background(), node) if err != nil { close(got) return } got <- release }() return got } func TestThreeStepsOnANodeOfCapacityTwoRunTwoAtOnceAndTheThirdStartsWhenOneEnds(t *testing.T) { // round 3 p := &Places{Capacity: map[string]int{"ace": 2}} a, b := acquireAsync(p, "ace"), acquireAsync(p, "ace") ra, rb := <-a, <-b third := acquireAsync(p, "ace") select { case <-third: t.Fatal("a third step started on a node of capacity 2 with two running") case <-time.After(10 * time.Millisecond): } if p.Running()["ace"] != 2 { t.Errorf("running = %v, want 2 on ace", p.Running()) } ra() select { case r := <-third: r() case <-time.After(2 * time.Second): t.Fatal("the third step did not start when one ended") } rb() } func TestANodeWithNoSettingRunsOneStepAtATime(t *testing.T) { // round 3 p := &Places{Capacity: map[string]int{"shanks": 3}} first := <-acquireAsync(p, "g14") second := acquireAsync(p, "g14") select { case <-second: t.Fatal("two steps on a node without a setting") case <-time.After(10 * time.Millisecond): } first() r := <-second r() if p.CapacityOf("g14") != 1 || p.CapacityOf("shanks") != 3 { t.Errorf("capacities = %d, %d", p.CapacityOf("g14"), p.CapacityOf("shanks")) } } func TestAWaitingStepStopsWhenItsContextEnds(t *testing.T) { // round 3 p := &Places{} r := <-acquireAsync(p, "ace") defer r() ctx, cancel := context.WithCancel(context.Background()) done := make(chan error, 1) go func() { _, err := p.Acquire(ctx, "ace"); done <- err }() cancel() select { case err := <-done: if err == nil { t.Error("Acquire returned no error for a cancelled wait") } case <-time.After(2 * time.Second): t.Fatal("a cancelled wait did not return") } } // The operator's decision of 2026-10-21 13:40: the builder builds on shanks and ace only; a capacity of 0 keeps it off a node. func TestACapacityOfZeroKeepsThePartOffTheNodeAndANodeNotNamedKeepsOne(t *testing.T) { // issue 1661 c, err := ParseCapacity("shanks=3,ace=2,g14=0,novox=0") if err != nil || c["g14"] != 0 || c["novox"] != 0 || c["shanks"] != 3 || len(c) != 4 { t.Fatalf("capacity = %v, %v", c, err) } p := &Places{Capacity: c} if p.CapacityOf("g14") != 0 || p.CapacityOf("novox") != 0 || p.CapacityOf("ace") != 2 || p.CapacityOf("other") != DefaultCapacity { t.Errorf("capacities = %d %d %d %d, want 0 0 2 and the default for a node not named", p.CapacityOf("g14"), p.CapacityOf("novox"), p.CapacityOf("ace"), p.CapacityOf("other")) } if _, err := ParseCapacity("shanks=-1"); err == nil { t.Error("a negative capacity accepted") } entries := tcs("go", "go1.27.1", "ace", "g14", "novox", "shanks", "other") for i := 0; i < 6; i++ { nodes, _, err := ChooseNodes("go", entries, p, nil, 2) if err != nil { t.Fatal(err) } for _, n := range nodes { if n == "g14" || n == "novox" { t.Fatalf("choice %d: %v includes a node of capacity 0", i, nodes) } } } // Only nodes of capacity 0 report: nothing qualifies, and the build says why. _, _, err = ChooseNodes("go", tcs("go", "go1.27.1", "g14", "novox"), p, nil, 2) if err == nil || !strings.Contains(err.Error(), "capacity") { t.Errorf("only capacity-0 nodes: error = %v, want it to name the capacity", err) } }