feat(pprof): add pprof HTTP server (#807)
- `PPROF_ENABLED=no` - `PPROF_BLOCK_PROFILE_RATE=0` - `PPROF_MUTEX_PROFILE_RATE=0` - `PPROF_HTTP_SERVER_ADDRESS=":6060"`
This commit is contained in:
7
internal/httpserver/address.go
Normal file
7
internal/httpserver/address.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package httpserver
|
||||
|
||||
// GetAddress obtains the address the HTTP server is listening on.
|
||||
func (s *Server) GetAddress() (address string) {
|
||||
<-s.addressSet
|
||||
return s.address
|
||||
}
|
||||
43
internal/httpserver/helpers_test.go
Normal file
43
internal/httpserver/helpers_test.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
func stringPtr(s string) *string { return &s }
|
||||
func durationPtr(d time.Duration) *time.Duration { return &d }
|
||||
|
||||
var _ Logger = (*testLogger)(nil)
|
||||
|
||||
type testLogger struct{}
|
||||
|
||||
func (t *testLogger) Info(msg string) {}
|
||||
func (t *testLogger) Warn(msg string) {}
|
||||
func (t *testLogger) Error(msg string) {}
|
||||
|
||||
var _ gomock.Matcher = (*regexMatcher)(nil)
|
||||
|
||||
type regexMatcher struct {
|
||||
regexp *regexp.Regexp
|
||||
}
|
||||
|
||||
func (r *regexMatcher) Matches(x interface{}) bool {
|
||||
s, ok := x.(string)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return r.regexp.MatchString(s)
|
||||
}
|
||||
|
||||
func (r *regexMatcher) String() string {
|
||||
return "regular expression " + r.regexp.String()
|
||||
}
|
||||
|
||||
func newRegexMatcher(regex string) *regexMatcher {
|
||||
return ®exMatcher{
|
||||
regexp: regexp.MustCompile(regex),
|
||||
}
|
||||
}
|
||||
9
internal/httpserver/logger.go
Normal file
9
internal/httpserver/logger.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package httpserver
|
||||
|
||||
// Logger is the logger interface accepted by the
|
||||
// HTTP server.
|
||||
type Logger interface {
|
||||
Info(msg string)
|
||||
Warn(msg string)
|
||||
Error(msg string)
|
||||
}
|
||||
70
internal/httpserver/logger_mock_test.go
Normal file
70
internal/httpserver/logger_mock_test.go
Normal file
@@ -0,0 +1,70 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/qdm12/gluetun/internal/httpserver (interfaces: Logger)
|
||||
|
||||
// Package httpserver is a generated GoMock package.
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
// MockLogger is a mock of Logger interface.
|
||||
type MockLogger struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockLoggerMockRecorder
|
||||
}
|
||||
|
||||
// MockLoggerMockRecorder is the mock recorder for MockLogger.
|
||||
type MockLoggerMockRecorder struct {
|
||||
mock *MockLogger
|
||||
}
|
||||
|
||||
// NewMockLogger creates a new mock instance.
|
||||
func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
|
||||
mock := &MockLogger{ctrl: ctrl}
|
||||
mock.recorder = &MockLoggerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Error mocks base method.
|
||||
func (m *MockLogger) Error(arg0 string) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Error", arg0)
|
||||
}
|
||||
|
||||
// Error indicates an expected call of Error.
|
||||
func (mr *MockLoggerMockRecorder) Error(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockLogger)(nil).Error), arg0)
|
||||
}
|
||||
|
||||
// Info mocks base method.
|
||||
func (m *MockLogger) Info(arg0 string) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Info", arg0)
|
||||
}
|
||||
|
||||
// Info indicates an expected call of Info.
|
||||
func (mr *MockLoggerMockRecorder) Info(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockLogger)(nil).Info), arg0)
|
||||
}
|
||||
|
||||
// Warn mocks base method.
|
||||
func (m *MockLogger) Warn(arg0 string) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Warn", arg0)
|
||||
}
|
||||
|
||||
// Warn indicates an expected call of Warn.
|
||||
func (mr *MockLoggerMockRecorder) Warn(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warn", reflect.TypeOf((*MockLogger)(nil).Warn), arg0)
|
||||
}
|
||||
66
internal/httpserver/run.go
Normal file
66
internal/httpserver/run.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Run runs the HTTP server until ctx is canceled.
|
||||
// The done channel has an error written to when the HTTP server
|
||||
// is terminated, and can be nil or not nil.
|
||||
func (s *Server) Run(ctx context.Context, ready chan<- struct{}, done chan<- struct{}) {
|
||||
server := http.Server{Addr: s.address, Handler: s.handler}
|
||||
|
||||
crashed := make(chan struct{})
|
||||
shutdownDone := make(chan struct{})
|
||||
go func() {
|
||||
defer close(shutdownDone)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-crashed:
|
||||
return
|
||||
}
|
||||
|
||||
s.logger.Warn(s.name + " http server shutting down: " + ctx.Err().Error())
|
||||
shutdownCtx, cancel := context.WithTimeout(
|
||||
context.Background(), s.shutdownTimeout)
|
||||
defer cancel()
|
||||
if err := server.Shutdown(shutdownCtx); err != nil {
|
||||
s.logger.Error(s.name + " http server failed shutting down within " +
|
||||
s.shutdownTimeout.String())
|
||||
}
|
||||
}()
|
||||
|
||||
listener, err := net.Listen("tcp", s.address)
|
||||
if err != nil {
|
||||
close(s.addressSet)
|
||||
close(crashed) // stop shutdown goroutine
|
||||
<-shutdownDone
|
||||
s.logger.Error(err.Error())
|
||||
close(done)
|
||||
return
|
||||
}
|
||||
|
||||
s.address = listener.Addr().String()
|
||||
close(s.addressSet)
|
||||
|
||||
// note: no further write so no need to mutex
|
||||
s.logger.Info(s.name + " http server listening on " + s.address)
|
||||
close(ready)
|
||||
|
||||
err = server.Serve(listener)
|
||||
|
||||
if err != nil && !errors.Is(ctx.Err(), context.Canceled) {
|
||||
// server crashed
|
||||
close(crashed) // stop shutdown goroutine
|
||||
} else {
|
||||
err = nil
|
||||
}
|
||||
<-shutdownDone
|
||||
if err != nil {
|
||||
s.logger.Error(err.Error())
|
||||
}
|
||||
close(done)
|
||||
}
|
||||
75
internal/httpserver/run_test.go
Normal file
75
internal/httpserver/run_test.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_Server_Run_success(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
logger := NewMockLogger(ctrl)
|
||||
logger.EXPECT().Info(newRegexMatcher("^test http server listening on 127.0.0.1:[1-9][0-9]{0,4}$"))
|
||||
logger.EXPECT().Warn("test http server shutting down: context canceled")
|
||||
const shutdownTimeout = 10 * time.Second
|
||||
|
||||
server := &Server{
|
||||
name: "test",
|
||||
address: "127.0.0.1:0",
|
||||
addressSet: make(chan struct{}),
|
||||
logger: logger,
|
||||
shutdownTimeout: shutdownTimeout,
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ready := make(chan struct{})
|
||||
done := make(chan struct{})
|
||||
|
||||
go server.Run(ctx, ready, done)
|
||||
|
||||
addressRegex := regexp.MustCompile(`^127.0.0.1:[1-9][0-9]{0,4}$`)
|
||||
address := server.GetAddress()
|
||||
assert.Regexp(t, addressRegex, address)
|
||||
address = server.GetAddress()
|
||||
assert.Regexp(t, addressRegex, address)
|
||||
|
||||
<-ready
|
||||
|
||||
cancel()
|
||||
_, ok := <-done
|
||||
assert.False(t, ok)
|
||||
}
|
||||
|
||||
func Test_Server_Run_failure(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
logger := NewMockLogger(ctrl)
|
||||
logger.EXPECT().Error("listen tcp: address -1: invalid port")
|
||||
|
||||
server := &Server{
|
||||
name: "test",
|
||||
address: "127.0.0.1:-1",
|
||||
addressSet: make(chan struct{}),
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
ready := make(chan struct{})
|
||||
done := make(chan struct{})
|
||||
|
||||
go server.Run(context.Background(), ready, done)
|
||||
|
||||
select {
|
||||
case <-ready:
|
||||
t.Fatal("server should not be ready")
|
||||
case _, ok := <-done:
|
||||
assert.False(t, ok)
|
||||
}
|
||||
}
|
||||
57
internal/httpserver/server.go
Normal file
57
internal/httpserver/server.go
Normal file
@@ -0,0 +1,57 @@
|
||||
// Package httpserver implements an HTTP server.
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
var _ Interface = (*Server)(nil)
|
||||
|
||||
// Interface is the HTTP server composite interface.
|
||||
type Interface interface {
|
||||
Runner
|
||||
AddressGetter
|
||||
}
|
||||
|
||||
// Runner is the interface for an HTTP server with a Run method.
|
||||
type Runner interface {
|
||||
Run(ctx context.Context, ready chan<- struct{}, done chan<- struct{})
|
||||
}
|
||||
|
||||
// AddressGetter obtains the address the HTTP server is listening on.
|
||||
type AddressGetter interface {
|
||||
GetAddress() (address string)
|
||||
}
|
||||
|
||||
// Server is an HTTP server implementation, which uses
|
||||
// the HTTP handler provided.
|
||||
type Server struct {
|
||||
name string
|
||||
address string
|
||||
addressSet chan struct{}
|
||||
handler http.Handler
|
||||
logger Logger
|
||||
shutdownTimeout time.Duration
|
||||
}
|
||||
|
||||
// New creates a new HTTP server with the given settings.
|
||||
// It returns an error if one of the settings is not valid.
|
||||
func New(settings Settings) (s *Server, err error) {
|
||||
settings.SetDefaults()
|
||||
|
||||
if err = settings.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("http server settings validation failed: %w", err)
|
||||
}
|
||||
|
||||
return &Server{
|
||||
name: *settings.Name,
|
||||
address: settings.Address,
|
||||
addressSet: make(chan struct{}),
|
||||
handler: settings.Handler,
|
||||
logger: settings.Logger,
|
||||
shutdownTimeout: *settings.ShutdownTimeout,
|
||||
}, nil
|
||||
}
|
||||
67
internal/httpserver/server_test.go
Normal file
67
internal/httpserver/server_test.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
//go:generate mockgen -destination=logger_mock_test.go -package $GOPACKAGE . Logger
|
||||
|
||||
func Test_New(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
someHandler := http.NewServeMux()
|
||||
someLogger := &testLogger{}
|
||||
|
||||
testCases := map[string]struct {
|
||||
settings Settings
|
||||
expected *Server
|
||||
errWrapped error
|
||||
errMessage string
|
||||
}{
|
||||
"empty settings": {
|
||||
errWrapped: ErrHandlerIsNotSet,
|
||||
errMessage: "http server settings validation failed: HTTP handler cannot be left unset",
|
||||
},
|
||||
"filled settings": {
|
||||
settings: Settings{
|
||||
Name: stringPtr("name"),
|
||||
Address: ":8001",
|
||||
Handler: someHandler,
|
||||
Logger: someLogger,
|
||||
ShutdownTimeout: durationPtr(time.Second),
|
||||
},
|
||||
expected: &Server{
|
||||
name: "name",
|
||||
address: ":8001",
|
||||
handler: someHandler,
|
||||
logger: someLogger,
|
||||
shutdownTimeout: time.Second,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
server, err := New(testCase.settings)
|
||||
assert.ErrorIs(t, err, testCase.errWrapped)
|
||||
if testCase.errWrapped != nil {
|
||||
require.EqualError(t, err, testCase.errMessage)
|
||||
}
|
||||
|
||||
if server != nil {
|
||||
assert.NotNil(t, server.addressSet)
|
||||
server.addressSet = nil
|
||||
}
|
||||
|
||||
assert.Equal(t, testCase.expected, server)
|
||||
})
|
||||
}
|
||||
}
|
||||
111
internal/httpserver/settings.go
Normal file
111
internal/httpserver/settings.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||
"github.com/qdm12/gotree"
|
||||
"github.com/qdm12/govalid/address"
|
||||
)
|
||||
|
||||
type Settings struct {
|
||||
// Name is the server name to use in logs.
|
||||
// It defaults to the empty string.
|
||||
Name *string
|
||||
// Address is the server listening address.
|
||||
// It defaults to :8000.
|
||||
Address string
|
||||
// Handler is the HTTP Handler to use.
|
||||
// It must be set and cannot be left to nil.
|
||||
Handler http.Handler
|
||||
// Logger is the logger to use.
|
||||
// It must be set and cannot be left to nil.
|
||||
Logger Logger
|
||||
// ShutdownTimeout is the shutdown timeout duration
|
||||
// of the HTTP server. It defaults to 3 seconds.
|
||||
ShutdownTimeout *time.Duration
|
||||
}
|
||||
|
||||
func (s *Settings) SetDefaults() {
|
||||
s.Name = helpers.DefaultStringPtr(s.Name, "")
|
||||
s.Address = helpers.DefaultString(s.Address, ":8000")
|
||||
const defaultShutdownTimeout = 3 * time.Second
|
||||
s.ShutdownTimeout = helpers.DefaultDuration(s.ShutdownTimeout, defaultShutdownTimeout)
|
||||
}
|
||||
|
||||
func (s Settings) Copy() Settings {
|
||||
return Settings{
|
||||
Name: helpers.CopyStringPtr(s.Name),
|
||||
Address: s.Address,
|
||||
Handler: s.Handler,
|
||||
Logger: s.Logger,
|
||||
ShutdownTimeout: helpers.CopyDurationPtr(s.ShutdownTimeout),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Settings) MergeWith(other Settings) {
|
||||
s.Name = helpers.MergeWithStringPtr(s.Name, other.Name)
|
||||
s.Address = helpers.MergeWithString(s.Address, other.Address)
|
||||
s.Handler = helpers.MergeWithHTTPHandler(s.Handler, other.Handler)
|
||||
if s.Logger == nil {
|
||||
s.Logger = other.Logger
|
||||
}
|
||||
s.ShutdownTimeout = helpers.MergeWithDuration(s.ShutdownTimeout, other.ShutdownTimeout)
|
||||
}
|
||||
|
||||
func (s *Settings) OverrideWith(other Settings) {
|
||||
s.Name = helpers.OverrideWithStringPtr(s.Name, other.Name)
|
||||
s.Address = helpers.OverrideWithString(s.Address, other.Address)
|
||||
s.Handler = helpers.OverrideWithHTTPHandler(s.Handler, other.Handler)
|
||||
if other.Logger != nil {
|
||||
s.Logger = other.Logger
|
||||
}
|
||||
s.ShutdownTimeout = helpers.OverrideWithDuration(s.ShutdownTimeout, other.ShutdownTimeout)
|
||||
}
|
||||
|
||||
var (
|
||||
ErrHandlerIsNotSet = errors.New("HTTP handler cannot be left unset")
|
||||
ErrLoggerIsNotSet = errors.New("logger cannot be left unset")
|
||||
ErrShutdownTimeoutTooSmall = errors.New("shutdown timeout is too small")
|
||||
)
|
||||
|
||||
func (s Settings) Validate() (err error) {
|
||||
uid := os.Getuid()
|
||||
_, err = address.Validate(s.Address, address.OptionListening(uid))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s.Handler == nil {
|
||||
return ErrHandlerIsNotSet
|
||||
}
|
||||
|
||||
if s.Logger == nil {
|
||||
return ErrLoggerIsNotSet
|
||||
}
|
||||
|
||||
const minShutdownTimeout = 5 * time.Millisecond
|
||||
if *s.ShutdownTimeout < minShutdownTimeout {
|
||||
return fmt.Errorf("%w: %s must be at least %s",
|
||||
ErrShutdownTimeoutTooSmall,
|
||||
*s.ShutdownTimeout, minShutdownTimeout)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s Settings) ToLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("%s HTTP server settings:", strings.Title(*s.Name))
|
||||
node.Appendf("Listening address: %s", s.Address)
|
||||
node.Appendf("Shutdown timeout: %s", *s.ShutdownTimeout)
|
||||
return node
|
||||
}
|
||||
|
||||
func (s Settings) String() string {
|
||||
return s.ToLinesNode().String()
|
||||
}
|
||||
330
internal/httpserver/settings_test.go
Normal file
330
internal/httpserver/settings_test.go
Normal file
@@ -0,0 +1,330 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/govalid/address"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_Settings_SetDefaults(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const defaultTimeout = 3 * time.Second
|
||||
|
||||
testCases := map[string]struct {
|
||||
settings Settings
|
||||
expected Settings
|
||||
}{
|
||||
"empty settings": {
|
||||
settings: Settings{},
|
||||
expected: Settings{
|
||||
Name: stringPtr(""),
|
||||
Address: ":8000",
|
||||
ShutdownTimeout: durationPtr(defaultTimeout),
|
||||
},
|
||||
},
|
||||
"filled settings": {
|
||||
settings: Settings{
|
||||
Name: stringPtr("name"),
|
||||
Address: ":8001",
|
||||
ShutdownTimeout: durationPtr(time.Second),
|
||||
},
|
||||
expected: Settings{
|
||||
Name: stringPtr("name"),
|
||||
Address: ":8001",
|
||||
ShutdownTimeout: durationPtr(time.Second),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCase.settings.SetDefaults()
|
||||
|
||||
assert.Equal(t, testCase.expected, testCase.settings)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Settings_Copy(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
someHandler := http.NewServeMux()
|
||||
someLogger := &testLogger{}
|
||||
|
||||
testCases := map[string]struct {
|
||||
settings Settings
|
||||
expected Settings
|
||||
}{
|
||||
"empty settings": {},
|
||||
"filled settings": {
|
||||
settings: Settings{
|
||||
Name: stringPtr("name"),
|
||||
Address: ":8001",
|
||||
Handler: someHandler,
|
||||
Logger: someLogger,
|
||||
ShutdownTimeout: durationPtr(time.Second),
|
||||
},
|
||||
expected: Settings{
|
||||
Name: stringPtr("name"),
|
||||
Address: ":8001",
|
||||
Handler: someHandler,
|
||||
Logger: someLogger,
|
||||
ShutdownTimeout: durationPtr(time.Second),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
copied := testCase.settings.Copy()
|
||||
|
||||
assert.Equal(t, testCase.expected, copied)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Settings_MergeWith(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
someHandler := http.NewServeMux()
|
||||
someLogger := &testLogger{}
|
||||
|
||||
testCases := map[string]struct {
|
||||
settings Settings
|
||||
other Settings
|
||||
expected Settings
|
||||
}{
|
||||
"merge empty with empty": {},
|
||||
"merge empty with filled": {
|
||||
other: Settings{
|
||||
Name: stringPtr("name"),
|
||||
Address: ":8001",
|
||||
Handler: someHandler,
|
||||
Logger: someLogger,
|
||||
ShutdownTimeout: durationPtr(time.Second),
|
||||
},
|
||||
expected: Settings{
|
||||
Name: stringPtr("name"),
|
||||
Address: ":8001",
|
||||
Handler: someHandler,
|
||||
Logger: someLogger,
|
||||
ShutdownTimeout: durationPtr(time.Second),
|
||||
},
|
||||
},
|
||||
"merge filled with empty": {
|
||||
settings: Settings{
|
||||
Name: stringPtr("name"),
|
||||
Address: ":8001",
|
||||
Handler: someHandler,
|
||||
Logger: someLogger,
|
||||
ShutdownTimeout: durationPtr(time.Second),
|
||||
},
|
||||
expected: Settings{
|
||||
Name: stringPtr("name"),
|
||||
Address: ":8001",
|
||||
Handler: someHandler,
|
||||
Logger: someLogger,
|
||||
ShutdownTimeout: durationPtr(time.Second),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCase.settings.MergeWith(testCase.other)
|
||||
|
||||
assert.Equal(t, testCase.expected, testCase.settings)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Settings_OverrideWith(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
someHandler := http.NewServeMux()
|
||||
someLogger := &testLogger{}
|
||||
|
||||
testCases := map[string]struct {
|
||||
settings Settings
|
||||
other Settings
|
||||
expected Settings
|
||||
}{
|
||||
"override empty with empty": {},
|
||||
"override empty with filled": {
|
||||
other: Settings{
|
||||
Name: stringPtr("name"),
|
||||
Address: ":8001",
|
||||
Handler: someHandler,
|
||||
Logger: someLogger,
|
||||
ShutdownTimeout: durationPtr(time.Second),
|
||||
},
|
||||
expected: Settings{
|
||||
Name: stringPtr("name"),
|
||||
Address: ":8001",
|
||||
Handler: someHandler,
|
||||
Logger: someLogger,
|
||||
ShutdownTimeout: durationPtr(time.Second),
|
||||
},
|
||||
},
|
||||
"override filled with empty": {
|
||||
settings: Settings{
|
||||
Name: stringPtr("name"),
|
||||
Address: ":8001",
|
||||
Handler: someHandler,
|
||||
Logger: someLogger,
|
||||
ShutdownTimeout: durationPtr(time.Second),
|
||||
},
|
||||
expected: Settings{
|
||||
Name: stringPtr("name"),
|
||||
Address: ":8001",
|
||||
Handler: someHandler,
|
||||
Logger: someLogger,
|
||||
ShutdownTimeout: durationPtr(time.Second),
|
||||
},
|
||||
},
|
||||
"override filled with filled": {
|
||||
settings: Settings{
|
||||
Name: stringPtr("name"),
|
||||
Address: ":8001",
|
||||
Handler: someHandler,
|
||||
Logger: someLogger,
|
||||
ShutdownTimeout: durationPtr(time.Second),
|
||||
},
|
||||
other: Settings{
|
||||
Name: stringPtr("name2"),
|
||||
Address: ":8002",
|
||||
ShutdownTimeout: durationPtr(time.Hour),
|
||||
},
|
||||
expected: Settings{
|
||||
Name: stringPtr("name2"),
|
||||
Address: ":8002",
|
||||
Handler: someHandler,
|
||||
Logger: someLogger,
|
||||
ShutdownTimeout: durationPtr(time.Hour),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCase.settings.OverrideWith(testCase.other)
|
||||
|
||||
assert.Equal(t, testCase.expected, testCase.settings)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Settings_Validate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
someHandler := http.NewServeMux()
|
||||
someLogger := &testLogger{}
|
||||
|
||||
testCases := map[string]struct {
|
||||
settings Settings
|
||||
errWrapped error
|
||||
errMessage string
|
||||
}{
|
||||
"bad address": {
|
||||
settings: Settings{
|
||||
Address: "noport",
|
||||
},
|
||||
errWrapped: address.ErrValueNotValid,
|
||||
errMessage: "value is not valid: address noport: missing port in address",
|
||||
},
|
||||
"nil handler": {
|
||||
settings: Settings{
|
||||
Address: ":8000",
|
||||
},
|
||||
errWrapped: ErrHandlerIsNotSet,
|
||||
errMessage: ErrHandlerIsNotSet.Error(),
|
||||
},
|
||||
"nil logger": {
|
||||
settings: Settings{
|
||||
Address: ":8000",
|
||||
Handler: someHandler,
|
||||
},
|
||||
errWrapped: ErrLoggerIsNotSet,
|
||||
errMessage: ErrLoggerIsNotSet.Error(),
|
||||
},
|
||||
"shutdown timeout too small": {
|
||||
settings: Settings{
|
||||
Address: ":8000",
|
||||
Handler: someHandler,
|
||||
Logger: someLogger,
|
||||
ShutdownTimeout: durationPtr(time.Millisecond),
|
||||
},
|
||||
errWrapped: ErrShutdownTimeoutTooSmall,
|
||||
errMessage: "shutdown timeout is too small: 1ms must be at least 5ms",
|
||||
},
|
||||
"valid settings": {
|
||||
settings: Settings{
|
||||
Address: ":8000",
|
||||
Handler: someHandler,
|
||||
Logger: someLogger,
|
||||
ShutdownTimeout: durationPtr(time.Second),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := testCase.settings.Validate()
|
||||
|
||||
assert.ErrorIs(t, err, testCase.errWrapped)
|
||||
if err != nil {
|
||||
assert.EqualError(t, err, testCase.errMessage)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Settings_String(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
settings Settings
|
||||
s string
|
||||
}{
|
||||
"all values": {
|
||||
settings: Settings{
|
||||
Name: stringPtr("name"),
|
||||
Address: ":8000",
|
||||
ShutdownTimeout: durationPtr(time.Second),
|
||||
},
|
||||
s: `Name HTTP server settings:
|
||||
├── Listening address: :8000
|
||||
└── Shutdown timeout: 1s`,
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := testCase.settings.String()
|
||||
|
||||
assert.Equal(t, testCase.s, s)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user