Files
x/service/status_test.go
T
ginuerzh 1c07cf6394 fix(service): default nil logger, fix Events() allocation, collect Close errors
- Default nil logger to Nop() in NewService to prevent panics
- Fix Events() returning 20-element slice padded with zero values
- Collect and join all errors in Close() instead of discarding
- Remove unnecessary variable shadow in execCmds
- Add doc comments to all 22 exported symbols
- Add 34 tests covering status, serve loop, close, and edge cases
2026-05-25 23:47:47 +08:00

219 lines
4.5 KiB
Go

package service
import (
"sync"
"testing"
"time"
"github.com/go-gost/core/observer/stats"
)
func TestStatusCreateAndState(t *testing.T) {
before := time.Now()
st := &Status{
createTime: time.Now(),
events: make([]Event, 0, MaxEventSize),
}
after := time.Now()
if st.CreateTime().Before(before) || st.CreateTime().After(after) {
t.Errorf("CreateTime = %v, want between %v and %v", st.CreateTime(), before, after)
}
// Initial state is zero value
if st.State() != State("") {
t.Errorf("State() = %q, want empty", st.State())
}
st.setState(StateReady)
if got := st.State(); got != StateReady {
t.Errorf("State() = %q, want %q", got, StateReady)
}
st.setState(StateFailed)
if got := st.State(); got != StateFailed {
t.Errorf("State() = %q, want %q", got, StateFailed)
}
st.setState(StateClosed)
if got := st.State(); got != StateClosed {
t.Errorf("State() = %q, want %q", got, StateClosed)
}
}
func TestStatusStateConcurrency(t *testing.T) {
st := &Status{
events: make([]Event, 0, MaxEventSize),
}
states := []State{StateRunning, StateReady, StateFailed, StateClosed}
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
st.setState(states[i%len(states)])
_ = st.State()
}(i)
}
wg.Wait()
}
func TestStatusEventsEmpty(t *testing.T) {
st := &Status{
events: make([]Event, 0, MaxEventSize),
}
events := st.Events()
if len(events) != 0 {
t.Errorf("Events() = %d events, want 0", len(events))
}
}
func TestStatusEventsAddAndRetrieve(t *testing.T) {
st := &Status{
events: make([]Event, 0, MaxEventSize),
}
want := []Event{
{Time: time.Now(), Message: "event1"},
{Time: time.Now(), Message: "event2"},
{Time: time.Now(), Message: "event3"},
}
for _, e := range want {
st.addEvent(e)
}
events := st.Events()
if len(events) != len(want) {
t.Fatalf("Events() = %d events, want %d", len(events), len(want))
}
for i, e := range events {
if e.Message != want[i].Message {
t.Errorf("Events()[%d].Message = %q, want %q", i, e.Message, want[i].Message)
}
}
}
func TestStatusEventsReturnsCopy(t *testing.T) {
st := &Status{
events: make([]Event, 0, MaxEventSize),
}
st.addEvent(Event{Message: "original"})
events := st.Events()
events[0] = Event{Message: "modified"}
original := st.Events()
if original[0].Message != "original" {
t.Error("Events() should return a copy, internal state was mutated")
}
}
func TestStatusEventsRingBuffer(t *testing.T) {
st := &Status{
events: make([]Event, 0, MaxEventSize),
}
// Add MaxEventSize + 5 events; first 5 should be evicted
for i := 0; i < MaxEventSize+5; i++ {
st.addEvent(Event{Message: time.Now().Format(time.RFC3339Nano)})
time.Sleep(time.Microsecond) // ensure unique timestamps
}
events := st.Events()
if len(events) != MaxEventSize {
t.Fatalf("len(Events()) = %d, want %d", len(events), MaxEventSize)
}
}
func TestStatusEventsConcurrency(t *testing.T) {
st := &Status{
events: make([]Event, 0, MaxEventSize),
}
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(2)
go func() {
defer wg.Done()
st.addEvent(Event{Message: "writer"})
}()
go func() {
defer wg.Done()
_ = st.Events()
}()
}
wg.Wait()
}
type mockStats struct {
mu sync.Mutex
vals map[stats.Kind]uint64
updated bool
}
func newMockStats() *mockStats {
return &mockStats{vals: make(map[stats.Kind]uint64)}
}
func (m *mockStats) Add(kind stats.Kind, n int64) {
m.mu.Lock()
defer m.mu.Unlock()
m.vals[kind] += uint64(n)
m.updated = true
}
func (m *mockStats) Get(kind stats.Kind) uint64 {
m.mu.Lock()
defer m.mu.Unlock()
return m.vals[kind]
}
func (m *mockStats) IsUpdated() bool {
m.mu.Lock()
defer m.mu.Unlock()
return m.updated
}
func (m *mockStats) Reset() {
m.mu.Lock()
defer m.mu.Unlock()
m.vals = make(map[stats.Kind]uint64)
m.updated = false
}
func TestStatusStats(t *testing.T) {
ms := newMockStats()
st := &Status{
events: make([]Event, 0, MaxEventSize),
stats: ms,
}
if got := st.Stats(); got == nil {
t.Error("Stats() = nil, want non-nil")
}
ms.Add(stats.KindTotalConns, 5)
if got := st.Stats().Get(stats.KindTotalConns); got != 5 {
t.Errorf("Stats().Get(TotalConns) = %d, want 5", got)
}
}
func TestStatusStatsNilReceiver(t *testing.T) {
var st *Status
if got := st.Stats(); got != nil {
t.Errorf("nil Stats() = %v, want nil", got)
}
}
func TestStatusStatsNil(t *testing.T) {
st := &Status{
events: make([]Event, 0, MaxEventSize),
stats: nil,
}
if got := st.Stats(); got != nil {
t.Errorf("Stats() = %v, want nil", got)
}
}