| // Copyright 2026 Google LLC |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package layout |
| |
| import ( |
| "reflect" |
| "testing" |
| |
| "github.com/google/go-cmp/cmp" |
| |
| _ "github.com/bazelbuild/rsclient/internal/pkg/testsetup" |
| ) |
| |
| func TestBoundedSize(t *testing.T) { |
| tests := []struct { |
| name string |
| in int |
| want int |
| }{ |
| {"Finite positive", 10, 10}, |
| {"Zero", 0, 0}, |
| {"unbounded", unbounded, 0}, |
| {"Negative other", -100, 0}, |
| } |
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| if got := BoundedSize(tt.in); got != tt.want { |
| t.Errorf("BoundedSize(%d) = %d, want %d", tt.in, got, tt.want) |
| } |
| }) |
| } |
| } |
| |
| func TestResolveSize(t *testing.T) { |
| tests := []struct { |
| name string |
| assigned int |
| intrinsic int |
| want int |
| }{ |
| {"Assigned finite", 10, 50, 10}, |
| {"Assigned zero", 0, 50, 0}, |
| {"Assigned Unlimited (uses intrinsic)", unbounded, 50, 50}, |
| {"Assigned Unlimited (uses zero intrinsic)", unbounded, 0, 0}, |
| {"Assigned negative (clamped to zero)", -5, 50, 0}, |
| } |
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| if got := ResolveSize(tt.assigned, tt.intrinsic); got != tt.want { |
| t.Errorf("ResolveSize(%d, %d) = %d, want %d", tt.assigned, tt.intrinsic, got, tt.want) |
| } |
| }) |
| } |
| } |
| |
| func TestConstraint_IsUnbounded(t *testing.T) { |
| tests := []struct { |
| name string |
| c Constraint |
| want bool |
| }{ |
| {"Finite", Constraint{Min: 10, Max: 20}, false}, |
| {"Zero", Constraint{Min: 0, Max: 0}, false}, |
| {"Unlimited", Constraint{Min: 10, Max: unbounded}, true}, |
| } |
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| if got := tt.c.IsUnbounded(); got != tt.want { |
| t.Errorf("IsUnbounded() = %v, want %v", got, tt.want) |
| } |
| }) |
| } |
| } |
| |
| func TestConstraint_Sum(t *testing.T) { |
| tests := []struct { |
| name string |
| a, b Constraint |
| want Constraint |
| }{ |
| { |
| name: "Finite sum", |
| a: Constraint{Min: 1, Max: 5, Weight: 1}, |
| b: Constraint{Min: 2, Max: 10, Weight: 2}, |
| want: Constraint{Min: 3, Max: 15, Weight: 3}, |
| }, |
| { |
| name: "One unlimited", |
| a: Constraint{Min: 1, Max: unbounded, Weight: 1}, |
| b: Constraint{Min: 2, Max: 10, Weight: 2}, |
| want: Constraint{Min: 3, Max: unbounded, Weight: 3}, |
| }, |
| { |
| name: "Both unlimited", |
| a: Constraint{Min: 1, Max: unbounded, Weight: 1}, |
| b: Constraint{Min: 2, Max: unbounded, Weight: 2}, |
| want: Constraint{Min: 3, Max: unbounded, Weight: 3}, |
| }, |
| } |
| |
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| if got := tt.a.Sum(tt.b); !reflect.DeepEqual(got, tt.want) { |
| t.Errorf("Sum() = %v, want %v", got, tt.want) |
| } |
| }) |
| } |
| } |
| |
| func TestConstraintSum(t *testing.T) { |
| tests := []struct { |
| name string |
| constraints []Constraint |
| want Constraint |
| }{ |
| { |
| name: "Empty", |
| constraints: nil, |
| want: Constraint{}, |
| }, |
| { |
| name: "Multiple finite", |
| constraints: []Constraint{ |
| {Min: 1, Max: 5, Weight: 1}, |
| {Min: 2, Max: 6, Weight: 2}, |
| {Min: 3, Max: 7, Weight: 3}, |
| }, |
| want: Constraint{Min: 6, Max: 18, Weight: 6}, |
| }, |
| { |
| name: "Mixed with unlimited", |
| constraints: []Constraint{ |
| {Min: 1, Max: 5, Weight: 1}, |
| {Min: 2, Max: unbounded, Weight: 2}, |
| {Min: 3, Max: 7, Weight: 3}, |
| }, |
| want: Constraint{Min: 6, Max: unbounded, Weight: 6}, |
| }, |
| } |
| |
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| if got := ConstraintSum(tt.constraints); !reflect.DeepEqual(got, tt.want) { |
| t.Errorf("ConstraintSum() = %v, want %v", got, tt.want) |
| } |
| }) |
| } |
| } |
| |
| func TestDistributeProportionally(t *testing.T) { |
| tests := []struct { |
| name string |
| total int |
| naturalWidths []int |
| want []int |
| }{ |
| { |
| name: "equal distribution", |
| total: 10, |
| naturalWidths: []int{5, 5}, |
| want: []int{5, 5}, |
| }, |
| { |
| name: "weighted distribution", |
| total: 12, |
| naturalWidths: []int{10, 20}, |
| want: []int{5, 7}, |
| }, |
| { |
| name: "rounding surplus", |
| total: 10, |
| naturalWidths: []int{3, 3, 3}, |
| want: []int{4, 3, 3}, // 3.33 each, first one gets the remainder |
| }, |
| { |
| name: "minimum 1 requirement", |
| total: 5, |
| naturalWidths: []int{100, 100, 100, 100, 100}, |
| want: []int{1, 1, 1, 1, 1}, |
| }, |
| { |
| name: "empty", |
| total: 10, |
| naturalWidths: nil, |
| want: nil, |
| }, |
| } |
| |
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| got := DistributeProportionally(tt.total, tt.naturalWidths) |
| if diff := cmp.Diff(tt.want, got); diff != "" { |
| t.Errorf("DistributeProportionally() mismatch (-want +got):\n%s", diff) |
| } |
| if got != nil { |
| sum := 0 |
| for _, w := range got { |
| sum += w |
| } |
| if sum != tt.total { |
| t.Errorf("Sum = %d, want %d", sum, tt.total) |
| } |
| } |
| }) |
| } |
| } |
| |
| func TestDistribute(t *testing.T) { |
| tests := []struct { |
| name string |
| total int |
| constraints []Constraint |
| want []int |
| }{ |
| { |
| name: "even split no weights", |
| total: 10, |
| constraints: []Constraint{ |
| {Min: 1, Max: unbounded}, |
| {Min: 1, Max: unbounded}, |
| }, |
| want: []int{5, 5}, |
| }, |
| { |
| name: "uneven split no weights", |
| total: 11, |
| constraints: []Constraint{ |
| {Min: 1, Max: unbounded}, |
| {Min: 1, Max: unbounded}, |
| }, |
| want: []int{6, 5}, |
| }, |
| { |
| name: "weighted split", |
| total: 100, |
| constraints: []Constraint{ |
| {Min: 1, Max: unbounded, Weight: 1}, |
| {Min: 1, Max: unbounded, Weight: 3}, |
| }, |
| want: []int{26, 74}, |
| }, |
| { |
| name: "respect minimums", |
| total: 10, |
| constraints: []Constraint{ |
| {Min: 8, Max: unbounded, Weight: 1}, |
| {Min: 1, Max: unbounded, Weight: 1}, |
| }, |
| want: []int{9, 1}, |
| }, |
| { |
| name: "oversubscribed total", |
| total: 5, |
| constraints: []Constraint{ |
| {Min: 4, Max: unbounded}, |
| {Min: 4, Max: unbounded}, |
| }, |
| want: []int{4, 1}, // Squashes second tile to fit total. |
| }, |
| { |
| name: "respect maximums", |
| total: 100, |
| constraints: []Constraint{ |
| {Min: 10, Max: 20, Weight: 1}, |
| {Min: 10, Max: 100, Weight: 1}, |
| }, |
| want: []int{20, 80}, |
| }, |
| { |
| name: "all at maximums", |
| total: 100, |
| constraints: []Constraint{ |
| {Min: 10, Max: 20, Weight: 1}, |
| {Min: 10, Max: 30, Weight: 1}, |
| }, |
| want: []int{20, 30}, // Total sum 50 < 100. |
| }, |
| { |
| name: "iterative locking with weights", |
| total: 100, |
| constraints: []Constraint{ |
| {Min: 0, Max: 10, Weight: 1}, // Hits Max fast |
| {Min: 0, Max: 40, Weight: 2}, // Hits Max later |
| {Min: 0, Max: 100, Weight: 2}, |
| }, |
| want: []int{10, 40, 50}, |
| }, |
| { |
| name: "remainder redistribution respects max", |
| total: 11, |
| constraints: []Constraint{ |
| {Min: 5, Max: 5, Weight: 0}, |
| {Min: 5, Max: 10, Weight: 0}, |
| }, |
| want: []int{5, 6}, |
| }, |
| } |
| |
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| if got := Distribute(tt.total, tt.constraints); !reflect.DeepEqual(got, tt.want) { |
| t.Errorf("Distribute() = %v, want %v", got, tt.want) |
| } |
| }) |
| } |
| } |