Files
gosentry/src/app/format_test.go
T
mix 263717874c fix: remove no-op SaveJobs calls, fix PendingRuns lifecycle and cap
Implements items 4-5 of the whole-project review's suggested order
(docs/PROJECT_REVIEW_PLAN.md):

- Drop the three SaveJobs calls in the run lifecycle (startRunLocked,
  executeRun, SetGlobalPause): none of them change a durable Job field,
  everything they touch lives on JobRuntime, which is never persisted.
  Retire TestStartRunLockedRollbackOnSaveFailure with the rollback it
  guarded, since a run can no longer fail to start this way.
- Clear PendingRuns (the "queue" overlap policy's backlog) when a job is
  disabled or the scheduler is globally paused, so resuming or
  re-enabling a job no longer replays a deferred run left over from
  before the pause/disable. Cap it at maxPendingRuns (10) so a job whose
  runs take longer than its own interval stops accumulating an unbounded
  backlog. Surface the queued count in the details pane via DisplayStats.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 17:06:55 +03:00

195 lines
7.1 KiB
Go

package app
import (
"testing"
"gitea.mixdep.ru/mix/gosentry/src/domain"
)
func TestStatusText(t *testing.T) {
tests := []struct {
name string
job domain.Job
runtime *domain.JobRuntime
want string
}{
{"disabled is paused", domain.Job{Enabled: false}, &domain.JobRuntime{LastState: "Running"}, "Paused"},
{"enabled shows runtime state", domain.Job{Enabled: true}, &domain.JobRuntime{LastState: "Success"}, "Success"},
{"enabled with nil runtime is empty", domain.Job{Enabled: true}, nil, ""},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if got := StatusText(tc.job, tc.runtime); got != tc.want {
t.Errorf("StatusText = %q, want %q", got, tc.want)
}
})
}
}
func TestEventText(t *testing.T) {
withLog := domain.RunRecord{
Time: "2026-06-19 12:00:00", Trigger: "Schedule", JobName: "Build",
State: "Success", Detail: "ok", LogFile: "build.log",
}
if got, want := EventText(withLog), "2026-06-19 12:00:00 Schedule Build Success ok build.log"; got != want {
t.Errorf("EventText with log = %q, want %q", got, want)
}
noLog := domain.RunRecord{
Time: "2026-06-19 12:00:00", Trigger: "Manual", JobName: "Build",
State: "Success", Detail: "ok",
}
if got, want := EventText(noLog), "2026-06-19 12:00:00 Manual Build Success ok"; got != want {
t.Errorf("EventText without log = %q, want %q", got, want)
}
// An empty trigger is shown as "Unknown".
blank := domain.RunRecord{Time: "t", JobName: "J", State: "S", Detail: "d"}
if got, want := EventText(blank), "t Unknown J S d"; got != want {
t.Errorf("EventText blank trigger = %q, want %q", got, want)
}
}
func TestDisplayFolder(t *testing.T) {
if got := DisplayFolder(" "); got != "(No folder)" {
t.Errorf("blank folder = %q, want %q", got, "(No folder)")
}
if got := DisplayFolder(" Reports "); got != "Reports" {
t.Errorf("folder = %q, want %q", got, "Reports")
}
}
func TestDisplayArguments(t *testing.T) {
if got := DisplayArguments(""); got != "(none)" {
t.Errorf("empty args = %q, want %q", got, "(none)")
}
if got := DisplayArguments(" -v "); got != "-v" {
t.Errorf("args = %q, want %q", got, "-v")
}
}
func TestDisplayRunMode(t *testing.T) {
if got := DisplayRunMode(domain.Job{StartOnly: true}); got != "Start only" {
t.Errorf("start-only = %q, want %q", got, "Start only")
}
if got := DisplayRunMode(domain.Job{StartOnly: false}); got != "Wait for completion" {
t.Errorf("wait = %q, want %q", got, "Wait for completion")
}
}
func TestDisplayInvocation(t *testing.T) {
if got := DisplayInvocation(domain.Job{Command: "echo"}); got != "echo" {
t.Errorf("no args = %q, want %q", got, "echo")
}
// Arguments are appended with spacing and their newlines collapsed to spaces.
job := domain.Job{Command: "echo", Arguments: " hi\nthere "}
if got, want := DisplayInvocation(job), "echo hi there"; got != want {
t.Errorf("with args = %q, want %q", got, want)
}
}
func TestDisplayIndex(t *testing.T) {
indexes := []int{4, 7, 2}
if got := DisplayIndex(indexes, 7); got != 1 {
t.Errorf("DisplayIndex(7) = %d, want 1", got)
}
// A jobIndex not present returns 0.
if got := DisplayIndex(indexes, 99); got != 0 {
t.Errorf("DisplayIndex(missing) = %d, want 0", got)
}
}
func TestDisplayStats(t *testing.T) {
// Zero RunCount → sentinel string.
if got := DisplayStats(nil); got != "No runs recorded" {
t.Errorf("nil runtime = %q, want %q", got, "No runs recorded")
}
if got := DisplayStats(&domain.JobRuntime{}); got != "No runs recorded" {
t.Errorf("zero runtime = %q, want %q", got, "No runs recorded")
}
rt := &domain.JobRuntime{
RunCount: 5,
FailCount: 2,
LastDurationMS: 450,
AvgDurationMS: 380,
MaxDurationMS: 520,
}
want := "5 runs, 2 failed, last 450 ms, avg 380 ms, max 520 ms"
if got := DisplayStats(rt); got != want {
t.Errorf("DisplayStats = %q, want %q", got, want)
}
// Zero failures are included in the output (not hidden).
rtNoFail := &domain.JobRuntime{RunCount: 3, FailCount: 0, LastDurationMS: 100, AvgDurationMS: 90, MaxDurationMS: 110}
wantNoFail := "3 runs, 0 failed, last 100 ms, avg 90 ms, max 110 ms"
if got := DisplayStats(rtNoFail); got != wantNoFail {
t.Errorf("DisplayStats no-fail = %q, want %q", got, wantNoFail)
}
// A "queue" overlap backlog is appended to whichever form applies, so it stays
// visible even before the first run has completed.
if got, want := DisplayStats(&domain.JobRuntime{PendingRuns: 2}), "No runs recorded, 2 queued"; got != want {
t.Errorf("DisplayStats pending, no runs = %q, want %q", got, want)
}
rtPending := &domain.JobRuntime{RunCount: 5, FailCount: 2, LastDurationMS: 450, AvgDurationMS: 380, MaxDurationMS: 520, PendingRuns: 3}
wantPending := "5 runs, 2 failed, last 450 ms, avg 380 ms, max 520 ms, 3 queued"
if got := DisplayStats(rtPending); got != wantPending {
t.Errorf("DisplayStats pending = %q, want %q", got, wantPending)
}
}
func TestEventLine(t *testing.T) {
withLog := domain.RunRecord{
Time: "2026-06-19 12:00:00", Trigger: "Schedule", JobName: "Build",
State: "Success", Detail: "ok", LogFile: "/home/user/logs/build-20260619.log",
}
if got, want := EventLine(withLog), "2026-06-19 12:00:00 Schedule Build Success ok build-20260619.log"; got != want {
t.Errorf("EventLine with log = %q, want %q", got, want)
}
noLog := domain.RunRecord{
Time: "2026-06-19 12:00:00", Trigger: "Manual", JobName: "Build",
State: "Success", Detail: "ok",
}
if got, want := EventLine(noLog), "2026-06-19 12:00:00 Manual Build Success ok"; got != want {
t.Errorf("EventLine without log = %q, want %q", got, want)
}
// An empty trigger is shown as "Unknown".
blank := domain.RunRecord{Time: "t", JobName: "J", State: "S", Detail: "d", LogFile: "/path/to/file.log"}
if got, want := EventLine(blank), "t Unknown J S d file.log"; got != want {
t.Errorf("EventLine blank trigger = %q, want %q", got, want)
}
}
func TestDisplayOverlapPolicy(t *testing.T) {
global := domain.OverlapPolicyQueue
jobOwn := domain.Job{OverlapPolicy: string(domain.OverlapPolicySkip)}
if got, want := DisplayOverlapPolicy(jobOwn, global), "skip"; got != want {
t.Errorf("per-job policy = %q, want %q", got, want)
}
inherit := domain.Job{OverlapPolicy: ""}
if got, want := DisplayOverlapPolicy(inherit, global), "queue (global default)"; got != want {
t.Errorf("inherited policy = %q, want %q", got, want)
}
}
func TestDisplayTimeout(t *testing.T) {
own := domain.Job{TimeoutSeconds: domain.TimeoutSecondsPtr(45)}
if got, want := DisplayTimeout(own, 30), "45 s"; got != want {
t.Errorf("per-job timeout = %q, want %q", got, want)
}
none := domain.Job{TimeoutSeconds: domain.TimeoutSecondsPtr(0)}
if got, want := DisplayTimeout(none, 30), "no timeout"; got != want {
t.Errorf("explicit per-job zero timeout = %q, want %q", got, want)
}
inherit := domain.Job{TimeoutSeconds: nil}
if got, want := DisplayTimeout(inherit, 30), "30 s (global default)"; got != want {
t.Errorf("inherited timeout = %q, want %q", got, want)
}
if got, want := DisplayTimeout(inherit, 0), "no timeout (global default)"; got != want {
t.Errorf("inherited infinite timeout = %q, want %q", got, want)
}
}