Ctrl K
ComunidadGuías…

internal/issues/harness_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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
}