Files
fenrir/internal/reader/diagnostics_test.go

157 lines
5.6 KiB
Go

// Fenrir console screen reader.
// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>.
package reader
import (
"bytes"
"encoding/json"
"log/slog"
"reflect"
"strings"
"testing"
"git.stormux.org/storm/fenrir/internal/command"
"git.stormux.org/storm/fenrir/internal/presentation"
"git.stormux.org/storm/fenrir/internal/screen"
)
func capture_diagnostics(t *testing.T, level slog.Leveler) *bytes.Buffer {
t.Helper()
var log bytes.Buffer
previousLogger := slog.Default()
slog.SetDefault(slog.New(slog.NewJSONHandler(&log, &slog.HandlerOptions{Level: level})))
t.Cleanup(func() { slog.SetDefault(previousLogger) })
return &log
}
func check_update_record(t *testing.T, log *bytes.Buffer, fields map[string]string) map[string]json.RawMessage {
t.Helper()
lines := bytes.Split(bytes.TrimSpace(log.Bytes()), []byte("\n"))
var record map[string]json.RawMessage
if err := json.Unmarshal(lines[len(lines)-1], &record); err != nil {
t.Fatal(err, log.String())
}
fields["msg"] = `"reader update"`
for field, want := range fields {
if got := string(record[field]); got != want {
t.Errorf("%s=%s, want %s", field, got, want)
}
}
if strings.Contains(log.String(), "PRIVATE") {
t.Fatal("diagnostics exposed screen or output text")
}
return record
}
func TestReaderDiagnosticsCursorAndStatusWithoutText(t *testing.T) {
var level slog.LevelVar
level.Set(slog.LevelDebug)
log := capture_diagnostics(t, &level)
r := New()
r.Update(fixture("PRIVATE first", "PRIVATE second", "1/2 PRIVATE"))
r.Command(command.CurrentLine)
r.Command(command.NextLine)
log.Reset()
next := fixture("PRIVATE first", "PRIVATE second", "2/2 PRIVATE")
next.Y = 1
r.Note_input(true, false, false)
if got := r.Update(next); !reflect.DeepEqual(got, []presentation.Message{{Text: "PRIVATE second"}}) {
t.Fatal("diagnostics changed cursor feedback", got)
}
check_update_record(t, log, map[string]string{
"cursor": "[0,1]", "previous_cursor": "[0,0]", "mode": `"cursor"`,
"text_rows": "[2]", "text_cells": "1", "messages": "1", "message_lengths": "[14]",
"path": `"screen-changes"`, "review": "false", "cursor_echo": "true",
})
log.Reset()
level.Set(slog.LevelInfo)
r.Update(next)
if log.Len() != 0 {
t.Fatal("reader diagnostics were not opt-in", log.String())
}
}
func TestReaderDiagnosticsStationaryCursorAndSuppression(t *testing.T) {
for _, testCase := range []struct {
name, mode, path string
review, automatic bool
messages string
}{
{"cursor", "cursor", "screen-changes", false, true, "0"},
{"highlight", "highlight", "highlight", false, true, "1"},
{"review", "cursor", "screen-changes", true, true, "0"},
{"automatic off", "cursor", "automatic-disabled", false, false, "0"},
} {
t.Run(testCase.name, func(t *testing.T) {
log := capture_diagnostics(t, slog.LevelDebug)
before := fixture("PRIVATE first", "PRIVATE second", "third", "status")
before.Attributes = make([]screen.Attribute, len(before.Cells))
next := before
next.Attributes = make([]screen.Attribute, len(before.Cells))
selected := screen.Attribute{Foreground: 7, Background: 4}
for col := range before.Cols {
before.Attributes[col] = selected
next.Attributes[next.Cols+col] = selected
}
r := New()
r.Configure(testCase.automatic, true)
r.Configure_tracking(testCase.mode)
r.Update(before)
if testCase.review {
r.Command(command.CurrentLine)
}
log.Reset()
r.Update(next)
check_update_record(t, log, map[string]string{
"cursor": "[0,0]", "previous_cursor": "[0,0]", "text_rows": "[]",
"attribute_rows": "[0,1]", "attribute_cells": "40", "attributes": "true",
"path": `"` + testCase.path + `"`, "messages": testCase.messages,
"review": map[bool]string{true: "true", false: "false"}[testCase.review],
})
})
}
}
func TestReaderDiagnosticsBoundAttributeDetailsAndHandleSessionChanges(t *testing.T) {
log := capture_diagnostics(t, slog.LevelDebug)
r := New()
r.Update(screen.Snapshot{}) // Inactive captures may have no geometry or cells.
check_update_record(t, log, map[string]string{"path": `"inactive"`, "comparable": "false"})
before := fixture("PRIVATE", "PRIVATE", "PRIVATE", "PRIVATE")
before.Attributes = make([]screen.Attribute, len(before.Cells))
r.Update(before)
check_update_record(t, log, map[string]string{"path": `"session-change"`, "comparable": "false"})
next := before
next.Attributes = make([]screen.Attribute, len(before.Cells))
for index := range next.Attributes {
next.Attributes[index].Background = uint32(index%2 + 1)
}
log.Reset()
r.Update(next)
record := check_update_record(t, log, map[string]string{
"attribute_changes_truncated": "true", "attribute_cells": "80", "attribute_rows": "[0,1,2,3]",
})
var changes []AttributeChange
if err := json.Unmarshal(record["attribute_changes"], &changes); err != nil {
t.Fatal(err)
}
if len(changes) != 32 || changes[0].Row != 0 || changes[0].Start != 0 || changes[0].End != 1 || changes[0].After.Background != 1 {
t.Fatal("attribute details lost geometry/appearance or exceeded their bound", changes)
}
}