From cd1bfc663e74d272027e66acb207708a3493c54d Mon Sep 17 00:00:00 2001 From: vinitparekh17 Date: Fri, 15 Nov 2024 13:02:37 +0530 Subject: [PATCH] feat: test cases of new config management --- config/parser.go | 72 +++---- integration_tests/config/config_test.go | 232 ++++++++++------------- integration_tests/config/parser_test.go | 237 ++++++++++++++++++++++++ 3 files changed, 370 insertions(+), 171 deletions(-) create mode 100644 integration_tests/config/parser_test.go diff --git a/config/parser.go b/config/parser.go index 84e141990..082c589f6 100644 --- a/config/parser.go +++ b/config/parser.go @@ -35,25 +35,25 @@ func (p *ConfigParser) ParseFromFile(filename string) error { return processConfigData(scanner, p) } -// processConfigData reads the configuration data line by line and stores it in the ConfigParser -func processConfigData(scanner *bufio.Scanner, p *ConfigParser) error { - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) - if line == "" || strings.HasPrefix(line, "#") { - continue - } +// ParseFromStdin reads the configuration data from stdin +func (p *ConfigParser) ParseFromStdin() error { + scanner := bufio.NewScanner(os.Stdin) + return processConfigData(scanner, p) +} - parts := strings.SplitN(line, "=", 2) - if len(parts) != 2 { - continue - } +// ParseDefaults populates a struct with default values based on struct tag `default` +func (p *ConfigParser) ParseDefaults(cfg interface{}) error { + val := reflect.ValueOf(cfg) + if val.Kind() != reflect.Ptr || val.IsNil() { + return fmt.Errorf("config must be a non-nil pointer to a struct") + } - key := strings.TrimSpace(parts[0]) - value := strings.Trim(strings.TrimSpace(parts[1]), "\"") - p.store[key] = value + val = val.Elem() + if val.Kind() != reflect.Struct { + return fmt.Errorf("config must be a pointer to a struct") } - return scanner.Err() + return p.unmarshalStruct(val, "") } // Loadconfig populates a struct with configuration values based on struct tags @@ -79,6 +79,27 @@ func (p *ConfigParser) Loadconfig(cfg interface{}) error { return nil } +// processConfigData reads the configuration data line by line and stores it in the ConfigParser +func processConfigData(scanner *bufio.Scanner, p *ConfigParser) error { + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + + parts := strings.SplitN(line, "=", 2) + if len(parts) != 2 { + return fmt.Errorf("invalid config line: %s", line) + } + + key := strings.TrimSpace(parts[0]) + value := strings.Trim(strings.TrimSpace(parts[1]), "\"") + p.store[key] = value + } + + return scanner.Err() +} + // unmarshalStruct handles the recursive struct parsing. func (p *ConfigParser) unmarshalStruct(val reflect.Value, prefix string) error { typ := val.Type() @@ -203,24 +224,3 @@ func setField(field reflect.Value, value string) error { return nil } - -// perse from stdin reads the configuration data from stdin -func (p *ConfigParser) ParseFromStdin() error { - scanner := bufio.NewScanner(os.Stdin) - return processConfigData(scanner, p) -} - -// ParseDefaults populates a struct with default values, will be used in case the config file is missing -func (p *ConfigParser) ParseDefaults(cfg interface{}) error { - val := reflect.ValueOf(cfg) - if val.Kind() != reflect.Ptr || val.IsNil() { - return fmt.Errorf("config must be a non-nil pointer to a struct") - } - - val = val.Elem() - if val.Kind() != reflect.Struct { - return fmt.Errorf("config must be a pointer to a struct") - } - - return p.unmarshalStruct(val, "") -} diff --git a/integration_tests/config/config_test.go b/integration_tests/config/config_test.go index a592ae81b..c68dae6d0 100644 --- a/integration_tests/config/config_test.go +++ b/integration_tests/config/config_test.go @@ -1,136 +1,98 @@ -package commands - -import "testing" - -// import ( -// "os" -// "path/filepath" -// "testing" - -// "github.com/dicedb/dice/config" -// ) - -// // scenario 1: Create a config file if the directory is provided (-o flag) -// func TestSetupConfig_CreateAndLoadDefault(t *testing.T) { -// config.ResetConfig() -// tempDir := t.TempDir() - -// // Simulate the flag: -o= -// config.CustomConfigFilePath = tempDir -// config.SetupConfig() - -// if config.DiceConfig.AsyncServer.Addr != config.DefaultHost { -// t.Fatalf("Expected server addr to be '%s', got '%s'", config.DefaultHost, config.DiceConfig.AsyncServer.Addr) -// } -// if config.DiceConfig.AsyncServer.Port != config.DefaultPort { -// t.Fatalf("Expected server port to be %d, got %d", config.DefaultPort, config.DiceConfig.AsyncServer.Port) -// } -// } - -// // scenario 2: Load default config if no config file or directory is provided -// func TestSetupConfig_DefaultConfig(t *testing.T) { -// // Simulate no flags being set (default config scenario) -// config.ResetConfig() -// config.CustomConfigFilePath = "" -// config.FileLocation = filepath.Join(config.DefaultConfigFilePath, config.DefaultConfigName) - -// // Verify that the configuration was loaded from the default values -// if config.DiceConfig.AsyncServer.Addr != config.DefaultHost { -// t.Fatalf("Expected server addr to be '%s', got '%s'", config.DefaultHost, config.DiceConfig.AsyncServer.Addr) // 127.0.0.1 -// } -// if config.DiceConfig.AsyncServer.Port != config.DefaultPort { -// t.Fatalf("Expected server port to be %d, got %d", 8739, config.DiceConfig.AsyncServer.Port) -// } -// } - -// // scenario 3: Config file is present but not well-structured (Malformed) -// func TestSetupConfig_InvalidConfigFile(t *testing.T) { -// config.DiceConfig = nil -// tempDir := t.TempDir() -// configFilePath := filepath.Join(tempDir, "dice.toml") - -// content := ` -// [asyncserver] -// addr = 127.0.0.1 // Missing quotes around string value -// port = abc // Invalid integer -// ` -// if err := os.WriteFile(configFilePath, []byte(content), 0666); err != nil { -// t.Fatalf("Failed to create invalid test config file: %v", err) -// } - -// // Simulate the flag: -c= -// config.CustomConfigFilePath = "" -// config.FileLocation = configFilePath - -// config.SetupConfig() - -// if config.DiceConfig.AsyncServer.Addr != config.DefaultHost { -// t.Fatalf("Expected server addr to be '%s' after unmarshal error, got '%s'", config.DefaultHost, config.DiceConfig.AsyncServer.Addr) -// } -// if config.DiceConfig.AsyncServer.Port != config.DefaultPort { -// t.Fatalf("Expected server port to be %d after unmarshal error, got %d", config.DefaultPort, config.DiceConfig.AsyncServer.Port) -// } -// } - -// // scenario 4: Config file is present with partial content -// func TestSetupConfig_PartialConfigFile(t *testing.T) { -// tempDir := t.TempDir() -// configFilePath := filepath.Join(tempDir, "dice.toml") - -// content := ` -// [asyncserver] -// addr = "127.0.0.1" -// ` -// if err := os.WriteFile(configFilePath, []byte(content), 0666); err != nil { -// t.Fatalf("Failed to create partial test config file: %v", err) -// } - -// // Simulate the flag: -c= -// config.CustomConfigFilePath = "" -// config.FileLocation = configFilePath - -// config.SetupConfig() - -// t.Log(config.DiceConfig.AsyncServer.Port) - -// if config.DiceConfig.AsyncServer.Addr != "127.0.0.1" { -// t.Fatalf("Expected server addr to be '127.0.0.1', got '%s'", config.DiceConfig.AsyncServer.Addr) -// } -// if config.DiceConfig.AsyncServer.Port != config.DefaultPort { -// t.Fatalf("Expected server port to be %d (default), got %d", config.DefaultPort, config.DiceConfig.AsyncServer.Port) -// } -// } - -// // scenario 5: Load config from the provided file path -// func TestSetupConfig_LoadFromFile(t *testing.T) { -// config.ResetConfig() -// tempDir := t.TempDir() -// configFilePath := filepath.Join(tempDir, "dice.toml") - -// content := ` -// [asyncserver] -// addr = "127.0.0.1" -// port = 8739 -// ` -// if err := os.WriteFile(configFilePath, []byte(content), 0666); err != nil { -// t.Fatalf("Failed to write test config file: %v", err) -// } - -// // Simulate the flag: -c= -// config.CustomConfigFilePath = "" -// config.FileLocation = configFilePath - -// config.SetupConfig() - -// if config.DiceConfig.AsyncServer.Addr != "127.0.0.1" { -// t.Fatalf("Expected server addr to be '127.0.0.1', got '%s'", config.DiceConfig.AsyncServer.Addr) -// } -// if config.DiceConfig.AsyncServer.Port != 8739 { -// t.Fatalf("Expected server port to be 8374, got %d", config.DiceConfig.AsyncServer.Port) -// } - -// } - -func Test_demo(t *testing.T) { - t.Log("todo implement new tests") +package config_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/dicedb/dice/config" +) + +const configFileName = "dicedb.conf" + +// TestCreateConfigFile_FileExists tests the scenario when config file already exists +func TestCreateConfigFile_FileExists(t *testing.T) { + tempDir := t.TempDir() + configPath := filepath.Join(tempDir, configFileName) + + if err := os.WriteFile(configPath, []byte("test config"), 0644); err != nil { + t.Fatalf("Failed to create test config file: %v", err) + } + + config.CreateConfigFile(configPath) + + content, err := os.ReadFile(configPath) + if err != nil { + t.Fatalf("Failed to read config file: %v", err) + } + + if string(content) != "test config" { + t.Error("Config file content was modified when it should have been preserved") + } +} + +// TestCreateConfigFile_NewFile tests creating a new config file +func TestCreateConfigFile_NewFile(t *testing.T) { + tempDir := t.TempDir() + configPath := filepath.Join(tempDir, configFileName) + config.CreateConfigFile(configPath) + + if _, err := os.Stat(configPath); os.IsNotExist(err) { + t.Error("Config file was not created") + } + + content, err := os.ReadFile(configPath) + if err != nil { + t.Fatalf("Failed to read created config file: %v", err) + } + + if len(content) == 0 { + t.Error("Created config file is empty") + } +} + +// TestCreateConfigFile_InvalidPath tests creation with an invalid file path +func TestCreateConfigFile_InvalidPath(t *testing.T) { + configPath := "/nonexistent/directory/dicedb.conf" + config.CreateConfigFile(configPath) + + if _, err := os.Stat(configPath); !os.IsNotExist(err) { + t.Error("Config file should not have been created at invalid path") + } +} + +func TestCreateConfigFile_NoPermission(t *testing.T) { + if os.Getuid() == 0 { + t.Skip("Skipping test when running as root") + } + + tempDir := t.TempDir() + err := os.Chmod(tempDir, 0555) // read + execute only + if err != nil { + t.Fatalf("Failed to change directory permissions: %v", err) + } + defer os.Chmod(tempDir, 0755) // restore permissions + + configPath := filepath.Join(tempDir, configFileName) + config.CreateConfigFile(configPath) + + if _, err := os.Stat(configPath); !os.IsNotExist(err) { + t.Error("Config file should not have been created without permissions") + } +} + +// TestCreateConfigFile_ExistingDirectory tests creation in existing directory +func TestCreateConfigFile_ExistingDirectory(t *testing.T) { + tempDir := t.TempDir() + configDir := filepath.Join(tempDir, "config") + if err := os.MkdirAll(configDir, 0755); err != nil { + t.Fatalf("Failed to create config directory: %v", err) + } + + configPath := filepath.Join(configDir, configFileName) + config.CreateConfigFile(configPath) + + if _, err := os.Stat(configPath); os.IsNotExist(err) { + t.Error("Config file was not created in existing directory") + } } diff --git a/integration_tests/config/parser_test.go b/integration_tests/config/parser_test.go new file mode 100644 index 000000000..60bf6ab96 --- /dev/null +++ b/integration_tests/config/parser_test.go @@ -0,0 +1,237 @@ +package config_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/dicedb/dice/config" +) + +// TestConfig is a test struct that mimics your actual config structure +type TestConfig struct { + Host string `default:"localhost"` + Port int `default:"8080"` + LogLevel string `default:"info"` +} + +func TestNewConfigParser(t *testing.T) { + parser := config.NewConfigParser() + if parser == nil { + t.Fatal("NewConfigParser returned nil") + } +} + +func TestParseFromFile(t *testing.T) { + tests := []struct { + name string + content string + wantErr bool + setupErr bool + }{ + { + name: "valid config", + content: `host=testhost +port=9090 +log_level=debug`, + wantErr: false, + }, + { + name: "empty file", + content: "", + wantErr: false, + }, + { + name: "malformed config", + content: `host=testhost +invalid-line +port=9090`, + wantErr: true, + }, + { + name: "non-existent file", + setupErr: true, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + parser := config.NewConfigParser() + + // Create temporary config file + tempDir := t.TempDir() + filename := filepath.Join(tempDir, "dicedb.conf") + + if !tt.setupErr { + err := os.WriteFile(filename, []byte(tt.content), 0644) + if err != nil { + t.Fatalf("Failed to create test file: %v", err) + } + } + + err := parser.ParseFromFile(filename) + if (err != nil) != tt.wantErr { + t.Errorf("ParseFromFile() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestParseFromStdin(t *testing.T) { + tests := []struct { + name string + input string + wantErr bool + }{ + { + name: "valid input", + input: `host=testhost +port=9090 +log_level=debug`, + wantErr: false, + }, + { + name: "empty input", + input: "", + wantErr: false, + }, + { + name: "malformed input", + input: `host=testhost +invalid-line +port=9090`, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + parser := config.NewConfigParser() + + // Store original stdin + oldStdin := os.Stdin + defer func() { os.Stdin = oldStdin }() + + // Create a pipe and pass the test input + r, w, err := os.Pipe() + if err != nil { + t.Fatalf("Failed to create pipe: %v", err) + } + os.Stdin = r + + go func() { + defer w.Close() + w.Write([]byte(tt.input)) + }() + + err = parser.ParseFromStdin() + if (err != nil) != tt.wantErr { + t.Errorf("ParseFromStdin() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestParseDefaults(t *testing.T) { + tests := []struct { + name string + cfg interface{} + wantErr bool + }{ + { + name: "valid struct", + cfg: &TestConfig{}, + wantErr: false, + }, + { + name: "nil pointer", + cfg: nil, + wantErr: true, + }, + { + name: "non-pointer", + cfg: TestConfig{}, + wantErr: true, + }, + { + name: "pointer to non-struct", + cfg: new(string), + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + parser := config.NewConfigParser() + err := parser.ParseDefaults(tt.cfg) + + if (err != nil) != tt.wantErr { + t.Errorf("ParseDefaults() error = %v, wantErr %v", err, tt.wantErr) + } + + if !tt.wantErr && tt.cfg != nil { + cfg := tt.cfg.(*TestConfig) + if cfg.Host != "localhost" || cfg.Port != 8080 || cfg.LogLevel != "info" { + t.Error("Default values were not properly set") + } + } + }) + } +} + +// TestLoadconfig tests the Loadconfig method +func TestLoadconfig(t *testing.T) { + tests := []struct { + name string + cfg interface{} + content string + wantErr bool + }{ + { + name: "nil pointer", + cfg: nil, + content: "", + wantErr: true, + }, + { + name: "non-pointer", + cfg: TestConfig{}, + content: "", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + parser := config.NewConfigParser() + + // Create and populate config file if content is provided + if tt.content != "" { + tempDir := t.TempDir() + filename := filepath.Join(tempDir, "dicedb.conf") + err := os.WriteFile(filename, []byte(tt.content), 0644) + if err != nil { + t.Fatalf("Failed to create test file: %v", err) + } + + err = parser.ParseFromFile(filename) + if err != nil { + t.Fatalf("Failed to parse test file: %v", err) + } + } + + err := parser.Loadconfig(tt.cfg) + if (err != nil) != tt.wantErr { + t.Errorf("Loadconfig() error = %v, wantErr %v", err, tt.wantErr) + } + + if !tt.wantErr && tt.cfg != nil { + cfg := tt.cfg.(*TestConfig) + if tt.content != "" && (cfg.Host != "customhost" || cfg.Port != 9090 || cfg.LogLevel != "debug") { + t.Error("Config values were not properly loaded") + } + } + }) + } +}