Ctrl K
ComunidadGuías…

internal/logs/stream_test.go

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

Archivo original ↗Preguntar a KiBot
package logs

import (
	"context"
	"net/http"
	"net/http/httptest"
	"net/url"
	"testing"
	"time"

	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/rest"
)

func clientFor(t *testing.T, handler http.HandlerFunc) kubernetes.Interface {
	t.Helper()
	ts := httptest.NewServer(handler)
	t.Cleanup(ts.Close)
	cs, err := kubernetes.NewForConfig(&rest.Config{Host: ts.URL})
	if err != nil {
		t.Fatalf("clientset: %v", err)
	}
	return cs
}

func collect(t *testing.T, stream *Stream, want int) []string {
	t.Helper()
	lines := []string{}
	deadline := time.After(5 * time.Second)
	for len(lines) < want {
		select {
		case line, ok := <-stream.Lines:
			if !ok {
				return lines
			}
			lines = append(lines, line.Text)
		case <-deadline:
			t.Fatalf("timed out after %d lines", len(lines))
		}
	}
	return lines
}

func TestOpenStreamsLines(t *testing.T) {
	cs := clientFor(t, func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte("first\nsecond\nthird\n"))
	})

	stream, err := Open(context.Background(), cs, Request{Namespace: "flux-system", Name: "web"})
	if err != nil {
		t.Fatalf("open: %v", err)
	}
	defer stream.Close()

	lines := collect(t, stream, 3)
	if len(lines) != 3 {
		t.Fatalf("lines = %v, want 3", lines)
	}
	if lines[0] != "first" || lines[2] != "third" {
		t.Fatalf("lines = %v", lines)
	}

	if _, ok := <-stream.Lines; ok {
		t.Fatalf("channel stayed open after the body ended")
	}
}

func TestACleanEndCarriesNoError(t *testing.T) {
	cs := clientFor(t, func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte("first\nsecond\n"))
	})

	stream, err := Open(context.Background(), cs, Request{Namespace: "flux-system", Name: "web"})
	if err != nil {
		t.Fatalf("open: %v", err)
	}
	defer stream.Close()
	drainLines(t, stream)

	if stream.Err() != nil {
		t.Fatalf("err = %v, want none for a pod that finished logging", stream.Err())
	}
}

func TestALineTooLongToReadIsReportedRatherThanEndingTheStreamQuietly(t *testing.T) {
	huge := make([]byte, maxLineBytes+1)
	for i := range huge {
		huge[i] = 'x'
	}
	cs := clientFor(t, func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte("first\n"))
		_, _ = w.Write(huge)
		_, _ = w.Write([]byte("\n"))
	})

	stream, err := Open(context.Background(), cs, Request{Namespace: "flux-system", Name: "web", Follow: true})
	if err != nil {
		t.Fatalf("open: %v", err)
	}
	defer stream.Close()
	drainLines(t, stream)

	if stream.Err() == nil {
		t.Fatal("a log line the reader could not take ended the stream as if the pod had finished")
	}
}

func TestClosingAStreamIsNotAFailure(t *testing.T) {
	cs := clientFor(t, func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte("first\n"))
		flush(w)
		<-r.Context().Done()
	})

	stream, err := Open(context.Background(), cs, Request{Namespace: "flux-system", Name: "web", Follow: true})
	if err != nil {
		t.Fatalf("open: %v", err)
	}
	collect(t, stream, 1)
	stream.Close()
	drainLines(t, stream)

	if stream.Err() != nil {
		t.Fatalf("err = %v, want a deliberate close to read as a clean end", stream.Err())
	}
}

func drainLines(t *testing.T, stream *Stream) {
	t.Helper()
	deadline := time.After(5 * time.Second)
	for {
		select {
		case _, ok := <-stream.Lines:
			if !ok {
				return
			}
		case <-deadline:
			t.Fatal("the stream never ended")
		}
	}
}

func TestOpenSendsLogOptions(t *testing.T) {
	query := ""
	path := ""
	cs := clientFor(t, func(w http.ResponseWriter, r *http.Request) {
		query = r.URL.RawQuery
		path = r.URL.Path
		_, _ = w.Write([]byte("line\n"))
	})

	stream, err := Open(context.Background(), cs, Request{
		Namespace: "flux-system",
		Name:      "web",
		Container: "app",
		TailLines: 100,
		Follow:    true,
	})
	if err != nil {
		t.Fatalf("open: %v", err)
	}
	defer stream.Close()
	collect(t, stream, 1)

	if path != "/api/v1/namespaces/flux-system/pods/web/log" {
		t.Fatalf("path = %q", path)
	}
	values, parseErr := url.ParseQuery(query)
	if parseErr != nil {
		t.Fatalf("parse query %q: %v", query, parseErr)
	}
	if values.Get("container") != "app" {
		t.Fatalf("container = %q, want app", values.Get("container"))
	}
	if values.Get("follow") != "true" {
		t.Fatalf("follow = %q, want true", values.Get("follow"))
	}
	if values.Get("tailLines") != "100" {
		t.Fatalf("tailLines = %q, want 100", values.Get("tailLines"))
	}
}

