From 34285d8cca93fc0f473636e78680fade03f26bda Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 16 Oct 2017 08:09:43 -0700 Subject: parallel tests (#7629) --- app/app.go | 8 ++++-- app/app_test.go | 48 ++++++++++++++++++++++++++++++++ app/apptestlib.go | 45 ++++++++++++++++++++++++++++-- app/options.go | 38 ++++++++++++++++++++++---- app/server.go | 82 ++++++++++++++++++++++++++++++++++++++++++------------- 5 files changed, 193 insertions(+), 28 deletions(-) create mode 100644 app/app_test.go (limited to 'app') diff --git a/app/app.go b/app/app.go index 7b6499b1f..34c0721a0 100644 --- a/app/app.go +++ b/app/app.go @@ -47,7 +47,8 @@ type App struct { Mfa einterfaces.MfaInterface Saml einterfaces.SamlInterface - newStore func() store.Store + newStore func() store.Store + configOverride func(*model.Config) *model.Config } var appCount = 0 @@ -77,7 +78,7 @@ func New(options ...Option) *App { if app.newStore == nil { app.newStore = func() store.Store { - return store.NewLayeredStore(sqlstore.NewSqlSupplier(utils.Cfg.SqlSettings, app.Metrics), app.Metrics, app.Cluster) + return store.NewLayeredStore(sqlstore.NewSqlSupplier(app.Config().SqlSettings, app.Metrics), app.Metrics, app.Cluster) } } @@ -233,6 +234,9 @@ func (a *App) initEnterprise() { } func (a *App) Config() *model.Config { + if a.configOverride != nil { + return a.configOverride(utils.Cfg) + } return utils.Cfg } diff --git a/app/app_test.go b/app/app_test.go new file mode 100644 index 000000000..00d08fb14 --- /dev/null +++ b/app/app_test.go @@ -0,0 +1,48 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "flag" + "os" + "testing" + + l4g "github.com/alecthomas/log4go" + + "github.com/mattermost/mattermost-server/store/storetest" + "github.com/mattermost/mattermost-server/utils" +) + +func TestMain(m *testing.M) { + flag.Parse() + + // In the case where a dev just wants to run a single test, it's faster to just use the default + // store. + if filter := flag.Lookup("test.run").Value.String(); filter != "" && filter != "." { + utils.TranslationsPreInit() + utils.LoadConfig("config.json") + l4g.Info("-test.run used, not creating temporary containers") + os.Exit(m.Run()) + } + + utils.TranslationsPreInit() + utils.LoadConfig("config.json") + utils.InitTranslations(utils.Cfg.LocalizationSettings) + + status := 0 + + container, settings, err := storetest.NewMySQLContainer() + if err != nil { + panic(err) + } + + UseTestStore(container, settings) + + defer func() { + StopTestStore() + os.Exit(status) + }() + + status = m.Run() +} diff --git a/app/apptestlib.go b/app/apptestlib.go index 09bf02d39..9c26e0bbb 100644 --- a/app/apptestlib.go +++ b/app/apptestlib.go @@ -7,6 +7,9 @@ import ( "time" "github.com/mattermost/mattermost-server/model" + "github.com/mattermost/mattermost-server/store" + "github.com/mattermost/mattermost-server/store/sqlstore" + "github.com/mattermost/mattermost-server/store/storetest" "github.com/mattermost/mattermost-server/utils" l4g "github.com/alecthomas/log4go" @@ -21,13 +24,47 @@ type TestHelper struct { BasicPost *model.Post } +type persistentTestStore struct { + store.Store +} + +func (*persistentTestStore) Close() {} + +var testStoreContainer *storetest.RunningContainer +var testStore *persistentTestStore + +// UseTestStore sets the container and corresponding settings to use for tests. Once the tests are +// complete (e.g. at the end of your TestMain implementation), you should call StopTestStore. +func UseTestStore(container *storetest.RunningContainer, settings *model.SqlSettings) { + testStoreContainer = container + testStore = &persistentTestStore{store.NewLayeredStore(sqlstore.NewSqlSupplier(*settings, nil), nil, nil)} +} + +func StopTestStore() { + if testStoreContainer != nil { + testStoreContainer.Stop() + testStoreContainer = nil + } +} + func setupTestHelper(enterprise bool) *TestHelper { - utils.TranslationsPreInit() + if utils.T == nil { + utils.TranslationsPreInit() + } utils.LoadConfig("config.json") utils.InitTranslations(utils.Cfg.LocalizationSettings) + var options []Option + if testStore != nil { + options = append(options, StoreOverride(testStore)) + options = append(options, ConfigOverride(func(cfg *model.Config) { + cfg.ServiceSettings.ListenAddress = new(string) + *cfg.ServiceSettings.ListenAddress = ":0" + })) + } + th := &TestHelper{ - App: New(), + App: New(options...), } *utils.Cfg.TeamSettings.MaxUsersPerTeam = 50 @@ -188,4 +225,8 @@ func (me *TestHelper) LinkUserToTeam(user *model.User, team *model.Team) { func (me *TestHelper) TearDown() { me.App.Shutdown() + if err := recover(); err != nil { + StopTestStore() + panic(err) + } } diff --git a/app/options.go b/app/options.go index e5ac85706..121bbbf80 100644 --- a/app/options.go +++ b/app/options.go @@ -4,25 +4,53 @@ package app import ( + "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/store" ) type Option func(a *App) +// By default, the app will use a global configuration file. This allows you to override all or part +// of that configuration. +// +// The override parameter must be a *model.Config, func(*model.Config), or func(*model.Config) *model.Config. +// +// XXX: Most code will not respect this at the moment. (We need to eliminate utils.Cfg first.) +func ConfigOverride(override interface{}) Option { + return func(a *App) { + switch o := override.(type) { + case *model.Config: + a.configOverride = func(*model.Config) *model.Config { + return o + } + case func(*model.Config): + a.configOverride = func(cfg *model.Config) *model.Config { + ret := *cfg + o(&ret) + return &ret + } + case func(*model.Config) *model.Config: + a.configOverride = o + default: + panic("invalid ConfigOverride") + } + } +} + // By default, the app will use the store specified by the configuration. This allows you to // construct an app with a different store. // -// The storeOrFactory parameter must be either a store.Store or func(App) store.Store. -func StoreOverride(storeOrFactory interface{}) Option { +// The override parameter must be either a store.Store or func(App) store.Store. +func StoreOverride(override interface{}) Option { return func(a *App) { - switch s := storeOrFactory.(type) { + switch o := override.(type) { case store.Store: a.newStore = func() store.Store { - return s + return o } case func(*App) store.Store: a.newStore = func() store.Store { - return s(a) + return o(a) } default: panic("invalid StoreOverride") diff --git a/app/server.go b/app/server.go index c509d0440..08772dce4 100644 --- a/app/server.go +++ b/app/server.go @@ -4,6 +4,7 @@ package app import ( + "context" "crypto/tls" "io" "io/ioutil" @@ -16,7 +17,6 @@ import ( "github.com/gorilla/handlers" "github.com/gorilla/mux" "github.com/rsc/letsencrypt" - "github.com/tylerb/graceful" "gopkg.in/throttled/throttled.v2" "gopkg.in/throttled/throttled.v2/store/memstore" @@ -29,7 +29,8 @@ type Server struct { Store store.Store WebSocketRouter *WebSocketRouter Router *mux.Router - GracefulServer *graceful.Server + Server *http.Server + ListenAddr *net.TCPAddr } var allowedMethods []string = []string{ @@ -152,16 +153,29 @@ func (a *App) StartServer() { handler = httpRateLimiter.RateLimit(handler) } - a.Srv.GracefulServer = &graceful.Server{ - Timeout: TIME_TO_WAIT_FOR_CONNECTIONS_TO_CLOSE_ON_SERVER_SHUTDOWN, - Server: &http.Server{ - Addr: *utils.Cfg.ServiceSettings.ListenAddress, - Handler: handlers.RecoveryHandler(handlers.RecoveryLogger(&RecoveryLogger{}), handlers.PrintRecoveryStack(true))(handler), - ReadTimeout: time.Duration(*utils.Cfg.ServiceSettings.ReadTimeout) * time.Second, - WriteTimeout: time.Duration(*utils.Cfg.ServiceSettings.WriteTimeout) * time.Second, - }, + a.Srv.Server = &http.Server{ + Handler: handlers.RecoveryHandler(handlers.RecoveryLogger(&RecoveryLogger{}), handlers.PrintRecoveryStack(true))(handler), + ReadTimeout: time.Duration(*utils.Cfg.ServiceSettings.ReadTimeout) * time.Second, + WriteTimeout: time.Duration(*utils.Cfg.ServiceSettings.WriteTimeout) * time.Second, } - l4g.Info(utils.T("api.server.start_server.listening.info"), *utils.Cfg.ServiceSettings.ListenAddress) + + addr := *a.Config().ServiceSettings.ListenAddress + if addr == "" { + if *utils.Cfg.ServiceSettings.ConnectionSecurity == model.CONN_SECURITY_TLS { + addr = ":https" + } else { + addr = ":http" + } + } + + listener, err := net.Listen("tcp", addr) + if err != nil { + l4g.Critical(utils.T("api.server.start_server.starting.critical"), err) + return + } + a.Srv.ListenAddr = listener.Addr().(*net.TCPAddr) + + l4g.Info(utils.T("api.server.start_server.listening.info"), listener.Addr().String()) if *utils.Cfg.ServiceSettings.Forward80To443 { go func() { @@ -189,25 +203,55 @@ func (a *App) StartServer() { tlsConfig.NextProtos = append(tlsConfig.NextProtos, "h2") - err = a.Srv.GracefulServer.ListenAndServeTLSConfig(tlsConfig) + a.Srv.Server.TLSConfig = tlsConfig + err = a.Srv.Server.ServeTLS(listener, "", "") } else { - err = a.Srv.GracefulServer.ListenAndServeTLS(*utils.Cfg.ServiceSettings.TLSCertFile, *utils.Cfg.ServiceSettings.TLSKeyFile) + err = a.Srv.Server.ServeTLS(listener, *utils.Cfg.ServiceSettings.TLSCertFile, *utils.Cfg.ServiceSettings.TLSKeyFile) } } else { - err = a.Srv.GracefulServer.ListenAndServe() + err = a.Srv.Server.Serve(listener) } - if err != nil { + if err != nil && err != http.ErrServerClosed { l4g.Critical(utils.T("api.server.start_server.starting.critical"), err) time.Sleep(time.Second) } }() } +type tcpKeepAliveListener struct { + *net.TCPListener +} + +func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) { + tc, err := ln.AcceptTCP() + if err != nil { + return + } + tc.SetKeepAlive(true) + tc.SetKeepAlivePeriod(3 * time.Minute) + return tc, nil +} + +func (a *App) Listen(addr string) (net.Listener, error) { + if addr == "" { + addr = ":http" + } + ln, err := net.Listen("tcp", addr) + if err != nil { + return nil, err + } + return tcpKeepAliveListener{ln.(*net.TCPListener)}, nil +} + func (a *App) StopServer() { - if a.Srv.GracefulServer != nil { - a.Srv.GracefulServer.Stop(TIME_TO_WAIT_FOR_CONNECTIONS_TO_CLOSE_ON_SERVER_SHUTDOWN) - <-a.Srv.GracefulServer.StopChan() - a.Srv.GracefulServer = nil + if a.Srv.Server != nil { + ctx, cancel := context.WithTimeout(context.Background(), TIME_TO_WAIT_FOR_CONNECTIONS_TO_CLOSE_ON_SERVER_SHUTDOWN) + defer cancel() + if err := a.Srv.Server.Shutdown(ctx); err != nil { + l4g.Warn(err.Error()) + } + a.Srv.Server.Close() + a.Srv.Server = nil } } -- cgit v1.2.3-1-g7c22