a73x

internal/agent/hyperlog/hyperlog_test.go

Ref:   Size: 3.0 KiB   History

package hyperlog

import (
	"os"
	"path/filepath"
	"strings"
	"testing"
)

// write puts body in a temp file and returns its path.
func write(t *testing.T, body string) string {
	t.Helper()
	p := filepath.Join(t.TempDir(), "hv.log")
	if err := os.WriteFile(p, []byte(body), 0o600); err != nil {
		t.Fatal(err)
	}
	return p
}

func TestReason(t *testing.T) {
	for _, tc := range []struct {
		name, body, want string
	}{
		{"last line wins", "starting\nvcpu 0 ready\nError: VmBoot(DeviceManager)\n",
			"Error: VmBoot(DeviceManager)"},
		{"trailing blank lines skipped", "Error: no bootable device\n\n\n   \n",
			"Error: no bootable device"},
		{"no trailing newline", "line one\nfatal: bad kernel", "fatal: bad kernel"},
		{"single line", "vfkit: failed to start", "vfkit: failed to start"},
		{"empty file", "", ""},
		{"whitespace only", "\n\n   \t\n", ""},
		{"collapses whitespace", "Error:   too    many\tspaces\n", "Error: too many spaces"},
		{"strips control characters", "Error: bad\x00 image\x07\n", "Error: bad image"},
		{"truncates at an ANSI escape", "\x1b[31mError: red\x1b[0m\n", ""},
	} {
		t.Run(tc.name, func(t *testing.T) {
			if got := Reason(write(t, tc.body)); got != tc.want {
				t.Errorf("Reason() = %q, want %q", got, tc.want)
			}
		})
	}
}

// TestReasonMissingFile pins that an absent log is "nothing to add" rather than
// an error the caller has to handle: a VM that never started writes no log, and
// that is not itself a fault worth reporting.
func TestReasonMissingFile(t *testing.T) {
	if got := Reason(filepath.Join(t.TempDir(), "absent.log")); got != "" {
		t.Errorf("Reason() on a missing file = %q, want empty", got)
	}
	if got := Reason(t.TempDir()); got != "" {
		t.Errorf("Reason() on a directory = %q, want empty", got)
	}
}

// TestReasonReadsOnlyTheTail pins that a long-lived log is not pulled into
// memory whole, and that the partial first line of the read window is never
// mistaken for the reason.
func TestReasonReadsOnlyTheTail(t *testing.T) {
	body := strings.Repeat("chatter chatter chatter\n", 4000) + "Error: the last word\n"
	if got := Reason(write(t, body)); got != "Error: the last word" {
		t.Errorf("Reason() = %q, want the final line", got)
	}
	// A window that lands mid-line must not quote the fragment: a file that is
	// one enormous line has no complete trailing line to report.
	if got := Reason(write(t, strings.Repeat("x", 40<<10))); got != "" {
		t.Errorf("Reason() on one huge line = %q, want empty", got)
	}
}

// TestReasonBoundsLength pins the cap: last_error is a database column shown in
// the console, so a runaway line is cut rather than stored whole.
func TestReasonBoundsLength(t *testing.T) {
	got := Reason(write(t, "Error: "+strings.Repeat("verbose ", 200)+"\n"))
	if len(got) > maxLen+len("…") {
		t.Errorf("Reason() len = %d, want <= %d", len(got), maxLen+len("…"))
	}
	if !strings.HasSuffix(got, "…") {
		t.Errorf("a truncated reason must say so, got %q", got)
	}
	if !strings.HasPrefix(got, "Error: verbose") {
		t.Errorf("truncation must keep the START of the line, got %q", got)
	}
}