func TestOpenOmitsTailLinesWhenUnset(t *testing.T) {
	query := ""
	cs := clientFor(t, func(w http.ResponseWriter, r *http.Request) {
		query = r.URL.RawQuery
		_, _ = w.Write([]byte("line\n"))
	})

	stream, err := Open(context.Background(), cs, Request{Namespace: "flux-system", Name: "web"})
	if err != nil {
		t.Fatalf("open: %v", err)
	}
	defer stream.Close()
	collect(t, stream, 1)

	values, parseErr := url.ParseQuery(query)
	if parseErr != nil {
		t.Fatalf("parse query %q: %v", query, parseErr)
	}
	if values.Has("tailLines") {
		t.Fatalf("tailLines present without a request value: %q", query)
	}
}

func TestOpenReturnsErrorFromAPI(t *testing.T) {
	cs := clientFor(t, func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusInternalServerError)
	})

	_, err := Open(context.Background(), cs, Request{Namespace: "flux-system", Name: "web"})
	if err == nil {
		t.Fatalf("expected an error from a failing log request")
	}
}

func TestCloseStopsAFollowingStream(t *testing.T) {
	released := make(chan struct{})
	cs := clientFor(t, func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte("first\n"))
		flush(w)
		<-r.Context().Done()
		close(released)
	})

	stream, err := Open(context.Background(), cs, Request{Namespace: "flux-system", Name: "web", Follow: true})
	if err != nil {
		t.Fatalf("open: %v", err)
	}
	lines := collect(t, stream, 1)
	if len(lines) != 1 || lines[0] != "first" {
		t.Fatalf("lines = %v", lines)
	}

	stream.Close()
	stream.Close()

	select {
	case <-released:
	case <-time.After(5 * time.Second):
		t.Fatalf("server request was not canceled by Close")
	}

	deadline := time.After(5 * time.Second)
	for {
		select {
		case _, ok := <-stream.Lines:
			if !ok {
				return
			}
		case <-deadline:
			t.Fatalf("channel never closed after Close")
		}
	}
}

func TestCloseUnblocksAFullBuffer(t *testing.T) {
	cs := clientFor(t, func(w http.ResponseWriter, r *http.Request) {
		for range lineBuffer * 2 {
			_, _ = w.Write([]byte("line\n"))
		}
		flush(w)
		<-r.Context().Done()
	})

	stream, err := Open(context.Background(), cs, Request{Namespace: "flux-system", Name: "web", Follow: true})
	if err != nil {
		t.Fatalf("open: %v", err)
	}

	deadline := time.After(5 * time.Second)
	for len(stream.Lines) < lineBuffer {
		select {
		case <-deadline:
			t.Fatalf("buffer never filled (%d lines)", len(stream.Lines))
		default:
		}
	}

	stream.Close()

	drain := time.After(5 * time.Second)
	for {
		select {
		case _, ok := <-stream.Lines:
			if !ok {
				return
			}
		case <-drain:
			t.Fatalf("channel never closed after Close with a full buffer")
		}
	}
}

func TestCancelledContextStopsTheStream(t *testing.T) {
	cs := clientFor(t, func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte("first\n"))
		flush(w)
		<-r.Context().Done()
	})

	ctx, cancel := context.WithCancel(context.Background())
	stream, err := Open(ctx, cs, Request{Namespace: "flux-system", Name: "web", Follow: true})
	if err != nil {
		t.Fatalf("open: %v", err)
	}
	defer stream.Close()
	collect(t, stream, 1)

	cancel()

	deadline := time.After(5 * time.Second)
	for {
		select {
		case _, ok := <-stream.Lines:
			if !ok {
				return
			}
		case <-deadline:
			t.Fatalf("channel never closed after the context was canceled")
		}
	}
}

func flush(w http.ResponseWriter) {
	flusher, ok := w.(http.Flusher)
	if !ok {
		return
	}
	flusher.Flush()
}