diff --git a/collection.go b/collection.go index 94f27b6..df53f57 100644 --- a/collection.go +++ b/collection.go @@ -1,78 +1,94 @@ package nested import ( - "math/rand" - "strconv" + "errors" + "fmt" + "sort" + "strings" "sync" ) // A Collection monitors multiple services and keeps track of the overall state. The overall state is defined as: -// - Ready if all of the services are ready. -// - Stopped if ANY of the services are stopped. -// - Not Ready otherwise. +// - Ready if ALL of the services are ready. +// - Stopped if ALL of the services are stopped. +// - Error if ANY of the services are erroring. +// - Error if some (but not all) of the services are stopped. +// - Initializing of ANY of the services are initializing (and none are erroring). // -// A Collection implements the Service interface but does not set the error states. +// A Collection implements the Service interface. // // Services to be monitored are added using the Add() method. Services cannot be removed once added. // -// An empty Collection is ready to use and in the Not Ready state. A Collection must not be copied after first use. +// To start monitoring, the caller must invoke the Run() method. Only when Run has been called AND all of the services +// have finished initialization will the collection change its state. Services should not be added after calling Run(). +// +// An empty Collection is ready to use and in the Initializing state. A Collection must not be copied after first use. type Collection struct { Monitor sync.Mutex services map[string]Service - id string - updates chan Notification + running bool } -// Verifies that a Monitor implements the Service interface. +// Verifies that a Collection implements the Service interface. var _ Service = &Collection{} -// Add adds a service to be monitored. Panics if the service has already been added. Panics if the label has been -// used already for another service. +// A CollectionError is returned by the collections Err() method when any of the services are erroring. It can be +// inspected for details of the errors from each service. +type CollectionError struct { + // Errors contains the error descriptions from each erroring service, indexed by label. Only erroring services are included. + Errors map[string]error +} + +// An ErrStoppedServices error is returned by the collections Err() method when no services are erroring and some (but +// not all) of the monitored services are stopped. It normally indicates that we're in the process of shutting down. +var ErrStoppedServices = errors.New("there are stopped services") + +// Error returns the error descriptions from all erroring services in a multi-line string. +func (ce CollectionError) Error() string { + msgs := make([]string, 0, len(ce.Errors)) + for id, err := range ce.Errors { + msgs = append(msgs, id+": "+err.Error()) + } + sort.Strings(msgs) + return strings.Join(msgs, "\n") +} + +// Add adds a service to be monitored. Panics if the label has already been used in this collection. func (c *Collection) Add(label string, s Service) { c.Lock() defer c.Unlock() - // Initialize the update channel if this is the first service to be added. + // Initialize the maps if this is the first service to be added. if c.services == nil { c.services = make(map[string]Service) - c.updates = make(chan Notification) - go func() { - for range c.updates { - c.Monitor.SetState(c.getOverallState(), nil) - } - }() - // Using the same ID to subscribe to all monitored services means that Subscribe will panic below if a service - // is added twice. - c.id = "collection-" + strconv.Itoa(rand.Int()) } else { // Otherwise check that we're not reusing a label. if _, ok := c.services[label]; ok { - panic("add: label " + label + " already in use") + panic(fmt.Sprintf("add: label %q already in use", label)) } } c.services[label] = s - s.Subscribe(c.id, c.updates) - // Trigger an update to include the state of the newly added service. - go func() { - c.updates <- Notification{} - }() + // Just in case someone adds a service to a running collection, make sure we get its events. The alternative would + // be to disallow adding the service in the first place, but we don't want to do that. + if c.running { + s.Register(c) + } } -// StateCount returns the number of monitored services currently in the given state. -func (c *Collection) StateCount(state State) int { +// Run starts monitoring the added services. The collection remains in the Initializing state until all of the +// monitored services are finished initializing. +// +// Calling Run on an already running collection has no effect. +func (c *Collection) Run() { + defer c.OnNotify(Event{}) c.Lock() defer c.Unlock() - - var n int - for _, service := range c.services { - if service.GetState() == state { - n++ - } + for _, s := range c.services { + s.Register(c) } - return n } // Up returns a map whose keys are the labels of all the currently monitored services and whose values are true if @@ -91,58 +107,68 @@ func (c *Collection) Up() map[string]bool { // any of the services should be used after calling stop. func (c *Collection) Stop() { - // Start stopping all of the member services, and then release the lock. - u := func() chan Notification { - - // Initialize the wait group first so that wg.Wait() runs after the lock is released. That way, if we block - // on any of the Stop() calls, we do so without holding the lock. - wg := sync.WaitGroup{} - defer wg.Wait() - - c.Lock() - defer c.Unlock() - - wg.Add(len(c.services)) - for _, service := range c.services { - // Unsubscribe first so that we can close the notifications channel. Note that a side effect of - // unsubscribing here is that we need to explicitly set the monitor to stopped when we're done. - service.Unsubscribe(c.id) - go func(s Service) { - s.Stop() - wg.Done() - }(service) - } - c.services = nil + // Initialize the wait group first so that wg.Wait() runs after the lock is released. That way, if we block + // on any of the Stop() calls, we do so without holding the lock. + wg := sync.WaitGroup{} + defer wg.Wait() - // Return the update channel so that we don't have to grab the lock again to get it. - return c.updates - }() + c.Lock() + defer c.Unlock() - // Close the update channel to release the goroutine in Add() above. If u is nil, that means that this collection - // hasn't been used, which is unexpected but not our concern. - if u != nil { - close(u) + wg.Add(len(c.services)) + for _, service := range c.services { + go func(s Service) { + s.Stop() + wg.Done() + }(service) } - - // Need to explicitly set the monitor to stopped, since we unsubscribed already above. - c.Monitor.Stop() } -// getOverallState computes the overall state of the collection: ready if all of the services are ready, stopped -// if any of the services are stopped, and not ready otherwise. getOverallState should not be called on an empty -// collection, as it will give the incorrect state. -func (c *Collection) getOverallState() State { +// OnNotify updates the state of the collection according to the states of all of the monitored services. No update is +// done if any of the services are still initializing. +// +// OnNotify is used internally as a callback when any monitored service changes state. It is not normally called directly. +func (c *Collection) OnNotify(_ Event) { + c.Lock() defer c.Unlock() - we := State(Ready) - for _, service := range c.services { - switch service.GetState() { + allStopped := true + anyStopped := false + errs := make(map[string]error) + + if len(c.services) == 0 { + return + } + + for id, s := range c.services { + switch s.GetState() { + case Initializing: + return + case Ready: + allStopped = false + case Error: + errs[id] = s.Err() + allStopped = false // not actually needed, since we check for errors first case Stopped: - return Stopped - case NotReady: - we = NotReady + anyStopped = true } } - return we + + if len(errs) > 0 { + c.Monitor.SetError(CollectionError{Errors: errs}) + return + } + + if allStopped { + c.Monitor.Stop() + return + } + + if anyStopped { + c.Monitor.SetError(ErrStoppedServices) + return + } + + c.Monitor.SetReady() } diff --git a/collection_test.go b/collection_test.go index a98a872..13cc67a 100644 --- a/collection_test.go +++ b/collection_test.go @@ -10,97 +10,54 @@ func TestCollection(t *testing.T) { co := Collection{} - // A new collection is not ready. - s, e := co.GetFullState() - assertEqual(t, NotReady, s) - assertEqual(t, nil, e) - assertEqual(t, s, co.GetState()) + // A new collection is initializing + assertEqual(t, Initializing, co.GetState()) + assertEqual(t, nil, co.Err()) assertEqual(t, map[string]bool{}, co.Up()) // Add services. s0, s1 := &Monitor{}, &Monitor{} co.Add("service 0", s0) co.Add("service 1", s1) + co.Run() time.Sleep(10 * time.Millisecond) - s, e = co.GetFullState() - assertEqual(t, NotReady, s) - assertEqual(t, nil, e) - assertEqual(t, s, co.GetState()) + assertEqual(t, Initializing, co.GetState()) + assertEqual(t, nil, co.Err()) assertEqual(t, map[string]bool{"service 0": false, "service 1": false}, co.Up()) + // Can't add another service 0. + assertPanic(t, func() { co.Add("service 0", s0) }, `add: label "service 0" already in use`) + // One service is ready. - s0.SetState(Ready, nil) + s0.SetReady() time.Sleep(10 * time.Millisecond) - s, e = co.GetFullState() - assertEqual(t, NotReady, s) - assertEqual(t, nil, e) - assertEqual(t, s, co.GetState()) + assertEqual(t, Initializing, co.GetState()) + assertEqual(t, nil, co.Err()) assertEqual(t, map[string]bool{"service 0": true, "service 1": false}, co.Up()) // Both services are ready. - s1.SetState(Ready, nil) + s1.SetReady() time.Sleep(10 * time.Millisecond) - s, e = co.GetFullState() - assertEqual(t, Ready, s) - assertEqual(t, nil, e) - assertEqual(t, s, co.GetState()) + assertEqual(t, Ready, co.GetState()) + assertEqual(t, nil, co.Err()) assertEqual(t, map[string]bool{"service 0": true, "service 1": true}, co.Up()) - assertEqual(t, 2, co.StateCount(Ready)) - assertEqual(t, 0, co.StateCount(NotReady)) - assertEqual(t, 0, co.StateCount(Stopped)) - // One service is stopped. s0.Stop() time.Sleep(10 * time.Millisecond) - s, e = co.GetFullState() - assertEqual(t, Stopped, s) - assertEqual(t, nil, e) - assertEqual(t, s, co.GetState()) + assertEqual(t, Error, co.GetState()) + assertEqual(t, ErrStoppedServices, co.Err()) assertEqual(t, map[string]bool{"service 0": false, "service 1": true}, co.Up()) - assertEqual(t, 1, co.StateCount(Ready)) - assertEqual(t, 0, co.StateCount(NotReady)) - assertEqual(t, 1, co.StateCount(Stopped)) - // One service is stopped, and the other is not ready. - s1.SetState(NotReady, nil) + nr := errors.New("not ready") + s1.SetError(nr) time.Sleep(10 * time.Millisecond) - s, e = co.GetFullState() - assertEqual(t, Stopped, s) - assertEqual(t, nil, e) - assertEqual(t, s, co.GetState()) + assertEqual(t, Error, co.GetState()) + assertEqual(t, error(CollectionError{Errors: map[string]error{"service 1": nr}}), co.Err()) assertEqual(t, map[string]bool{"service 0": false, "service 1": false}, co.Up()) // Stop all services. co.Stop() - assertEqual(t, 0, co.StateCount(Ready)) - assertEqual(t, 0, co.StateCount(NotReady)) - assertEqual(t, map[string]bool{}, co.Up()) - - // We also have no stopped services because the service list has been emptied. - assertEqual(t, 0, co.StateCount(Stopped)) -} - -func TestCollection2(t *testing.T) { - - co := Collection{} - - // A new collection is not ready. - s, e := co.GetFullState() - assertEqual(t, NotReady, s) - assertEqual(t, nil, e) - - // Add two services; one is ready, one isn't. - s0, s1 := &Monitor{}, &Monitor{} - s0.SetState(Ready, nil) - co.Add("service 0", s0) - s1.SetState(NotReady, errors.New("oh, no!")) - co.Add("service 1", s1) - time.Sleep(10 * time.Millisecond) - s, e = co.GetFullState() - assertEqual(t, NotReady, s) - assertEqual(t, nil, e) - assertEqual(t, s, co.GetState()) - assertEqual(t, map[string]bool{"service 0": true, "service 1": false}, co.Up()) + assertEqual(t, map[string]bool{"service 0": false, "service 1": false}, co.Up()) } diff --git a/doc.go b/doc.go index 445f0ad..6df2863 100644 --- a/doc.go +++ b/doc.go @@ -7,17 +7,26 @@ // should implement. // // The state machine has the following states: +// - Initializing. The service is not ready yet. // - Ready. The service is running normally. // - Not ready. The service is temporarily unavailable. // - Stopped. The service is permanently unavailable. // -// Additionally, an error state is exposed. -// - When ready, the error state should always be nil. -// - When not ready, the error state may indicate a reason for being not ready. Not ready with a nil error state -// implies that the service is initializing. -// - When stopped, the error state may indicate a reason for being stopped. Stopped with a nil error state implies -// that the service was stopped by the calling process with Stop(). +// The state machine begins in the initializing state. Once it transitions to one of the other states, it can never +// return to the initializing state. +// +// A state machine in the stopped state cannot change states. // // This package also provides a Monitor type, which implements the state machine. A Monitor can be embedded in // any service to make it a nested service. +// +// A common pattern is to include a Monitor in the struct that defines the nested service, e.g. +// +// type MyService struct { +// nested.Monitor +// ... +// } +// +// The MyService constructor may either return an initializing service or a fully initialized service. The MyService +// Stop() method, however, should always wait until the service has stopped completely before returning. package nested diff --git a/monitor.go b/monitor.go index 5d1343a..2d0cae9 100644 --- a/monitor.go +++ b/monitor.go @@ -1,7 +1,6 @@ package nested import ( - "fmt" "sync" ) @@ -10,9 +9,9 @@ import ( // An empty Monitor is ready to use and in the Not Ready state. A Monitor must not be copied after first use. type Monitor struct { sync.Mutex - state State // current state - err error // current error state - subscriptions map[string]chan<- Notification + state State // current state + err error // current error state, if the state is not ready + observers map[Observer]struct{} } // Verifies that a Monitor implements the Service interface. Note that the Service interface does NOT include the @@ -27,86 +26,84 @@ func (m *Monitor) GetState() State { return m.state } -// GetFullState returns the current state and error state of the service. -func (m *Monitor) GetFullState() (State, error) { +// Err returns the error from the most recent Err state, or nil if the Monitor has never been in the error state. +func (m *Monitor) Err() error { m.Lock() defer m.Unlock() - return m.state, m.err + return m.err } -// Stop sets the service to stopped and cancels all subsriptions. Does nothing if the service is already stopped -// or failed. +// Stop sets the service to stopped. If there are registered observers, all observers are called before returning. func (m *Monitor) Stop() { - m.setState(Stopped, nil, true) + m.setState(Stopped, nil) } -// Subscribe creates a subscription to state changes, and will send all subsequent state changes to the channel provided. -func (m *Monitor) Subscribe(id string, channel chan<- Notification) { +// Register registers an observer, whose OnNotify method will be called any time there is a state change. Does nothing +// if the observer is already registered. +func (m *Monitor) Register(o Observer) { m.Lock() defer m.Unlock() - if m.subscriptions == nil { - m.subscriptions = make(map[string]chan<- Notification) + if m.observers == nil { + m.observers = make(map[Observer]struct{}) } - m.subscriptions[id] = channel + m.observers[o] = struct{}{} } -// Unsubscribe removes the subscription with the id provided. Does nothing if the subscription doesn't exist. -func (m *Monitor) Unsubscribe(id string) { +// Deregister removes a registered observer. Does nothing if the observer is not registered. +func (m *Monitor) Deregister(o Observer) { m.Lock() defer m.Unlock() - delete(m.subscriptions, id) + delete(m.observers, o) } -// SetState sets the state and error state. SetState should only be set by the package implementing the service. -// If there are subscriptions, SetState returns after the notifications are consumed. -// -// SetState can be used to change either the state or the error state, or both. If a call to SetState results -// in no change, then the result is a no-op. -// -// SetState panics on an attempt to change the state or error state of a stopped service. (It won't panic if -// there's no change.) -func (m *Monitor) SetState(newState State, newErr error) { - m.setState(newState, newErr, false) +// SetReady sets the monitor state to Ready. If there are registered observers, all observers are called before returning. +// Panics if the monitor is already stopped. +func (m *Monitor) SetReady() { + m.setState(Ready, nil) } -func (m *Monitor) setState(newState State, newErr error, ignoreStopped bool) { +// SetReady sets the monitor state to Error. If there are registered observers, all observers are called before returning. +// Panics if the monitor is already stopped. +func (m *Monitor) SetError(err error) { + m.setState(Error, err) +} - if _, ok := names[newState]; !ok { - panic(fmt.Sprintf("state %d is undefined", newState)) - } +func (m *Monitor) setState(newState State, newErr error) { // Initialize the wait group first so that wg.Wait() runs after the lock is released. That way, if we block - // on any of the subscription channels, we do so without holding the lock. + // on any of the observer callbacks, we do so without holding the lock. var wg sync.WaitGroup defer wg.Wait() m.Lock() defer m.Unlock() - if newState == m.state && newErr == m.err { + if newState == m.state && !(newState == Error && newErr != m.err) { return // nothing to do } if m.state == Stopped { - if ignoreStopped { - return - } panic("cannot transition from stopped state") } - m.state, m.err = newState, newErr + ev := Event{ + OldState: m.state, + NewState: newState, + Error: newErr, + } - // Notify all subscribers. - wg.Add(len(m.subscriptions)) - for id, ch := range m.subscriptions { - // Run these in the background so as not to block while holding the lock. - go func(id string, ch chan<- Notification) { - ch <- Notification{ID: id, State: newState, Error: newErr} - wg.Done() - }(id, ch) + m.state = newState + if newState == Error { + m.err = newErr } - if newState == Stopped { - m.subscriptions = nil + // Notify all observers. + wg.Add(len(m.observers)) + for o := range m.observers { + // Run these in the background so as not to block while holding the lock. + go func(o Observer) { + o.OnNotify(ev) + wg.Done() + }(o) } } diff --git a/monitor_test.go b/monitor_test.go index 1dcd8a0..5f95811 100644 --- a/monitor_test.go +++ b/monitor_test.go @@ -35,97 +35,75 @@ func assertReceived[X any](t *testing.T, ch <-chan X) (x X) { func TestMonitor(t *testing.T) { - // A new Monitor is not ready. + // A new Monitor's state is Initializing. mon := Monitor{} - s, e := mon.GetFullState() - assertEqual(t, NotReady, s) - assertEqual(t, nil, e) + assertEqual(t, Initializing, mon.GetState()) + assertEqual(t, nil, mon.Err()) - // Set to ready. - mon.SetState(Ready, nil) - s, e = mon.GetFullState() - assertEqual(t, Ready, s) - assertEqual(t, nil, e) + // Set to Ready. + mon.SetReady() + assertEqual(t, Ready, mon.GetState()) + assertEqual(t, nil, mon.Err()) - // Set to not ready with a reason. + // Set to Error. reason := errors.New("some reason") - mon.SetState(NotReady, reason) - s, e = mon.GetFullState() - assertEqual(t, NotReady, s) - assertEqual(t, reason, e) - - // Can't set to an undefined state. - assertPanic(t, func() { mon.SetState(-1, nil) }, "undefined") + mon.SetError(reason) + assertEqual(t, Error, mon.GetState()) + assertEqual(t, reason, mon.Err()) - // Set ready again. - mon.SetState(Ready, nil) - s, e = mon.GetFullState() - assertEqual(t, Ready, s) - assertEqual(t, nil, e) + // Set Ready again. Previous error can still be retrieved. + mon.SetReady() + assertEqual(t, Ready, mon.GetState()) + assertEqual(t, reason, mon.Err()) // Stop. mon.Stop() - s, e = mon.GetFullState() - assertEqual(t, Stopped, s) - assertEqual(t, nil, e) + assertEqual(t, Stopped, mon.GetState()) // Can't restart. - assertPanic(t, func() { mon.SetState(Ready, nil) }, "cannot transition from stopped state") -} - -func TestMonitor2(t *testing.T) { - - mon := Monitor{} - - // Failure on initialization. - failure := errors.New("some failure") - mon.SetState(Stopped, failure) - s, e := mon.GetFullState() - assertEqual(t, Stopped, s) - assertEqual(t, failure, e) - - // Now Stop() should be a no-op - mon.Stop() - s, e = mon.GetFullState() - assertEqual(t, Stopped, s) - assertEqual(t, failure, e) // note that the error condition is still there + assertPanic(t, func() { mon.SetReady() }, "cannot transition from stopped state") + assertPanic(t, func() { mon.SetError(reason) }, "cannot transition from stopped state") } func TestMonitorNotifications(t *testing.T) { mon := Monitor{} - ch := make(chan Notification, 1) - mon.Subscribe("foo", ch) + ch := make(testObserver, 1) + mon.Register(ch) - // Set to ready. - mon.SetState(Ready, nil) + // Set to Ready. + mon.SetReady() n := assertReceived(t, ch) - assertEqual(t, Ready, n.State) + assertEqual(t, Initializing, n.OldState) + assertEqual(t, Ready, n.NewState) assertEqual(t, nil, n.Error) - // Set to ready again, and there's not an additional notification. - mon.SetState(Ready, nil) + // Set to Ready again, and there's not an additional notification. + mon.SetReady() if len(ch) > 0 { t.Error("unexpected notification") } - // Set to not ready with a reason. + // Set to Error. reason := errors.New("some reason") - mon.SetState(NotReady, reason) + mon.SetError(reason) n = assertReceived(t, ch) - assertEqual(t, NotReady, n.State) + assertEqual(t, Ready, n.OldState) + assertEqual(t, Error, n.NewState) assertEqual(t, reason, n.Error) // Set ready again. - mon.SetState(Ready, nil) + mon.SetReady() n = assertReceived(t, ch) - assertEqual(t, Ready, n.State) + assertEqual(t, Error, n.OldState) + assertEqual(t, Ready, n.NewState) assertEqual(t, nil, n.Error) // Stop. mon.Stop() n = assertReceived(t, ch) - assertEqual(t, Stopped, n.State) + assertEqual(t, Ready, n.OldState) + assertEqual(t, Stopped, n.NewState) assertEqual(t, nil, n.Error) // Stop again, and there's not an additional notification. @@ -137,29 +115,34 @@ func TestMonitorNotifications(t *testing.T) { close(ch) } -func TestUnsubscribe(t *testing.T) { +func TestDeregister(t *testing.T) { mon := Monitor{} + ch := make(testObserver, 1) - // Unsubscribing something that doesn't exist is not an error. - mon.Unsubscribe("bar") + // Deregistering something that doesn't exist is not an error. + mon.Deregister(ch) - ch := make(chan Notification, 1) - mon.Subscribe("foo", ch) + mon.Register(ch) // Set to ready. - mon.SetState(Ready, nil) + mon.SetReady() n := assertReceived(t, ch) - assertEqual(t, n.State, Ready) + assertEqual(t, n.OldState, Initializing) + assertEqual(t, n.NewState, Ready) assertEqual(t, n.Error, nil) - mon.Unsubscribe("foo") + mon.Deregister(ch) // No more notifications. - mon.SetState(NotReady, nil) + mon.Stop() if len(ch) > 0 { t.Error("unexpected notification") } close(ch) } + +type testObserver chan Event + +func (ch testObserver) OnNotify(ev Event) { ch <- ev } diff --git a/nested.go b/nested.go index 7f4df9d..4e95253 100644 --- a/nested.go +++ b/nested.go @@ -3,43 +3,46 @@ package nested type State int8 const ( - NotReady State = iota + Initializing State = iota Ready + Error Stopped ) var names = map[State]string{ - NotReady: "not ready", - Ready: "ready", - Stopped: "stopped", + Initializing: "initializing", + Ready: "ready", + Error: "error", + Stopped: "stopped", } func (s State) String() string { return names[s] } +// An event is a single notification of a state change. +type Event struct { + OldState State + NewState State + Error error // error condition if the new state is Error, nil otherwise +} + +// An observer receives notifications of state changes. +type Observer interface { + OnNotify(Event) +} + +// The Service interface defines the behavior of a nested service. type Service interface { // GetState returns the current state of the service. GetState() State - // GetFullState returns the current state and error state of the service. - GetFullState() (State, error) - // Stop stops the service, and releases all resources. After sending the final update to the stopped state, - // all subscriptions are unsubscribed. Future calls to GetState() will always return Stopped. + // Err returns the most recent error condition. Returns nil if the service has never been in the Err state. + Err() error + // Stop stops the service and releases all resources. Stop should not return until the service shutdown is complete. Stop() - // Subscribe starts sending all state changes to the channel provided. The ID must unique. Subscribe panics - // if the ID is already subscribed. - Subscribe(id string, channel chan<- Notification) - // Unsubscribe stops sending notifications. The caller must provide the same ID as was provided in the call - // to Subscribe(). Repeated calls to Unsubscribed() with the same ID are ignored. Calls to Unscrubscribe() - // with an unknown ID are also ignored. - Unsubscribe(id string) -} - -type Notification struct { - // The ID as provided by the call to Subscribe() - ID string - // The new state - State State - // The new error state - Error error + // Register registers an observer, whose OnNotify method will be called any time there is a state change. Does + // nothing if the observer is already registered. + Register(Observer) + // Deregister removes a registered observer. Does nothing if the observer is not registered. + Deregister(Observer) } diff --git a/nested_test.go b/nested_test.go index c28dc5b..b1161b2 100644 --- a/nested_test.go +++ b/nested_test.go @@ -24,7 +24,8 @@ func assertEqual[X any](t *testing.T, want, got X) { } func TestName(t *testing.T) { + assertEqual(t, "initializing", Initializing.String()) assertEqual(t, "ready", Ready.String()) - assertEqual(t, "not ready", NotReady.String()) + assertEqual(t, "error", Error.String()) assertEqual(t, "stopped", Stopped.String()) }