177 lines
5.1 KiB
Go
177 lines
5.1 KiB
Go
// Cthulhu screen reader.
|
|
// Copyright (c) 2026 Storm Dragon
|
|
// Authors: Storm Dragon, Angus Kola
|
|
//
|
|
// 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 lifecycle
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"log/slog"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestStartAndStopReverseOrder(t *testing.T) {
|
|
var events []string
|
|
runner, err := New(
|
|
Service{
|
|
Name: "first",
|
|
Start: func(context.Context) error { events = append(events, "start first"); return nil },
|
|
Stop: func(context.Context) error { events = append(events, "stop first"); return nil },
|
|
},
|
|
Service{
|
|
Name: "second",
|
|
Start: func(context.Context) error { events = append(events, "start second"); return nil },
|
|
Stop: func(context.Context) error { events = append(events, "stop second"); return nil },
|
|
},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("New: %v", err)
|
|
}
|
|
if err := runner.Start(context.Background()); err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
if runner.State() != StateRunning {
|
|
t.Fatalf("state after start = %s, want %s", runner.State(), StateRunning)
|
|
}
|
|
if err := runner.Stop(context.Background()); err != nil {
|
|
t.Fatalf("Stop: %v", err)
|
|
}
|
|
want := strings.Join([]string{"start first", "start second", "stop second", "stop first"}, ",")
|
|
if got := strings.Join(events, ","); got != want {
|
|
t.Fatalf("events = %q, want %q", got, want)
|
|
}
|
|
if runner.State() != StateStopped {
|
|
t.Fatalf("state after stop = %s, want %s", runner.State(), StateStopped)
|
|
}
|
|
}
|
|
|
|
func TestStartupFailureStopsStartedServices(t *testing.T) {
|
|
startFailure := errors.New("start failed")
|
|
stopFailure := errors.New("stop failed")
|
|
var stopped bool
|
|
runner, err := New(
|
|
Service{
|
|
Name: "ready",
|
|
Start: func(context.Context) error { return nil },
|
|
Stop: func(context.Context) error {
|
|
stopped = true
|
|
return stopFailure
|
|
},
|
|
},
|
|
Service{
|
|
Name: "broken",
|
|
Start: func(context.Context) error { return startFailure },
|
|
Stop: func(context.Context) error { return nil },
|
|
},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("New: %v", err)
|
|
}
|
|
|
|
err = runner.Start(context.Background())
|
|
if !errors.Is(err, startFailure) || !errors.Is(err, stopFailure) {
|
|
t.Fatalf("Start error = %v, want both start and stop failures", err)
|
|
}
|
|
if !stopped {
|
|
t.Fatal("started service was not stopped")
|
|
}
|
|
if runner.State() != StateStopped {
|
|
t.Fatalf("state after failed start = %s, want %s", runner.State(), StateStopped)
|
|
}
|
|
}
|
|
|
|
func TestCanceledStartDoesNotCallService(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
var called bool
|
|
runner, err := New(Service{
|
|
Name: "service",
|
|
Start: func(context.Context) error {
|
|
called = true
|
|
return nil
|
|
},
|
|
Stop: func(context.Context) error { return nil },
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("New: %v", err)
|
|
}
|
|
err = runner.Start(ctx)
|
|
if !errors.Is(err, context.Canceled) {
|
|
t.Fatalf("Start error = %v, want context.Canceled", err)
|
|
}
|
|
if called {
|
|
t.Fatal("service start was called after cancellation")
|
|
}
|
|
if runner.State() != StateStopped {
|
|
t.Fatalf("state after canceled start = %s, want %s", runner.State(), StateStopped)
|
|
}
|
|
}
|
|
|
|
func TestStopIsIdempotent(t *testing.T) {
|
|
runner, err := New(Service{
|
|
Name: "service",
|
|
Start: func(context.Context) error { return nil },
|
|
Stop: func(context.Context) error { return nil },
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("New: %v", err)
|
|
}
|
|
if err := runner.Start(context.Background()); err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
if err := runner.Stop(context.Background()); err != nil {
|
|
t.Fatalf("first Stop: %v", err)
|
|
}
|
|
if err := runner.Stop(context.Background()); err != nil {
|
|
t.Fatalf("second Stop: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestStopLogsCompletedServiceStages(t *testing.T) {
|
|
var logs bytes.Buffer
|
|
logger := slog.New(slog.NewTextHandler(&logs, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
runner, err := NewWithLogger(logger, Service{
|
|
Name: "service",
|
|
Start: func(context.Context) error { return nil },
|
|
Stop: func(context.Context) error { return nil },
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewWithLogger: %v", err)
|
|
}
|
|
if err := runner.Start(context.Background()); err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
if err := runner.Stop(context.Background()); err != nil {
|
|
t.Fatalf("Stop: %v", err)
|
|
}
|
|
|
|
output := logs.String()
|
|
if !strings.Contains(output, "lifecycle service stopping") ||
|
|
!strings.Contains(output, "lifecycle service stopped") ||
|
|
!strings.Contains(output, "service") {
|
|
t.Fatalf("stop logs = %q, want stopping and stopped records for service", output)
|
|
}
|
|
}
|
|
|
|
func TestNewRejectsInvalidService(t *testing.T) {
|
|
if _, err := New(Service{Name: "invalid"}); err == nil {
|
|
t.Fatal("New accepted a service without functions")
|
|
}
|
|
}
|