cmd/syncthing: Extract interfaces for things the API depends on

Enables testing of the API service, in the long run.
This commit is contained in:
Jakob Borg 2016-03-21 19:36:08 +00:00
parent 894ccd18ff
commit a492cfba13
10 changed files with 112 additions and 51 deletions

View File

@ -32,10 +32,10 @@ import (
"github.com/syncthing/syncthing/lib/discover" "github.com/syncthing/syncthing/lib/discover"
"github.com/syncthing/syncthing/lib/events" "github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/logger" "github.com/syncthing/syncthing/lib/logger"
"github.com/syncthing/syncthing/lib/model"
"github.com/syncthing/syncthing/lib/osutil" "github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/protocol" "github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/relay" "github.com/syncthing/syncthing/lib/relay"
"github.com/syncthing/syncthing/lib/stats"
"github.com/syncthing/syncthing/lib/sync" "github.com/syncthing/syncthing/lib/sync"
"github.com/syncthing/syncthing/lib/tlsutil" "github.com/syncthing/syncthing/lib/tlsutil"
"github.com/syncthing/syncthing/lib/upgrade" "github.com/syncthing/syncthing/lib/upgrade"
@ -50,15 +50,15 @@ var (
type apiService struct { type apiService struct {
id protocol.DeviceID id protocol.DeviceID
cfg *config.Wrapper cfg configIntf
httpsCertFile string httpsCertFile string
httpsKeyFile string httpsKeyFile string
assetDir string assetDir string
themes []string themes []string
model *model.Model model modelIntf
eventSub *events.BufferedSubscription eventSub events.BufferedSubscription
discoverer *discover.CachingMux discoverer discover.CachingMux
relayService *relay.Service relayService relay.Service
fss *folderSummaryService fss *folderSummaryService
systemConfigMut sync.Mutex // serializes posts to /rest/system/config systemConfigMut sync.Mutex // serializes posts to /rest/system/config
stop chan struct{} // signals intentional stop stop chan struct{} // signals intentional stop
@ -68,11 +68,52 @@ type apiService struct {
listener net.Listener listener net.Listener
listenerMut sync.Mutex listenerMut sync.Mutex
guiErrors *logger.Recorder guiErrors logger.Recorder
systemLog *logger.Recorder systemLog logger.Recorder
} }
func newAPIService(id protocol.DeviceID, cfg *config.Wrapper, httpsCertFile, httpsKeyFile, assetDir string, m *model.Model, eventSub *events.BufferedSubscription, discoverer *discover.CachingMux, relayService *relay.Service, errors, systemLog *logger.Recorder) (*apiService, error) { type modelIntf interface {
GlobalDirectoryTree(folder, prefix string, levels int, dirsonly bool) map[string]interface{}
Completion(device protocol.DeviceID, folder string) float64
Override(folder string)
NeedFolderFiles(folder string, page, perpage int) ([]db.FileInfoTruncated, []db.FileInfoTruncated, []db.FileInfoTruncated, int)
NeedSize(folder string) (nfiles int, bytes int64)
ConnectionStats() map[string]interface{}
DeviceStatistics() map[string]stats.DeviceStatistics
FolderStatistics() map[string]stats.FolderStatistics
CurrentFolderFile(folder string, file string) (protocol.FileInfo, bool)
CurrentGlobalFile(folder string, file string) (protocol.FileInfo, bool)
ResetFolder(folder string)
Availability(folder, file string) []protocol.DeviceID
GetIgnores(folder string) ([]string, []string, error)
SetIgnores(folder string, content []string) error
PauseDevice(device protocol.DeviceID)
ResumeDevice(device protocol.DeviceID)
DelayScan(folder string, next time.Duration)
ScanFolder(folder string) error
ScanFolders() map[string]error
ScanFolderSubs(folder string, subs []string) error
BringToFront(folder, file string)
ConnectedTo(deviceID protocol.DeviceID) bool
GlobalSize(folder string) (nfiles, deleted int, bytes int64)
LocalSize(folder string) (nfiles, deleted int, bytes int64)
CurrentLocalVersion(folder string) (int64, bool)
RemoteLocalVersion(folder string) (int64, bool)
State(folder string) (string, time.Time, error)
}
type configIntf interface {
GUI() config.GUIConfiguration
Raw() config.Configuration
Options() config.OptionsConfiguration
Replace(cfg config.Configuration) config.CommitResponse
Subscribe(c config.Committer)
Folders() map[string]config.FolderConfiguration
Devices() map[protocol.DeviceID]config.DeviceConfiguration
Save() error
}
func newAPIService(id protocol.DeviceID, cfg configIntf, httpsCertFile, httpsKeyFile, assetDir string, m modelIntf, eventSub events.BufferedSubscription, discoverer discover.CachingMux, relayService relay.Service, errors, systemLog logger.Recorder) (*apiService, error) {
service := &apiService{ service := &apiService{
id: id, id: id,
cfg: cfg, cfg: cfg,
@ -554,7 +595,7 @@ func (s *apiService) getDBStatus(w http.ResponseWriter, r *http.Request) {
sendJSON(w, folderSummary(s.cfg, s.model, folder)) sendJSON(w, folderSummary(s.cfg, s.model, folder))
} }
func folderSummary(cfg *config.Wrapper, m *model.Model, folder string) map[string]interface{} { func folderSummary(cfg configIntf, m modelIntf, folder string) map[string]interface{} {
var res = make(map[string]interface{}) var res = make(map[string]interface{})
res["invalid"] = cfg.Folders()[folder].Invalid res["invalid"] = cfg.Folders()[folder].Invalid

View File

@ -739,7 +739,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
// Start relay management // Start relay management
var relayService *relay.Service var relayService relay.Service
if opts.RelaysEnabled { if opts.RelaysEnabled {
relayService = relay.NewService(cfg, tlsCfg) relayService = relay.NewService(cfg, tlsCfg)
mainService.Add(relayService) mainService.Add(relayService)
@ -972,7 +972,7 @@ func startAuditing(mainService *suture.Supervisor) {
l.Infoln("Audit log in", auditFile) l.Infoln("Audit log in", auditFile)
} }
func setupGUI(mainService *suture.Supervisor, cfg *config.Wrapper, m *model.Model, apiSub *events.BufferedSubscription, discoverer *discover.CachingMux, relayService *relay.Service, errors, systemLog *logger.Recorder, runtimeOptions RuntimeOptions) { func setupGUI(mainService *suture.Supervisor, cfg *config.Wrapper, m *model.Model, apiSub events.BufferedSubscription, discoverer discover.CachingMux, relayService relay.Service, errors, systemLog logger.Recorder, runtimeOptions RuntimeOptions) {
guiCfg := cfg.GUI() guiCfg := cfg.GUI()
if !guiCfg.Enabled { if !guiCfg.Enabled {

View File

@ -9,9 +9,7 @@ package main
import ( import (
"time" "time"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/events" "github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/model"
"github.com/syncthing/syncthing/lib/sync" "github.com/syncthing/syncthing/lib/sync"
"github.com/thejerf/suture" "github.com/thejerf/suture"
) )
@ -21,8 +19,8 @@ import (
type folderSummaryService struct { type folderSummaryService struct {
*suture.Supervisor *suture.Supervisor
cfg *config.Wrapper cfg configIntf
model *model.Model model modelIntf
stop chan struct{} stop chan struct{}
immediate chan string immediate chan string
@ -35,7 +33,7 @@ type folderSummaryService struct {
lastEventReqMut sync.Mutex lastEventReqMut sync.Mutex
} }
func newFolderSummaryService(cfg *config.Wrapper, m *model.Model) *folderSummaryService { func newFolderSummaryService(cfg configIntf, m modelIntf) *folderSummaryService {
service := &folderSummaryService{ service := &folderSummaryService{
Supervisor: suture.NewSimple("folderSummaryService"), Supervisor: suture.NewSimple("folderSummaryService"),
cfg: cfg, cfg: cfg,

View File

@ -79,7 +79,7 @@ func (m *usageReportingManager) String() string {
// reportData returns the data to be sent in a usage report. It's used in // reportData returns the data to be sent in a usage report. It's used in
// various places, so not part of the usageReportingManager object. // various places, so not part of the usageReportingManager object.
func reportData(cfg *config.Wrapper, m *model.Model) map[string]interface{} { func reportData(cfg configIntf, m modelIntf) map[string]interface{} {
res := make(map[string]interface{}) res := make(map[string]interface{})
res["urVersion"] = usageReportVersion res["urVersion"] = usageReportVersion
res["uniqueID"] = cfg.Options().URUniqueID res["uniqueID"] = cfg.Options().URUniqueID

View File

@ -52,7 +52,7 @@ type connectionService struct {
tlsCfg *tls.Config tlsCfg *tls.Config
discoverer discover.Finder discoverer discover.Finder
conns chan model.IntermediateConnection conns chan model.IntermediateConnection
relayService *relay.Service relayService relay.Service
bepProtocolName string bepProtocolName string
tlsDefaultCommonName string tlsDefaultCommonName string
lans []*net.IPNet lans []*net.IPNet
@ -66,7 +66,7 @@ type connectionService struct {
relaysEnabled bool relaysEnabled bool
} }
func NewConnectionService(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *tls.Config, discoverer discover.Finder, relayService *relay.Service, func NewConnectionService(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *tls.Config, discoverer discover.Finder, relayService relay.Service,
bepProtocolName string, tlsDefaultCommonName string, lans []*net.IPNet) suture.Service { bepProtocolName string, tlsDefaultCommonName string, lans []*net.IPNet) suture.Service {
service := &connectionService{ service := &connectionService{
Supervisor: suture.NewSimple("connectionService"), Supervisor: suture.NewSimple("connectionService"),

View File

@ -22,7 +22,13 @@ import (
// time sets how long we refrain from asking about the same device ID after // time sets how long we refrain from asking about the same device ID after
// receiving a negative answer. The value of zero disables caching (positive // receiving a negative answer. The value of zero disables caching (positive
// or negative). // or negative).
type CachingMux struct { type CachingMux interface {
FinderService
Add(finder Finder, cacheTime, negCacheTime time.Duration, priority int)
ChildErrors() map[string]error
}
type cachingMux struct {
*suture.Supervisor *suture.Supervisor
finders []cachedFinder finders []cachedFinder
caches []*cache caches []*cache
@ -51,15 +57,15 @@ type cachedError interface {
CacheFor() time.Duration CacheFor() time.Duration
} }
func NewCachingMux() *CachingMux { func NewCachingMux() CachingMux {
return &CachingMux{ return &cachingMux{
Supervisor: suture.NewSimple("discover.cachingMux"), Supervisor: suture.NewSimple("discover.cachingMux"),
mut: sync.NewRWMutex(), mut: sync.NewRWMutex(),
} }
} }
// Add registers a new Finder, with associated cache timeouts. // Add registers a new Finder, with associated cache timeouts.
func (m *CachingMux) Add(finder Finder, cacheTime, negCacheTime time.Duration, priority int) { func (m *cachingMux) Add(finder Finder, cacheTime, negCacheTime time.Duration, priority int) {
m.mut.Lock() m.mut.Lock()
m.finders = append(m.finders, cachedFinder{finder, cacheTime, negCacheTime, priority}) m.finders = append(m.finders, cachedFinder{finder, cacheTime, negCacheTime, priority})
m.caches = append(m.caches, newCache()) m.caches = append(m.caches, newCache())
@ -72,7 +78,7 @@ func (m *CachingMux) Add(finder Finder, cacheTime, negCacheTime time.Duration, p
// Lookup attempts to resolve the device ID using any of the added Finders, // Lookup attempts to resolve the device ID using any of the added Finders,
// while obeying the cache settings. // while obeying the cache settings.
func (m *CachingMux) Lookup(deviceID protocol.DeviceID) (direct []string, relays []Relay, err error) { func (m *cachingMux) Lookup(deviceID protocol.DeviceID) (direct []string, relays []Relay, err error) {
var pdirect []prioritizedAddress var pdirect []prioritizedAddress
m.mut.RLock() m.mut.RLock()
@ -140,15 +146,15 @@ func (m *CachingMux) Lookup(deviceID protocol.DeviceID) (direct []string, relays
return direct, relays, nil return direct, relays, nil
} }
func (m *CachingMux) String() string { func (m *cachingMux) String() string {
return "discovery cache" return "discovery cache"
} }
func (m *CachingMux) Error() error { func (m *cachingMux) Error() error {
return nil return nil
} }
func (m *CachingMux) ChildErrors() map[string]error { func (m *cachingMux) ChildErrors() map[string]error {
children := make(map[string]error, len(m.finders)) children := make(map[string]error, len(m.finders))
m.mut.RLock() m.mut.RLock()
for _, f := range m.finders { for _, f := range m.finders {
@ -158,7 +164,7 @@ func (m *CachingMux) ChildErrors() map[string]error {
return children return children
} }
func (m *CachingMux) Cache() map[protocol.DeviceID]CacheEntry { func (m *cachingMux) Cache() map[protocol.DeviceID]CacheEntry {
// Res will be the "total" cache, i.e. the union of our cache and all our // Res will be the "total" cache, i.e. the union of our cache and all our
// children's caches. // children's caches.
res := make(map[protocol.DeviceID]CacheEntry) res := make(map[protocol.DeviceID]CacheEntry)

View File

@ -33,7 +33,7 @@ func TestCacheUnique(t *testing.T) {
relays := []Relay{{URL: "relay://192.0.2.44:443"}, {URL: "tcp://192.0.2.45:443"}} relays := []Relay{{URL: "relay://192.0.2.44:443"}, {URL: "tcp://192.0.2.45:443"}}
c := NewCachingMux() c := NewCachingMux()
c.ServeBackground() c.(*cachingMux).ServeBackground()
defer c.Stop() defer c.Stop()
// Add a fake discovery service and verify we get it's answers through the // Add a fake discovery service and verify we get it's answers through the
@ -94,7 +94,7 @@ func (f *fakeDiscovery) Cache() map[protocol.DeviceID]CacheEntry {
func TestCacheSlowLookup(t *testing.T) { func TestCacheSlowLookup(t *testing.T) {
c := NewCachingMux() c := NewCachingMux()
c.ServeBackground() c.(*cachingMux).ServeBackground()
defer c.Stop() defer c.Stop()
// Add a slow discovery service. // Add a slow discovery service.

View File

@ -227,7 +227,7 @@ func (s *Subscription) C() <-chan Event {
return s.events return s.events
} }
type BufferedSubscription struct { type bufferedSubscription struct {
sub *Subscription sub *Subscription
buf []Event buf []Event
next int next int
@ -236,8 +236,12 @@ type BufferedSubscription struct {
cond *stdsync.Cond cond *stdsync.Cond
} }
func NewBufferedSubscription(s *Subscription, size int) *BufferedSubscription { type BufferedSubscription interface {
bs := &BufferedSubscription{ Since(id int, into []Event) []Event
}
func NewBufferedSubscription(s *Subscription, size int) BufferedSubscription {
bs := &bufferedSubscription{
sub: s, sub: s,
buf: make([]Event, size), buf: make([]Event, size),
mut: sync.NewMutex(), mut: sync.NewMutex(),
@ -247,7 +251,7 @@ func NewBufferedSubscription(s *Subscription, size int) *BufferedSubscription {
return bs return bs
} }
func (s *BufferedSubscription) pollingLoop() { func (s *bufferedSubscription) pollingLoop() {
for { for {
ev, err := s.sub.Poll(60 * time.Second) ev, err := s.sub.Poll(60 * time.Second)
if err == ErrTimeout { if err == ErrTimeout {
@ -269,7 +273,7 @@ func (s *BufferedSubscription) pollingLoop() {
} }
} }
func (s *BufferedSubscription) Since(id int, into []Event) []Event { func (s *bufferedSubscription) Since(id int, into []Event) []Event {
s.mut.Lock() s.mut.Lock()
defer s.mut.Unlock() defer s.mut.Unlock()

View File

@ -290,7 +290,12 @@ func (l *facilityLogger) Debugf(format string, vals ...interface{}) {
} }
// A Recorder keeps a size limited record of log events. // A Recorder keeps a size limited record of log events.
type Recorder struct { type Recorder interface {
Since(t time.Time) []Line
Clear()
}
type recorder struct {
lines []Line lines []Line
initial int initial int
mut sync.Mutex mut sync.Mutex
@ -302,8 +307,8 @@ type Line struct {
Message string `json:"message"` Message string `json:"message"`
} }
func NewRecorder(l Logger, level LogLevel, size, initial int) *Recorder { func NewRecorder(l Logger, level LogLevel, size, initial int) Recorder {
r := &Recorder{ r := &recorder{
lines: make([]Line, 0, size), lines: make([]Line, 0, size),
initial: initial, initial: initial,
} }
@ -311,7 +316,7 @@ func NewRecorder(l Logger, level LogLevel, size, initial int) *Recorder {
return r return r
} }
func (r *Recorder) Since(t time.Time) []Line { func (r *recorder) Since(t time.Time) []Line {
r.mut.Lock() r.mut.Lock()
defer r.mut.Unlock() defer r.mut.Unlock()
@ -330,13 +335,13 @@ func (r *Recorder) Since(t time.Time) []Line {
return cp return cp
} }
func (r *Recorder) Clear() { func (r *recorder) Clear() {
r.mut.Lock() r.mut.Lock()
r.lines = r.lines[:0] r.lines = r.lines[:0]
r.mut.Unlock() r.mut.Unlock()
} }
func (r *Recorder) append(l LogLevel, msg string) { func (r *recorder) append(l LogLevel, msg string) {
line := Line{ line := Line{
When: time.Now(), When: time.Now(),
Message: msg, Message: msg,

View File

@ -26,7 +26,14 @@ const (
eventBroadcasterCheckInterval = 10 * time.Second eventBroadcasterCheckInterval = 10 * time.Second
) )
type Service struct { type Service interface {
suture.Service
Accept() *tls.Conn
Relays() []string
RelayStatus(uri string) (time.Duration, bool)
}
type service struct {
*suture.Supervisor *suture.Supervisor
cfg *config.Wrapper cfg *config.Wrapper
tlsCfg *tls.Config tlsCfg *tls.Config
@ -38,10 +45,10 @@ type Service struct {
conns chan *tls.Conn conns chan *tls.Conn
} }
func NewService(cfg *config.Wrapper, tlsCfg *tls.Config) *Service { func NewService(cfg *config.Wrapper, tlsCfg *tls.Config) Service {
conns := make(chan *tls.Conn) conns := make(chan *tls.Conn)
service := &Service{ service := &service{
Supervisor: suture.New("Service", suture.Spec{ Supervisor: suture.New("Service", suture.Spec{
Log: func(log string) { Log: func(log string) {
l.Debugln(log) l.Debugln(log)
@ -82,7 +89,7 @@ func NewService(cfg *config.Wrapper, tlsCfg *tls.Config) *Service {
return service return service
} }
func (s *Service) VerifyConfiguration(from, to config.Configuration) error { func (s *service) VerifyConfiguration(from, to config.Configuration) error {
for _, addr := range to.Options.RelayServers { for _, addr := range to.Options.RelayServers {
_, err := url.Parse(addr) _, err := url.Parse(addr)
if err != nil { if err != nil {
@ -92,7 +99,7 @@ func (s *Service) VerifyConfiguration(from, to config.Configuration) error {
return nil return nil
} }
func (s *Service) CommitConfiguration(from, to config.Configuration) bool { func (s *service) CommitConfiguration(from, to config.Configuration) bool {
existing := make(map[string]*url.URL, len(to.Options.RelayServers)) existing := make(map[string]*url.URL, len(to.Options.RelayServers))
for _, addr := range to.Options.RelayServers { for _, addr := range to.Options.RelayServers {
@ -142,7 +149,7 @@ type Status struct {
} }
// Relays return the list of relays that currently have an OK status. // Relays return the list of relays that currently have an OK status.
func (s *Service) Relays() []string { func (s *service) Relays() []string {
if s == nil { if s == nil {
// A nil client does not have a status, really. Yet we may be called // A nil client does not have a status, really. Yet we may be called
// this way, for raisins... // this way, for raisins...
@ -162,7 +169,7 @@ func (s *Service) Relays() []string {
} }
// RelayStatus returns the latency and OK status for a given relay. // RelayStatus returns the latency and OK status for a given relay.
func (s *Service) RelayStatus(uri string) (time.Duration, bool) { func (s *service) RelayStatus(uri string) (time.Duration, bool) {
if s == nil { if s == nil {
// A nil client does not have a status, really. Yet we may be called // A nil client does not have a status, really. Yet we may be called
// this way, for raisins... // this way, for raisins...
@ -182,7 +189,7 @@ func (s *Service) RelayStatus(uri string) (time.Duration, bool) {
} }
// Accept returns a new *tls.Conn. The connection is already handshaken. // Accept returns a new *tls.Conn. The connection is already handshaken.
func (s *Service) Accept() *tls.Conn { func (s *service) Accept() *tls.Conn {
return <-s.conns return <-s.conns
} }
@ -234,7 +241,7 @@ func (r *invitationReceiver) Stop() {
// no way to get the event feed directly from the relay lib. This may be // no way to get the event feed directly from the relay lib. This may be
// something to revisit later, possibly. // something to revisit later, possibly.
type eventBroadcaster struct { type eventBroadcaster struct {
Service *Service Service Service
stop chan struct{} stop chan struct{}
} }