Initial commit
This commit is contained in:
169
internal/config/config.go
Normal file
169
internal/config/config.go
Normal file
@@ -0,0 +1,169 @@
|
||||
// @tag config
|
||||
// @tag core
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/eleksir/pekoe/internal/logger"
|
||||
)
|
||||
|
||||
// Constants for default values - chosen to be production-ready defaults
|
||||
const (
|
||||
DefaultBind = "0.0.0.0"
|
||||
DefaultPort = 6379
|
||||
DefaultMaxClients = 10000
|
||||
DefaultReadTimeout = 0 // 0 means no timeout (blocking I/O)
|
||||
DefaultWriteTimeout = 0 // 0 means no timeout
|
||||
DefaultMaxMsgBytes = 512 * 1024 * 1024 // 512 MB - allows large messages
|
||||
DefaultLogLevel = "info"
|
||||
DefaultLogFile = "" // empty = stdout only
|
||||
DefaultPidFile = "" // empty = no PID file created
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Bind string `json:"bind"`
|
||||
Port int `json:"port"`
|
||||
MaxClients int `json:"max_clients"`
|
||||
ReadTimeout int `json:"read_timeout"` // in seconds, 0 = no timeout
|
||||
WriteTimeout int `json:"write_timeout"` // in seconds, 0 = no timeout
|
||||
MaxMsgBytes int `json:"max_message_bytes"`
|
||||
LogLevel string `json:"log_level"`
|
||||
LogFile string `json:"log_file"` // empty = stdout only
|
||||
PidFile string `json:"pid_file"` // empty = no PID file
|
||||
}
|
||||
|
||||
// SetDefaults applies sensible defaults when config values are missing or invalid.
|
||||
// This ensures the server can start even with incomplete configuration.
|
||||
// nolint: cyclop
|
||||
func (c *Config) SetDefaults() {
|
||||
if c.Bind == "" {
|
||||
c.Bind = DefaultBind
|
||||
}
|
||||
if c.Port <= 0 || c.Port > 65535 {
|
||||
c.Port = DefaultPort
|
||||
}
|
||||
if c.MaxClients <= 0 {
|
||||
c.MaxClients = DefaultMaxClients
|
||||
}
|
||||
if c.ReadTimeout < 0 {
|
||||
c.ReadTimeout = 0
|
||||
}
|
||||
if c.WriteTimeout < 0 {
|
||||
c.WriteTimeout = 0
|
||||
}
|
||||
if c.MaxMsgBytes <= 0 {
|
||||
c.MaxMsgBytes = DefaultMaxMsgBytes
|
||||
}
|
||||
if c.LogLevel == "" {
|
||||
c.LogLevel = DefaultLogLevel
|
||||
}
|
||||
if c.LogFile == "" {
|
||||
c.LogFile = DefaultLogFile
|
||||
}
|
||||
if c.PidFile == "" {
|
||||
c.PidFile = DefaultPidFile
|
||||
}
|
||||
}
|
||||
|
||||
// Validate checks that all configuration values are within acceptable bounds.
|
||||
// Returns an error if validation fails, which should prevent server startup.
|
||||
func (c *Config) Validate() error {
|
||||
if c.Bind == "" {
|
||||
return fmt.Errorf("bind address cannot be empty")
|
||||
}
|
||||
if net.ParseIP(c.Bind) == nil {
|
||||
return fmt.Errorf("bind must be a valid IP address (e.g. 0.0.0.0, ::, ::1, 192.168.1.1)")
|
||||
}
|
||||
if c.Port <= 0 || c.Port > 65535 {
|
||||
return fmt.Errorf("port must be between 1 and 65535")
|
||||
}
|
||||
if c.MaxClients <= 0 {
|
||||
return fmt.Errorf("max_clients must be positive")
|
||||
}
|
||||
if c.MaxMsgBytes <= 0 {
|
||||
return fmt.Errorf("max_message_bytes must be positive")
|
||||
}
|
||||
// Validate log level against allowed values
|
||||
validLevels := map[string]bool{
|
||||
"debug": true,
|
||||
"info": true,
|
||||
"warn": true,
|
||||
"error": true,
|
||||
}
|
||||
if !validLevels[c.LogLevel] {
|
||||
return fmt.Errorf("invalid log_level: %s (must be debug, info, warn, or error)", c.LogLevel)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadTimeoutDuration converts the config's read timeout (in seconds) to a Duration.
|
||||
// Zero means no timeout, allowing the server to block on reads indefinitely.
|
||||
func (c *Config) ReadTimeoutDuration() time.Duration {
|
||||
if c.ReadTimeout == 0 {
|
||||
return 0
|
||||
}
|
||||
return time.Duration(c.ReadTimeout) * time.Second
|
||||
}
|
||||
|
||||
// WriteTimeoutDuration converts the config's write timeout (in seconds) to a Duration.
|
||||
func (c *Config) WriteTimeoutDuration() time.Duration {
|
||||
if c.WriteTimeout == 0 {
|
||||
return 0
|
||||
}
|
||||
return time.Duration(c.WriteTimeout) * time.Second
|
||||
}
|
||||
|
||||
// Load reads and parses the configuration from a JSON file.
|
||||
// It applies defaults for missing/invalid values and validates the final config.
|
||||
func Load(path string) (*Config, error) {
|
||||
//nolint: gosec
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read config file: %w", err)
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse config file: %w", err)
|
||||
}
|
||||
|
||||
// Apply defaults before validation to ensure minimum viable config
|
||||
cfg.SetDefaults()
|
||||
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("config validation failed: %w", err)
|
||||
}
|
||||
|
||||
logger.SetLevel(cfg.LogLevel)
|
||||
|
||||
if cfg.LogFile != "" {
|
||||
if err := logger.SetOutput(cfg.LogFile); err != nil {
|
||||
return nil, fmt.Errorf("failed to set log file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
// Addr returns the bind address in host:port format for use in net.Listen.
|
||||
// IPv6 addresses are wrapped in brackets: [::]:6379.
|
||||
func (c *Config) Addr() string {
|
||||
ip := net.ParseIP(c.Bind)
|
||||
if ip != nil && ip.To4() == nil {
|
||||
// IPv6 address — wrap in brackets
|
||||
return "[" + c.Bind + "]:" + strconv.Itoa(c.Port)
|
||||
}
|
||||
return c.Bind + ":" + strconv.Itoa(c.Port)
|
||||
}
|
||||
|
||||
// String returns a JSON representation of the config for debugging.
|
||||
func (c *Config) String() string {
|
||||
b, _ := json.MarshalIndent(c, "", " ")
|
||||
return string(b)
|
||||
}
|
||||
435
internal/config/config_test.go
Normal file
435
internal/config/config_test.go
Normal file
@@ -0,0 +1,435 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// nolint: cyclop
|
||||
func TestSetDefaults(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg *Config
|
||||
check func(*Config)
|
||||
}{
|
||||
{
|
||||
name: "empty config gets all defaults",
|
||||
cfg: &Config{},
|
||||
check: func(c *Config) {
|
||||
if c.Bind != DefaultBind {
|
||||
t.Errorf("Bind = %q, want %q", c.Bind, DefaultBind)
|
||||
}
|
||||
if c.Port != DefaultPort {
|
||||
t.Errorf("Port = %d, want %d", c.Port, DefaultPort)
|
||||
}
|
||||
if c.MaxClients != DefaultMaxClients {
|
||||
t.Errorf("MaxClients = %d, want %d", c.MaxClients, DefaultMaxClients)
|
||||
}
|
||||
if c.MaxMsgBytes != DefaultMaxMsgBytes {
|
||||
t.Errorf("MaxMsgBytes = %d, want %d", c.MaxMsgBytes, DefaultMaxMsgBytes)
|
||||
}
|
||||
if c.LogLevel != DefaultLogLevel {
|
||||
t.Errorf("LogLevel = %q, want %q", c.LogLevel, DefaultLogLevel)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "port zero uses default",
|
||||
cfg: &Config{Port: 0},
|
||||
check: func(c *Config) {
|
||||
if c.Port != DefaultPort {
|
||||
t.Errorf("Port = %d, want %d", c.Port, DefaultPort)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "port out of range uses default",
|
||||
cfg: &Config{Port: 70000},
|
||||
check: func(c *Config) {
|
||||
if c.Port != DefaultPort {
|
||||
t.Errorf("Port = %d, want %d", c.Port, DefaultPort)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "negative port uses default",
|
||||
cfg: &Config{Port: -1},
|
||||
check: func(c *Config) {
|
||||
if c.Port != DefaultPort {
|
||||
t.Errorf("Port = %d, want %d", c.Port, DefaultPort)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "negative timeout becomes zero",
|
||||
cfg: &Config{ReadTimeout: -5, WriteTimeout: -10},
|
||||
check: func(c *Config) {
|
||||
if c.ReadTimeout != 0 {
|
||||
t.Errorf("ReadTimeout = %d, want 0", c.ReadTimeout)
|
||||
}
|
||||
if c.WriteTimeout != 0 {
|
||||
t.Errorf("WriteTimeout = %d, want 0", c.WriteTimeout)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "zero max_clients uses default",
|
||||
cfg: &Config{MaxClients: 0},
|
||||
check: func(c *Config) {
|
||||
if c.MaxClients != DefaultMaxClients {
|
||||
t.Errorf("MaxClients = %d, want %d", c.MaxClients, DefaultMaxClients)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "zero max_message_bytes uses default",
|
||||
cfg: &Config{MaxMsgBytes: 0},
|
||||
check: func(c *Config) {
|
||||
if c.MaxMsgBytes != DefaultMaxMsgBytes {
|
||||
t.Errorf("MaxMsgBytes = %d, want %d", c.MaxMsgBytes, DefaultMaxMsgBytes)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty log_level uses default",
|
||||
cfg: &Config{LogLevel: ""},
|
||||
check: func(c *Config) {
|
||||
if c.LogLevel != DefaultLogLevel {
|
||||
t.Errorf("LogLevel = %q, want %q", c.LogLevel, DefaultLogLevel)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid values are preserved",
|
||||
cfg: &Config{
|
||||
Bind: "127.0.0.1",
|
||||
Port: 6380,
|
||||
MaxClients: 500,
|
||||
ReadTimeout: 30,
|
||||
WriteTimeout: 60,
|
||||
MaxMsgBytes: 1024,
|
||||
LogLevel: "debug",
|
||||
},
|
||||
check: func(c *Config) {
|
||||
if c.Bind != "127.0.0.1" {
|
||||
t.Errorf("Bind = %q, want 127.0.0.1", c.Bind)
|
||||
}
|
||||
if c.Port != 6380 {
|
||||
t.Errorf("Port = %d, want 6380", c.Port)
|
||||
}
|
||||
if c.MaxClients != 500 {
|
||||
t.Errorf("MaxClients = %d, want 500", c.MaxClients)
|
||||
}
|
||||
if c.ReadTimeout != 30 {
|
||||
t.Errorf("ReadTimeout = %d, want 30", c.ReadTimeout)
|
||||
}
|
||||
if c.WriteTimeout != 60 {
|
||||
t.Errorf("WriteTimeout = %d, want 60", c.WriteTimeout)
|
||||
}
|
||||
if c.MaxMsgBytes != 1024 {
|
||||
t.Errorf("MaxMsgBytes = %d, want 1024", c.MaxMsgBytes)
|
||||
}
|
||||
if c.LogLevel != "debug" {
|
||||
t.Errorf("LogLevel = %q, want debug", c.LogLevel)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.cfg.SetDefaults()
|
||||
tt.check(tt.cfg)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg *Config
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "valid config passes",
|
||||
cfg: &Config{Bind: "0.0.0.0", Port: 6379, MaxClients: 100, MaxMsgBytes: 1024, LogLevel: "info"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid IPv6 bind passes",
|
||||
cfg: &Config{Bind: "::", Port: 6379, MaxClients: 100, MaxMsgBytes: 1024, LogLevel: "info"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid IPv6 loopback passes",
|
||||
cfg: &Config{Bind: "::1", Port: 6379, MaxClients: 100, MaxMsgBytes: 1024, LogLevel: "info"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "empty bind fails",
|
||||
cfg: &Config{Bind: "", Port: 6379, MaxClients: 100, MaxMsgBytes: 1024, LogLevel: "info"},
|
||||
wantErr: true,
|
||||
errMsg: "bind address cannot be empty",
|
||||
},
|
||||
{
|
||||
name: "invalid bind fails",
|
||||
cfg: &Config{Bind: "not-an-ip", Port: 6379, MaxClients: 100, MaxMsgBytes: 1024, LogLevel: "info"},
|
||||
wantErr: true,
|
||||
errMsg: "bind must be a valid IP address (e.g. 0.0.0.0, ::, ::1, 192.168.1.1)",
|
||||
},
|
||||
{
|
||||
name: "port zero fails",
|
||||
cfg: &Config{Bind: "0.0.0.0", Port: 0, MaxClients: 100, MaxMsgBytes: 1024, LogLevel: "info"},
|
||||
wantErr: true,
|
||||
errMsg: "port must be between 1 and 65535",
|
||||
},
|
||||
{
|
||||
name: "port out of range fails",
|
||||
cfg: &Config{Bind: "0.0.0.0", Port: 70000, MaxClients: 100, MaxMsgBytes: 1024, LogLevel: "info"},
|
||||
wantErr: true,
|
||||
errMsg: "port must be between 1 and 65535",
|
||||
},
|
||||
{
|
||||
name: "negative port fails",
|
||||
cfg: &Config{Bind: "0.0.0.0", Port: -1, MaxClients: 100, MaxMsgBytes: 1024, LogLevel: "info"},
|
||||
wantErr: true,
|
||||
errMsg: "port must be between 1 and 65535",
|
||||
},
|
||||
{
|
||||
name: "zero max_clients fails",
|
||||
cfg: &Config{Bind: "0.0.0.0", Port: 6379, MaxClients: 0, MaxMsgBytes: 1024, LogLevel: "info"},
|
||||
wantErr: true,
|
||||
errMsg: "max_clients must be positive",
|
||||
},
|
||||
{
|
||||
name: "negative max_clients fails",
|
||||
cfg: &Config{Bind: "0.0.0.0", Port: 6379, MaxClients: -5, MaxMsgBytes: 1024, LogLevel: "info"},
|
||||
wantErr: true,
|
||||
errMsg: "max_clients must be positive",
|
||||
},
|
||||
{
|
||||
name: "zero max_message_bytes fails",
|
||||
cfg: &Config{Bind: "0.0.0.0", Port: 6379, MaxClients: 100, MaxMsgBytes: 0, LogLevel: "info"},
|
||||
wantErr: true,
|
||||
errMsg: "max_message_bytes must be positive",
|
||||
},
|
||||
{
|
||||
name: "invalid log_level fails",
|
||||
cfg: &Config{Bind: "0.0.0.0", Port: 6379, MaxClients: 100, MaxMsgBytes: 1024, LogLevel: "invalid"},
|
||||
wantErr: true,
|
||||
errMsg: "invalid log_level: invalid (must be debug, info, warn, or error)",
|
||||
},
|
||||
{
|
||||
name: "valid log levels pass",
|
||||
cfg: &Config{Bind: "0.0.0.0", Port: 6379, MaxClients: 100, MaxMsgBytes: 1024, LogLevel: "debug"},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.cfg.Validate()
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Errorf("Validate() expected error, got nil")
|
||||
return
|
||||
}
|
||||
if err.Error() != tt.errMsg {
|
||||
t.Errorf("Validate() error = %q, want %q", err.Error(), tt.errMsg)
|
||||
}
|
||||
} else if err != nil {
|
||||
t.Errorf("Validate() unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// nolint: cyclop
|
||||
func TestLoad(t *testing.T) {
|
||||
tmpfile, err := os.CreateTemp("", "config*.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() { _ = os.Remove(tmpfile.Name()) }()
|
||||
|
||||
content := `{
|
||||
"bind": "127.0.0.1",
|
||||
"port": 6380,
|
||||
"max_clients": 500,
|
||||
"read_timeout": 30,
|
||||
"write_timeout": 60,
|
||||
"max_message_bytes": 1048576,
|
||||
"log_level": "warn"
|
||||
}`
|
||||
if _, err := tmpfile.WriteString(content); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := tmpfile.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cfg, err := Load(tmpfile.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("Load() error: %v", err)
|
||||
}
|
||||
|
||||
if cfg.Bind != "127.0.0.1" {
|
||||
t.Errorf("Bind = %q, want 127.0.0.1", cfg.Bind)
|
||||
}
|
||||
if cfg.Port != 6380 {
|
||||
t.Errorf("Port = %d, want 6380", cfg.Port)
|
||||
}
|
||||
if cfg.MaxClients != 500 {
|
||||
t.Errorf("MaxClients = %d, want 500", cfg.MaxClients)
|
||||
}
|
||||
if cfg.ReadTimeout != 30 {
|
||||
t.Errorf("ReadTimeout = %d, want 30", cfg.ReadTimeout)
|
||||
}
|
||||
if cfg.WriteTimeout != 60 {
|
||||
t.Errorf("WriteTimeout = %d, want 60", cfg.WriteTimeout)
|
||||
}
|
||||
if cfg.MaxMsgBytes != 1048576 {
|
||||
t.Errorf("MaxMsgBytes = %d, want 1048576", cfg.MaxMsgBytes)
|
||||
}
|
||||
if cfg.LogLevel != "warn" {
|
||||
t.Errorf("LogLevel = %q, want warn", cfg.LogLevel)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadInvalidFile(t *testing.T) {
|
||||
_, err := Load("nonexistent.json")
|
||||
if err == nil {
|
||||
t.Error("Load() expected error for nonexistent file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadInvalidJSON(t *testing.T) {
|
||||
tmpfile, err := os.CreateTemp("", "config*.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() { _ = os.Remove(tmpfile.Name()) }()
|
||||
|
||||
if _, err := tmpfile.WriteString("not valid json"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := tmpfile.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = Load(tmpfile.Name())
|
||||
if err == nil {
|
||||
t.Error("Load() expected error for invalid JSON")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadAppliesDefaults(t *testing.T) {
|
||||
tmpfile, err := os.CreateTemp("", "config*.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() { _ = os.Remove(tmpfile.Name()) }()
|
||||
|
||||
content := `{"bind": "127.0.0.1", "port": 6379}`
|
||||
if _, err := tmpfile.WriteString(content); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := tmpfile.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cfg, err := Load(tmpfile.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("Load() error: %v", err)
|
||||
}
|
||||
|
||||
if cfg.MaxClients != DefaultMaxClients {
|
||||
t.Errorf("MaxClients = %d, want default %d", cfg.MaxClients, DefaultMaxClients)
|
||||
}
|
||||
if cfg.LogLevel != DefaultLogLevel {
|
||||
t.Errorf("LogLevel = %q, want default %q", cfg.LogLevel, DefaultLogLevel)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddr(t *testing.T) {
|
||||
tests := []struct {
|
||||
cfg *Config
|
||||
want string
|
||||
}{
|
||||
// IPv4
|
||||
{&Config{Bind: "0.0.0.0", Port: 6379}, "0.0.0.0:6379"},
|
||||
{&Config{Bind: "127.0.0.1", Port: 6380}, "127.0.0.1:6380"},
|
||||
{&Config{Bind: "192.168.1.100", Port: 8080}, "192.168.1.100:8080"},
|
||||
// IPv6 — must be wrapped in brackets
|
||||
{&Config{Bind: "::", Port: 6379}, "[::]:6379"},
|
||||
{&Config{Bind: "::1", Port: 6379}, "[::1]:6379"},
|
||||
{&Config{Bind: "2001:db8::1", Port: 8080}, "[2001:db8::1]:8080"},
|
||||
{&Config{Bind: "fe80::1", Port: 6379}, "[fe80::1]:6379"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := tt.cfg.Addr()
|
||||
if got != tt.want {
|
||||
t.Errorf("Addr(bind=%q, port=%d) = %q, want %q", tt.cfg.Bind, tt.cfg.Port, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadTimeoutDuration(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg *Config
|
||||
expected string
|
||||
}{
|
||||
{"zero returns zero duration", &Config{ReadTimeout: 0}, "0s"},
|
||||
{"30 seconds", &Config{ReadTimeout: 30}, "30s"},
|
||||
{"120 seconds", &Config{ReadTimeout: 120}, "2m0s"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := tt.cfg.ReadTimeoutDuration().String()
|
||||
if got != tt.expected {
|
||||
t.Errorf("ReadTimeoutDuration() = %s, want %s", got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteTimeoutDuration(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg *Config
|
||||
expected string
|
||||
}{
|
||||
{"zero returns zero duration", &Config{WriteTimeout: 0}, "0s"},
|
||||
{"60 seconds", &Config{WriteTimeout: 60}, "1m0s"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := tt.cfg.WriteTimeoutDuration().String()
|
||||
if got != tt.expected {
|
||||
t.Errorf("WriteTimeoutDuration() = %s, want %s", got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestString(t *testing.T) {
|
||||
cfg := &Config{
|
||||
Bind: "0.0.0.0",
|
||||
Port: 6379,
|
||||
MaxClients: 10000,
|
||||
ReadTimeout: 0,
|
||||
WriteTimeout: 0,
|
||||
MaxMsgBytes: 536870912,
|
||||
LogLevel: "info",
|
||||
}
|
||||
|
||||
str := cfg.String()
|
||||
if len(str) == 0 {
|
||||
t.Error("String() returned empty string")
|
||||
}
|
||||
}
|
||||
24
internal/config/configpath_bsd.go
Normal file
24
internal/config/configpath_bsd.go
Normal file
@@ -0,0 +1,24 @@
|
||||
//go:build darwin || freebsd || openbsd || netbsd || dragonfly
|
||||
|
||||
// @tag config
|
||||
// @tag platform:bsd
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// DefaultConfigPaths returns the config file search paths for BSD and macOS.
|
||||
// Looks up ~/.pekoe.json first, then /usr/local/etc/pekoe/config.json.
|
||||
func DefaultConfigPaths() []string {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return []string{"/usr/local/etc/pekoe/config.json"}
|
||||
}
|
||||
|
||||
return []string{
|
||||
filepath.Join(home, ".pekoe.json"),
|
||||
"/usr/local/etc/pekoe/config.json",
|
||||
}
|
||||
}
|
||||
29
internal/config/configpath_linux.go
Normal file
29
internal/config/configpath_linux.go
Normal file
@@ -0,0 +1,29 @@
|
||||
//go:build linux
|
||||
|
||||
// @tag config
|
||||
// @tag platform:linux
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// DefaultConfigPaths returns the config file search paths for Linux.
|
||||
// Root user: only /etc/pekoe/config.json
|
||||
// Regular user: ~/.pekoe.json, then /etc/pekoe/config.json
|
||||
func DefaultConfigPaths() []string {
|
||||
if os.Getuid() == 0 {
|
||||
return []string{"/etc/pekoe/config.json"}
|
||||
}
|
||||
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return []string{"/etc/pekoe/config.json"}
|
||||
}
|
||||
|
||||
return []string{
|
||||
filepath.Join(home, ".pekoe.json"),
|
||||
"/etc/pekoe/config.json",
|
||||
}
|
||||
}
|
||||
11
internal/config/configpath_windows.go
Normal file
11
internal/config/configpath_windows.go
Normal file
@@ -0,0 +1,11 @@
|
||||
//go:build windows
|
||||
|
||||
// @tag config
|
||||
// @tag platform:windows
|
||||
package config
|
||||
|
||||
// DefaultConfigPaths returns no default paths for Windows.
|
||||
// On Windows the --config flag is required.
|
||||
func DefaultConfigPaths() []string {
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user