Refactoring complete: v0.4.0 architectural milestone (#1)
## Summary Completed Phase 5 refactoring and reached the target architecture. **Architectural milestone achieved:** - Service layer owns all state and is the sole writer - UI is a thin Fyne view, all widget updates marshaled via `fyne.Do` - Core engines are stateless and injectable - Domain types are pure (no `yaml:"-"` fields) - Full module builds and `go vet ./...` clean ## Changes - Bump version: 0.3.6 → 0.4.0 - Update CHANGELOG with Phase 5 summary - Add ROADMAP "Refactoring Follow-Ups" section ## Known follow-up work 1. **Linux test build broken** — `runner_test.go` needs `//go:build windows` tag 2. **File-size limits exceeded** — `operations.go` (486 lines), `jobs_view.go` (415 lines) See ROADMAP.md for details. --------- Co-authored-by: mixeme <mix.public@ya.ru> Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.mixdep.ru/mix/gosentry/src/domain"
|
||||
)
|
||||
|
||||
func TestEmitDeliversToAllObserversInOrder(t *testing.T) {
|
||||
svc := newTestService(nil)
|
||||
|
||||
var first, second []Event
|
||||
svc.Subscribe(ObserverFunc(func(e Event) { first = append(first, e) }))
|
||||
svc.Subscribe(ObserverFunc(func(e Event) { second = append(second, e) }))
|
||||
|
||||
svc.emit(JobChanged{JobID: 7})
|
||||
svc.emit(RunRecorded{Record: domain.RunRecord{JobID: 7, State: "Success"}})
|
||||
svc.emit(SchedulerStateChanged{Paused: true})
|
||||
|
||||
for name, got := range map[string][]Event{"first": first, "second": second} {
|
||||
if len(got) != 3 {
|
||||
t.Fatalf("%s observer got %d events, want 3", name, len(got))
|
||||
}
|
||||
if jc, ok := got[0].(JobChanged); !ok || jc.JobID != 7 {
|
||||
t.Errorf("%s event[0] = %#v, want JobChanged{JobID:7}", name, got[0])
|
||||
}
|
||||
if rr, ok := got[1].(RunRecorded); !ok || rr.Record.State != "Success" {
|
||||
t.Errorf("%s event[1] = %#v, want RunRecorded Success", name, got[1])
|
||||
}
|
||||
if ss, ok := got[2].(SchedulerStateChanged); !ok || !ss.Paused {
|
||||
t.Errorf("%s event[2] = %#v, want SchedulerStateChanged{Paused:true}", name, got[2])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmitWithNoObserversIsNoop(t *testing.T) {
|
||||
svc := newTestService(nil)
|
||||
// Must not panic with an empty observer list.
|
||||
svc.emit(JobChanged{})
|
||||
}
|
||||
|
||||
// Observers may read Service state from within OnEvent without deadlocking,
|
||||
// because emit is called outside the state lock.
|
||||
func TestObserverCanReadServiceState(t *testing.T) {
|
||||
jobs := []domain.Job{{ID: 1, Name: "Job", Enabled: true}}
|
||||
svc := newTestService(jobs)
|
||||
|
||||
var sawName string
|
||||
svc.Subscribe(ObserverFunc(func(Event) {
|
||||
if snapshot := svc.Jobs(); len(snapshot) == 1 {
|
||||
sawName = snapshot[0].Name
|
||||
}
|
||||
}))
|
||||
|
||||
svc.emit(JobChanged{JobID: 1})
|
||||
if sawName != "Job" {
|
||||
t.Errorf("observer read name = %q, want %q", sawName, "Job")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user