internal/issues/harness_test.go
Rama main · réplica de lectura del repositorio original.
package issues
import (
"context"
"maps"
"slices"
"strings"
"sync"
"testing"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/types"
"github.com/sophotechlabs/spinoza/internal/api"
)
var testNow = time.Date(2026, time.August, 28, 12, 0, 0, 0, time.UTC)
const (
phasePending = "Pending"
phaseRunning = "Running"
)
type stubLister struct {
mu sync.Mutex
items map[string][]*unstructured.Unstructured
errs map[string]error
panics map[string]bool
cached []api.ResourceDescriptor
leased []api.ResourceDescriptor
}
func (s *stubLister) Lease(
_ context.Context,
desc api.ResourceDescriptor,
) ([]*unstructured.Unstructured, error) {
s.mu.Lock()
s.leased = append(s.leased, desc)
s.mu.Unlock()
if s.panics[desc.Resource] {
panic("broken resource reader")
}
err, refused := s.errs[desc.Resource]
if refused {
return nil, err
}
return s.items[desc.Resource], nil
}
func (s *stubLister) Cached() []api.ResourceDescriptor {
return s.cached
}
type stubEvents struct {
mu sync.Mutex
byUID map[string][]api.Event
err error
panics bool
asked []string
}
func (s *stubEvents) Events(_ context.Context, _, uid string) ([]api.Event, error) {
s.mu.Lock()
s.asked = append(s.asked, uid)
s.mu.Unlock()
if s.panics {
panic("broken event reader")
}
if s.err != nil {
return nil, s.err
}
return s.byUID[uid], nil
}
func (s *stubEvents) askedAbout() []string {
s.mu.Lock()
defer s.mu.Unlock()
out := slices.Clone(s.asked)
slices.Sort(out)
return out
}
func descriptor(group, version, resource, kind string) api.ResourceDescriptor {
return api.ResourceDescriptor{
Group: group,
Version: version,
Resource: resource,
Kind: kind,
Namespaced: true,
}
}
func podDescriptor() api.ResourceDescriptor {
return descriptor("", "v1", "pods", kindPod)
}
func replicaSetDescriptor() api.ResourceDescriptor {
return descriptor(appsGroup, "v1", "replicasets", kindReplicaSet)
}
func deploymentDescriptor() api.ResourceDescriptor {
return descriptor(appsGroup, "v1", "deployments", kindDeployment)
}
func catalog(descs ...api.ResourceDescriptor) map[string]api.ResourceDescriptor {
out := map[string]api.ResourceDescriptor{}
for _, desc := range descs {
out[keyOf(desc)] = desc
}
return out
}
type podOption func(pod *unstructured.Unstructured)
func newPod(name string, options ...podOption) *unstructured.Unstructured {
pod := &unstructured.Unstructured{Object: map[string]any{
"apiVersion": "v1",
"kind": kindPod,
"metadata": map[string]any{
"name": name,
"namespace": "default",
"uid": "uid-" + name,
"creationTimestamp": testNow.Add(-time.Hour).Format(time.RFC3339),
},
"spec": map[string]any{"nodeName": "node-a"},
"status": map[string]any{"phase": phaseRunning},
}}
for _, apply := range options {
apply(pod)
}
return pod
}
func withPhase(phase string) podOption {
return func(pod *unstructured.Unstructured) {
setNested(pod, phase, "status", "phase")
}
}
func withContainer(name string, state map[string]any) podOption {
return func(pod *unstructured.Unstructured) {
entry := map[string]any{"name": name, "state": state}
appendNested(pod, entry, "status", "containerStatuses")
}
}
func withContainerEntry(entry map[string]any) podOption {
return func(pod *unstructured.Unstructured) {
appendNested(pod, entry, "status", "containerStatuses")
}
}
func withOwner(kind, name, uid string) podOption {
return func(pod *unstructured.Unstructured) {
controller := true
pod.SetOwnerReferences([]metav1.OwnerReference{{
APIVersion: "apps/v1",
Kind: kind,
Name: name,
UID: types.UID(uid),
Controller: &controller,
}})
}
}
func withStartTime(at time.Time) podOption {
return func(pod *unstructured.Unstructured) {
setNested(pod, at.Format(time.RFC3339), "status", "startTime")
}
}
func withNode(name string) podOption {
return func(pod *unstructured.Unstructured) {
setNested(pod, name, "spec", "nodeName")
}
}
func withPodCondition(entry map[string]any) podOption {
return func(pod *unstructured.Unstructured) {
appendNested(pod, entry, "status", "conditions")
}
}
func withDeleted() podOption {
return func(pod *unstructured.Unstructured) {
stamp := metav1.NewTime(testNow)
pod.SetDeletionTimestamp(&stamp)
}
}
func setNested(obj *unstructured.Unstructured, value any, fields ...string) {
err := unstructured.SetNestedField(obj.Object, value, fields...)
if err != nil {
panic(err)
}
}
func appendNested(obj *unstructured.Unstructured, entry map[string]any, fields ...string) {
existing, _, _ := unstructured.NestedSlice(obj.Object, fields...)
err := unstructured.SetNestedSlice(obj.Object, append(existing, entry), fields...)
if err != nil {
panic(err)
}
}
func newWorkload(kind, name, uid string, status, spec map[string]any) *unstructured.Unstructured {
return &unstructured.Unstructured{Object: map[string]any{
"apiVersion": "apps/v1",
"kind": kind,
"metadata": map[string]any{
"name": name,
"namespace": "default",
"uid": uid,
"generation": int64(3),
"creationTimestamp": testNow.Add(-time.Hour).Format(time.RFC3339),
},
"spec": spec,
"status": status,
}}
}
func condition(kind, status string, extras map[string]any) map[string]any {
entry := map[string]any{
"type": kind,
"status": status,
"lastTransitionTime": testNow.Add(-30 * time.Minute).Format(time.RFC3339),
}
maps.Copy(entry, extras)
return entry
}
func build(t *testing.T, lister *stubLister, descs map[string]api.ResourceDescriptor) api.IssueQueue {
t.Helper()
return buildLimited(t, lister, &stubEvents{}, descs, testLimits())
}
func testLimits() Limits {
return Limits{
Budget: 2 * time.Second,
StallBudget: time.Second,
StallGrace: 5 * time.Minute,
ReadyGrace: 2 * time.Minute,
}
}
func buildLimited(
t *testing.T,
lister Lister,
events Events,
descs map[string]api.ResourceDescriptor,
limits Limits,
) api.IssueQueue {
t.Helper()
return Build(t.Context(), lister, events, descs, func() time.Time { return testNow }, limits)
}
func rowNamed(queue api.IssueQueue, name string) (api.Issue, bool) {
for _, row := range queue.Rows {
if row.Object.Name == name {
return row, true
}
}
return api.Issue{}, false
}
func itemsOf(resource string, objs ...*unstructured.Unstructured) map[string][]*unstructured.Unstructured {
return map[string][]*unstructured.Unstructured{resource: objs}
}
func contains(haystack, needle string) bool {
return strings.Contains(haystack, needle)
}
func metaNow() metav1.Time {
return metav1.NewTime(testNow)
}
func ownerReference(kind, name, uid string, controller *bool) []metav1.OwnerReference {
return []metav1.OwnerReference{{
APIVersion: "apps/v1",
Kind: kind,
Name: name,
UID: types.UID(uid),
Controller: controller,
}}
}
func snapshotOf(items ...object) *snapshot {
snap := newSnapshot()
for _, item := range items {
snap.byUID[item.uid()] = item
key := item.desc.Group + "/" + item.desc.Kind
snap.byKind[key] = append(snap.byKind[key], item)
owner := controllerUID(item.obj)
if owner == "" {
continue
}
snap.byOwner[owner] = append(snap.byOwner[owner], item)
}
return snap
}
type countingEvents struct {
mu sync.Mutex
live int
peak int
}
func (c *countingEvents) Events(_ context.Context, _, _ string) ([]api.Event, error) {
c.mu.Lock()
c.live++
if c.live > c.peak {
c.peak = c.live
}
c.mu.Unlock()
time.Sleep(20 * time.Millisecond)
c.mu.Lock()
c.live--
c.mu.Unlock()
return []api.Event{{Reason: "Pulling"}}, nil
}
func (c *countingEvents) highWater() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.peak
}
func (s *stubLister) leasedResources() []string {
s.mu.Lock()
defer s.mu.Unlock()
out := make([]string, 0, len(s.leased))
for _, desc := range s.leased {
out = append(out, desc.Resource)
}
return out
}