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)
}
}