mirror of https://github.com/Wox-launcher/Wox
feat(test): Implement robust loading strategies for Wox settings
- Added `loadWoxSettingWithFallback` to handle various corrupted JSON scenarios. - Introduced multiple strategies for loading settings, including normal decoding, JSON repair, and partial loading. - Enhanced error handling to log issues and default to safe settings when loading fails. - Created comprehensive tests for loading settings with various corrupted JSON files.
This commit is contained in:
parent
d423296d73
commit
c1a87df75b
|
@ -6,8 +6,10 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"wox/common"
|
||||
"wox/i18n"
|
||||
|
@ -79,6 +81,7 @@ func (m *Manager) loadWoxSetting(ctx context.Context) error {
|
|||
|
||||
woxSettingPath := util.GetLocation().GetWoxSettingPath()
|
||||
if _, statErr := os.Stat(woxSettingPath); os.IsNotExist(statErr) {
|
||||
// Create default setting file if not exists
|
||||
defaultWoxSettingJson, marshalErr := json.Marshal(defaultWoxSetting)
|
||||
if marshalErr != nil {
|
||||
return marshalErr
|
||||
|
@ -88,44 +91,20 @@ func (m *Manager) loadWoxSetting(ctx context.Context) error {
|
|||
if writeErr != nil {
|
||||
return writeErr
|
||||
}
|
||||
m.woxSetting = &defaultWoxSetting
|
||||
return nil
|
||||
}
|
||||
|
||||
woxSettingFile, openErr := os.Open(woxSettingPath)
|
||||
if openErr != nil {
|
||||
return openErr
|
||||
}
|
||||
defer woxSettingFile.Close()
|
||||
|
||||
woxSetting := &WoxSetting{}
|
||||
decodeErr := json.NewDecoder(woxSettingFile).Decode(woxSetting)
|
||||
if decodeErr != nil {
|
||||
return decodeErr
|
||||
}
|
||||
// some settings were added later, json file may not have them, so we need to set them to default value
|
||||
if woxSetting.MainHotkey.Get() == "" {
|
||||
woxSetting.MainHotkey.Set(defaultWoxSetting.MainHotkey.Get())
|
||||
}
|
||||
if woxSetting.SelectionHotkey.Get() == "" {
|
||||
woxSetting.SelectionHotkey.Set(defaultWoxSetting.SelectionHotkey.Get())
|
||||
}
|
||||
if woxSetting.LangCode == "" {
|
||||
woxSetting.LangCode = defaultWoxSetting.LangCode
|
||||
}
|
||||
if woxSetting.LastQueryMode == "" {
|
||||
woxSetting.LastQueryMode = defaultWoxSetting.LastQueryMode
|
||||
}
|
||||
if woxSetting.AppWidth == 0 {
|
||||
woxSetting.AppWidth = defaultWoxSetting.AppWidth
|
||||
}
|
||||
if woxSetting.MaxResultCount == 0 {
|
||||
woxSetting.MaxResultCount = defaultWoxSetting.MaxResultCount
|
||||
}
|
||||
if woxSetting.ThemeId == "" {
|
||||
woxSetting.ThemeId = defaultWoxSetting.ThemeId
|
||||
// Try to load setting with maximum tolerance for errors
|
||||
woxSetting, err := m.loadWoxSettingWithFallback(ctx, woxSettingPath, defaultWoxSetting)
|
||||
if err != nil {
|
||||
// If all attempts fail, log error and use defaults
|
||||
logger.Error(ctx, fmt.Sprintf("Failed to load setting file, using defaults: %v", err))
|
||||
m.woxSetting = &defaultWoxSetting
|
||||
return nil // Don't return error, just use defaults
|
||||
}
|
||||
|
||||
m.woxSetting = woxSetting
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -366,6 +345,272 @@ func (m *Manager) LoadPluginSetting(ctx context.Context, pluginId string, plugin
|
|||
return pluginSetting, nil
|
||||
}
|
||||
|
||||
// loadWoxSettingWithFallback attempts to load setting with multiple fallback strategies
|
||||
func (m *Manager) loadWoxSettingWithFallback(ctx context.Context, settingPath string, defaultSetting WoxSetting) (*WoxSetting, error) {
|
||||
// Strategy 1: Try normal JSON decoding
|
||||
if setting, err := m.tryLoadWoxSetting(settingPath, defaultSetting); err == nil {
|
||||
logger.Info(ctx, "Successfully loaded setting with normal JSON decoding")
|
||||
return setting, nil
|
||||
} else {
|
||||
logger.Warn(ctx, fmt.Sprintf("Normal JSON decoding failed: %v", err))
|
||||
}
|
||||
|
||||
// Strategy 2: Try to fix common JSON issues and reload
|
||||
if setting, err := m.tryLoadWithJSONRepair(settingPath, defaultSetting); err == nil {
|
||||
logger.Info(ctx, "Successfully loaded setting after JSON repair")
|
||||
return setting, nil
|
||||
} else {
|
||||
logger.Warn(ctx, fmt.Sprintf("JSON repair failed: %v", err))
|
||||
}
|
||||
|
||||
// Strategy 3: Try field-by-field parsing to salvage what we can
|
||||
if setting, err := m.tryPartialLoad(settingPath, defaultSetting); err == nil {
|
||||
logger.Info(ctx, "Successfully loaded setting with partial parsing")
|
||||
return setting, nil
|
||||
} else {
|
||||
logger.Warn(ctx, fmt.Sprintf("Partial parsing failed: %v", err))
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("all loading strategies failed")
|
||||
}
|
||||
|
||||
// tryLoadWoxSetting attempts normal JSON decoding
|
||||
func (m *Manager) tryLoadWoxSetting(settingPath string, defaultSetting WoxSetting) (*WoxSetting, error) {
|
||||
fileContent, err := os.ReadFile(settingPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read file: %w", err)
|
||||
}
|
||||
|
||||
// Check for empty file
|
||||
if len(fileContent) == 0 {
|
||||
return nil, fmt.Errorf("file is empty")
|
||||
}
|
||||
|
||||
woxSetting := &WoxSetting{}
|
||||
if err := json.Unmarshal(fileContent, woxSetting); err != nil {
|
||||
return nil, fmt.Errorf("JSON decode error: %w", err)
|
||||
}
|
||||
|
||||
// Apply defaults and sanitize values
|
||||
m.applyDefaultsToWoxSetting(woxSetting, defaultSetting)
|
||||
m.sanitizeWoxSetting(woxSetting, defaultSetting)
|
||||
|
||||
return woxSetting, nil
|
||||
}
|
||||
|
||||
// tryLoadWithJSONRepair attempts to fix common JSON syntax issues
|
||||
func (m *Manager) tryLoadWithJSONRepair(settingPath string, defaultSetting WoxSetting) (*WoxSetting, error) {
|
||||
fileContent, err := os.ReadFile(settingPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read file: %w", err)
|
||||
}
|
||||
|
||||
// Try to repair common JSON issues
|
||||
repairedContent := m.repairJSONContent(fileContent)
|
||||
|
||||
woxSetting := &WoxSetting{}
|
||||
if err := json.Unmarshal(repairedContent, woxSetting); err != nil {
|
||||
return nil, fmt.Errorf("JSON decode error after repair: %w", err)
|
||||
}
|
||||
|
||||
// Apply defaults and sanitize values
|
||||
m.applyDefaultsToWoxSetting(woxSetting, defaultSetting)
|
||||
m.sanitizeWoxSetting(woxSetting, defaultSetting)
|
||||
|
||||
return woxSetting, nil
|
||||
}
|
||||
|
||||
// repairJSONContent attempts to fix common JSON syntax issues
|
||||
func (m *Manager) repairJSONContent(content []byte) []byte {
|
||||
contentStr := string(content)
|
||||
|
||||
// If completely empty, return empty object
|
||||
if len(contentStr) == 0 {
|
||||
return []byte("{}")
|
||||
}
|
||||
|
||||
// Remove trailing commas before } or ]
|
||||
contentStr = regexp.MustCompile(`,(\s*[}\]])`).ReplaceAllString(contentStr, "$1")
|
||||
|
||||
// Try to fix missing closing braces/brackets by counting
|
||||
openBraces := strings.Count(contentStr, "{")
|
||||
closeBraces := strings.Count(contentStr, "}")
|
||||
if openBraces > closeBraces {
|
||||
for i := 0; i < openBraces-closeBraces; i++ {
|
||||
contentStr += "}"
|
||||
}
|
||||
}
|
||||
|
||||
openBrackets := strings.Count(contentStr, "[")
|
||||
closeBrackets := strings.Count(contentStr, "]")
|
||||
if openBrackets > closeBrackets {
|
||||
for i := 0; i < openBrackets-closeBrackets; i++ {
|
||||
contentStr += "]"
|
||||
}
|
||||
}
|
||||
|
||||
return []byte(contentStr)
|
||||
}
|
||||
|
||||
// tryPartialLoad attempts to parse individual fields to salvage what we can
|
||||
func (m *Manager) tryPartialLoad(settingPath string, defaultSetting WoxSetting) (*WoxSetting, error) {
|
||||
fileContent, err := os.ReadFile(settingPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read file: %w", err)
|
||||
}
|
||||
|
||||
// Start with default setting
|
||||
woxSetting := defaultSetting
|
||||
|
||||
// Try to parse as a generic map to extract individual fields
|
||||
var rawData map[string]interface{}
|
||||
if err := json.Unmarshal(fileContent, &rawData); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse as map: %w", err)
|
||||
}
|
||||
|
||||
// Extract fields one by one with error tolerance
|
||||
m.extractFieldSafely(rawData, "UsePinYin", &woxSetting.UsePinYin)
|
||||
m.extractFieldSafely(rawData, "SwitchInputMethodABC", &woxSetting.SwitchInputMethodABC)
|
||||
m.extractFieldSafely(rawData, "HideOnStart", &woxSetting.HideOnStart)
|
||||
m.extractFieldSafely(rawData, "HideOnLostFocus", &woxSetting.HideOnLostFocus)
|
||||
m.extractFieldSafely(rawData, "ShowTray", &woxSetting.ShowTray)
|
||||
m.extractFieldSafely(rawData, "EnableAutoBackup", &woxSetting.EnableAutoBackup)
|
||||
m.extractFieldSafely(rawData, "EnableAutoUpdate", &woxSetting.EnableAutoUpdate)
|
||||
|
||||
// Extract numeric fields
|
||||
m.extractIntFieldSafely(rawData, "AppWidth", &woxSetting.AppWidth)
|
||||
m.extractIntFieldSafely(rawData, "MaxResultCount", &woxSetting.MaxResultCount)
|
||||
|
||||
// Extract string fields
|
||||
m.extractStringFieldSafely(rawData, "LangCode", (*string)(&woxSetting.LangCode))
|
||||
m.extractStringFieldSafely(rawData, "LastQueryMode", &woxSetting.LastQueryMode)
|
||||
m.extractStringFieldSafely(rawData, "ThemeId", &woxSetting.ThemeId)
|
||||
|
||||
// Sanitize the loaded values
|
||||
m.sanitizeWoxSetting(&woxSetting, defaultSetting)
|
||||
|
||||
return &woxSetting, nil
|
||||
}
|
||||
|
||||
// extractFieldSafely safely extracts a boolean field from raw JSON data
|
||||
func (m *Manager) extractFieldSafely(rawData map[string]interface{}, fieldName string, target *bool) {
|
||||
if value, exists := rawData[fieldName]; exists {
|
||||
if boolVal, ok := value.(bool); ok {
|
||||
*target = boolVal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// extractIntFieldSafely safely extracts an integer field from raw JSON data
|
||||
func (m *Manager) extractIntFieldSafely(rawData map[string]interface{}, fieldName string, target *int) {
|
||||
if value, exists := rawData[fieldName]; exists {
|
||||
switch v := value.(type) {
|
||||
case float64:
|
||||
*target = int(v)
|
||||
case int:
|
||||
*target = v
|
||||
case int64:
|
||||
*target = int(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// extractStringFieldSafely safely extracts a string field from raw JSON data
|
||||
func (m *Manager) extractStringFieldSafely(rawData map[string]interface{}, fieldName string, target *string) {
|
||||
if value, exists := rawData[fieldName]; exists {
|
||||
if strVal, ok := value.(string); ok {
|
||||
*target = strVal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sanitizeWoxSetting ensures all values are within acceptable ranges
|
||||
func (m *Manager) sanitizeWoxSetting(setting *WoxSetting, defaultSetting WoxSetting) {
|
||||
// Sanitize AppWidth
|
||||
if setting.AppWidth <= 0 || setting.AppWidth > 10000 {
|
||||
setting.AppWidth = defaultSetting.AppWidth
|
||||
}
|
||||
|
||||
// Sanitize MaxResultCount
|
||||
if setting.MaxResultCount <= 0 || setting.MaxResultCount > 1000 {
|
||||
setting.MaxResultCount = defaultSetting.MaxResultCount
|
||||
}
|
||||
|
||||
// Sanitize LangCode
|
||||
validLangCodes := []string{"en_US", "zh_CN", "zh_TW", "ja_JP", "ko_KR", "fr_FR", "de_DE", "es_ES", "pt_BR", "ru_RU"}
|
||||
isValidLang := false
|
||||
for _, code := range validLangCodes {
|
||||
if string(setting.LangCode) == code {
|
||||
isValidLang = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isValidLang {
|
||||
setting.LangCode = defaultSetting.LangCode
|
||||
}
|
||||
|
||||
// Sanitize LastQueryMode
|
||||
if setting.LastQueryMode != LastQueryModePreserve && setting.LastQueryMode != LastQueryModeEmpty {
|
||||
setting.LastQueryMode = defaultSetting.LastQueryMode
|
||||
}
|
||||
|
||||
// Sanitize ShowPosition
|
||||
validPositions := []PositionType{PositionTypeMouseScreen, PositionTypeActiveScreen, PositionTypeLastLocation}
|
||||
isValidPosition := false
|
||||
for _, pos := range validPositions {
|
||||
if setting.ShowPosition == pos {
|
||||
isValidPosition = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isValidPosition {
|
||||
setting.ShowPosition = defaultSetting.ShowPosition
|
||||
}
|
||||
}
|
||||
|
||||
// applyDefaultsToWoxSetting applies default values for missing or zero-value fields
|
||||
func (m *Manager) applyDefaultsToWoxSetting(setting *WoxSetting, defaultSetting WoxSetting) {
|
||||
// Apply defaults for hotkeys
|
||||
if setting.MainHotkey.Get() == "" {
|
||||
setting.MainHotkey.Set(defaultSetting.MainHotkey.Get())
|
||||
}
|
||||
if setting.SelectionHotkey.Get() == "" {
|
||||
setting.SelectionHotkey.Set(defaultSetting.SelectionHotkey.Get())
|
||||
}
|
||||
|
||||
// Apply defaults for string fields
|
||||
if setting.LangCode == "" {
|
||||
setting.LangCode = defaultSetting.LangCode
|
||||
}
|
||||
if setting.LastQueryMode == "" {
|
||||
setting.LastQueryMode = defaultSetting.LastQueryMode
|
||||
}
|
||||
if setting.ThemeId == "" {
|
||||
setting.ThemeId = defaultSetting.ThemeId
|
||||
}
|
||||
|
||||
// Apply defaults for numeric fields (only if zero)
|
||||
if setting.AppWidth == 0 {
|
||||
setting.AppWidth = defaultSetting.AppWidth
|
||||
}
|
||||
if setting.MaxResultCount == 0 {
|
||||
setting.MaxResultCount = defaultSetting.MaxResultCount
|
||||
}
|
||||
|
||||
// Apply defaults for position if empty
|
||||
if setting.ShowPosition == "" {
|
||||
setting.ShowPosition = defaultSetting.ShowPosition
|
||||
}
|
||||
|
||||
// Apply defaults for slices if nil
|
||||
if setting.QueryShortcuts == nil {
|
||||
setting.QueryShortcuts = defaultSetting.QueryShortcuts
|
||||
}
|
||||
if setting.AIProviders == nil {
|
||||
setting.AIProviders = defaultSetting.AIProviders
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) SavePluginSetting(ctx context.Context, pluginId string, pluginSetting *PluginSetting) error {
|
||||
pluginSettingPath := path.Join(util.GetLocation().GetPluginSettingDirectory(), fmt.Sprintf("%s.json", pluginId))
|
||||
pluginSettingJson, marshalErr := json.Marshal(pluginSetting)
|
||||
|
|
|
@ -0,0 +1,384 @@
|
|||
package test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"wox/setting"
|
||||
)
|
||||
|
||||
// ConfigBlackboxTestSuite tests configuration loading with various corrupted JSON files
|
||||
type ConfigBlackboxTestSuite struct {
|
||||
*TestSuite
|
||||
testDir string
|
||||
}
|
||||
|
||||
// NewConfigBlackboxTestSuite creates a new configuration blackbox test suite
|
||||
func NewConfigBlackboxTestSuite(t *testing.T) *ConfigBlackboxTestSuite {
|
||||
suite := &ConfigBlackboxTestSuite{
|
||||
TestSuite: NewTestSuite(t),
|
||||
}
|
||||
|
||||
// Create isolated test directory for config tests
|
||||
testConfig := GetCurrentTestConfig()
|
||||
suite.testDir = filepath.Join(testConfig.TestDataDirectory, "config_blackbox_test")
|
||||
err := os.MkdirAll(suite.testDir, 0755)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test directory: %v", err)
|
||||
}
|
||||
|
||||
return suite
|
||||
}
|
||||
|
||||
// TestConfigBlackbox runs all configuration blackbox tests
|
||||
func TestConfigBlackbox(t *testing.T) {
|
||||
suite := NewConfigBlackboxTestSuite(t)
|
||||
defer suite.cleanup()
|
||||
|
||||
// Test loadWoxSetting with various corrupted JSON files
|
||||
t.Run("LoadWoxSetting", func(t *testing.T) {
|
||||
suite.testLoadWoxSettingBlackbox(t)
|
||||
})
|
||||
|
||||
// TODO: Add loadWoxAppData tests later
|
||||
// t.Run("LoadWoxAppData", func(t *testing.T) {
|
||||
// suite.testLoadWoxAppDataBlackbox(t)
|
||||
// })
|
||||
}
|
||||
|
||||
// testLoadWoxSettingBlackbox tests loadWoxSetting method with various corrupted JSON scenarios
|
||||
func (suite *ConfigBlackboxTestSuite) testLoadWoxSettingBlackbox(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
jsonContent string
|
||||
expectError bool
|
||||
expectDefaults bool
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "EmptyFile",
|
||||
jsonContent: "",
|
||||
expectError: false,
|
||||
expectDefaults: true,
|
||||
description: "Empty configuration file should load with defaults",
|
||||
},
|
||||
{
|
||||
name: "InvalidJSON_MissingBrace",
|
||||
jsonContent: `{"MainHotkey": {"WinValue": "alt+space"`,
|
||||
expectError: false,
|
||||
expectDefaults: true,
|
||||
description: "Missing closing brace should be repaired and load with defaults",
|
||||
},
|
||||
{
|
||||
name: "InvalidJSON_ExtraComma",
|
||||
jsonContent: `{"MainHotkey": {"WinValue": "alt+space",}, "UsePinYin": true,}`,
|
||||
expectError: false,
|
||||
expectDefaults: true,
|
||||
description: "Extra trailing commas should be repaired and load successfully",
|
||||
},
|
||||
{
|
||||
name: "ValidJSON_MissingFields",
|
||||
jsonContent: `{"UsePinYin": true}`,
|
||||
expectError: false,
|
||||
expectDefaults: true,
|
||||
description: "Valid JSON with missing fields should load with defaults",
|
||||
},
|
||||
{
|
||||
name: "ValidJSON_NullValues",
|
||||
jsonContent: `{"MainHotkey": null, "UsePinYin": null, "QueryShortcuts": null}`,
|
||||
expectError: false,
|
||||
expectDefaults: true,
|
||||
description: "Null values should load with defaults",
|
||||
},
|
||||
{
|
||||
name: "ValidJSON_ExtraFields",
|
||||
jsonContent: `{"UsePinYin": true, "UnknownField": "value", "AnotherUnknown": 123}`,
|
||||
expectError: false,
|
||||
expectDefaults: true,
|
||||
description: "Extra unknown fields should be ignored",
|
||||
},
|
||||
{
|
||||
name: "ValidJSON_PartialData",
|
||||
jsonContent: `{"UsePinYin": true, "ShowTray": false, "AppWidth": 1200}`,
|
||||
expectError: false,
|
||||
expectDefaults: true,
|
||||
description: "Partial data should load with defaults for missing fields",
|
||||
},
|
||||
{
|
||||
name: "InvalidJSON_WrongDataTypes",
|
||||
jsonContent: `{"UsePinYin": "not_a_boolean", "AppWidth": "not_a_number"}`,
|
||||
expectError: false,
|
||||
expectDefaults: true,
|
||||
description: "Wrong data types should be ignored and load with defaults",
|
||||
},
|
||||
{
|
||||
name: "InvalidJSON_CorruptedNestedObject",
|
||||
jsonContent: `{"MainHotkey": {"WinValue": "alt+space", "MacValue": }, "UsePinYin": true}`,
|
||||
expectError: false,
|
||||
expectDefaults: true,
|
||||
description: "Corrupted nested object should be repaired and load with defaults",
|
||||
},
|
||||
{
|
||||
name: "InvalidJSON_CorruptedArray",
|
||||
jsonContent: `{"QueryShortcuts": [{"Shortcut": "test", "Query": "test"}, {"Shortcut": "test2",}], "UsePinYin": true}`,
|
||||
expectError: false,
|
||||
expectDefaults: true,
|
||||
description: "Corrupted array element should be repaired and load with defaults",
|
||||
},
|
||||
{
|
||||
name: "ValidJSON_EmptyArrays",
|
||||
jsonContent: `{"QueryShortcuts": [], "AIProviders": [], "UsePinYin": true}`,
|
||||
expectError: false,
|
||||
expectDefaults: true,
|
||||
description: "Empty arrays should load successfully",
|
||||
},
|
||||
{
|
||||
name: "InvalidJSON_VeryLargeNumbers",
|
||||
jsonContent: `{"AppWidth": 999999999999999999999999999999, "MaxResultCount": -999999999999999999999999999999}`,
|
||||
expectError: false,
|
||||
expectDefaults: true,
|
||||
description: "Numbers too large for int type should be sanitized to defaults",
|
||||
},
|
||||
{
|
||||
name: "ValidJSON_ComplexNestedStructures",
|
||||
jsonContent: `{"MainHotkey": {"WinValue": "ctrl+space", "MacValue": "cmd+space"}, "QueryShortcuts": [{"Shortcut": "g", "Query": "google {0}"}], "AIProviders": [{"Name": "openai", "ApiKey": "test", "Host": "api.openai.com"}]}`,
|
||||
expectError: false,
|
||||
expectDefaults: true,
|
||||
description: "Complex nested structures should load successfully",
|
||||
},
|
||||
{
|
||||
name: "InvalidJSON_CorruptedPlatformSettings",
|
||||
jsonContent: `{"MainHotkey": {"WinValue": "ctrl+space", "MacValue": }, "EnableAutostart": {"WinValue": "not_a_bool"}}`,
|
||||
expectError: false,
|
||||
expectDefaults: true,
|
||||
description: "Corrupted platform-specific settings should be repaired",
|
||||
},
|
||||
{
|
||||
name: "ValidJSON_InvalidLangCode",
|
||||
jsonContent: `{"LangCode": "invalid_lang", "UsePinYin": true}`,
|
||||
expectError: false,
|
||||
expectDefaults: true,
|
||||
description: "Invalid language code should be sanitized to default",
|
||||
},
|
||||
{
|
||||
name: "ValidJSON_InvalidShowPosition",
|
||||
jsonContent: `{"ShowPosition": "invalid_position", "AppWidth": 1000}`,
|
||||
expectError: false,
|
||||
expectDefaults: true,
|
||||
description: "Invalid show position should be sanitized to default",
|
||||
},
|
||||
{
|
||||
name: "ValidJSON_InvalidLastQueryMode",
|
||||
jsonContent: `{"LastQueryMode": "invalid_mode", "MaxResultCount": 15}`,
|
||||
expectError: false,
|
||||
expectDefaults: true,
|
||||
description: "Invalid last query mode should be sanitized to default",
|
||||
},
|
||||
{
|
||||
name: "InvalidJSON_MalformedArrayElements",
|
||||
jsonContent: `{"QueryShortcuts": [{"Shortcut": "g"}, {"Query": "test"}], "AIProviders": [{"Name": "openai"}, {"ApiKey": "test"}]}`,
|
||||
expectError: false,
|
||||
expectDefaults: true,
|
||||
description: "Malformed array elements should be handled gracefully",
|
||||
},
|
||||
{
|
||||
name: "ValidJSON_BoundaryValues",
|
||||
jsonContent: `{"AppWidth": 1, "MaxResultCount": 999, "UsePinYin": false}`,
|
||||
expectError: false,
|
||||
expectDefaults: true,
|
||||
description: "Boundary values should be accepted",
|
||||
},
|
||||
{
|
||||
name: "ValidJSON_NegativeNumbers",
|
||||
jsonContent: `{"AppWidth": -100, "MaxResultCount": -5}`,
|
||||
expectError: false,
|
||||
expectDefaults: true,
|
||||
description: "Negative numbers should be sanitized to defaults",
|
||||
},
|
||||
{
|
||||
name: "InvalidJSON_UnterminatedString",
|
||||
jsonContent: `{"ThemeId": "some-theme-id, "UsePinYin": true}`,
|
||||
expectError: false,
|
||||
expectDefaults: true,
|
||||
description: "Unterminated string should be repaired",
|
||||
},
|
||||
{
|
||||
name: "InvalidJSON_MissingQuotes",
|
||||
jsonContent: `{ThemeId: some-theme-id, UsePinYin: true}`,
|
||||
expectError: false,
|
||||
expectDefaults: true,
|
||||
description: "Missing quotes should be handled gracefully",
|
||||
},
|
||||
{
|
||||
name: "ValidJSON_UnicodeContent",
|
||||
jsonContent: `{"LangCode": "zh_CN", "QueryShortcuts": [{"Shortcut": "测试", "Query": "test 中文"}]}`,
|
||||
expectError: false,
|
||||
expectDefaults: true,
|
||||
description: "Unicode content should be handled correctly",
|
||||
},
|
||||
{
|
||||
name: "InvalidJSON_OnlyBraces",
|
||||
jsonContent: `{}`,
|
||||
expectError: false,
|
||||
expectDefaults: true,
|
||||
description: "Empty JSON object should load with all defaults",
|
||||
},
|
||||
{
|
||||
name: "InvalidJSON_OnlyWhitespace",
|
||||
jsonContent: " \n\t \r\n ",
|
||||
expectError: false,
|
||||
expectDefaults: true,
|
||||
description: "Whitespace-only content should be treated as empty",
|
||||
},
|
||||
{
|
||||
name: "InvalidJSON_BinaryData",
|
||||
jsonContent: string([]byte{0x00, 0x01, 0x02, 0xFF, 0xFE}),
|
||||
expectError: false,
|
||||
expectDefaults: true,
|
||||
description: "Binary data should be handled gracefully",
|
||||
},
|
||||
{
|
||||
name: "ValidJSON_ExtremelyLongString",
|
||||
jsonContent: `{"ThemeId": "` + strings.Repeat("a", 10000) + `", "UsePinYin": true}`,
|
||||
expectError: false,
|
||||
expectDefaults: true,
|
||||
description: "Extremely long strings should be handled",
|
||||
},
|
||||
{
|
||||
name: "InvalidJSON_NestedBracesOverflow",
|
||||
jsonContent: `{"MainHotkey": ` + strings.Repeat("{", 100) + `"test"` + strings.Repeat("}", 100) + `}`,
|
||||
expectError: false,
|
||||
expectDefaults: true,
|
||||
description: "Deeply nested structures should be handled",
|
||||
},
|
||||
{
|
||||
name: "ValidJSON_ZeroValues",
|
||||
jsonContent: `{"AppWidth": 0, "MaxResultCount": 0, "UsePinYin": false, "ShowTray": false}`,
|
||||
expectError: false,
|
||||
expectDefaults: true,
|
||||
description: "Zero values should be replaced with defaults",
|
||||
},
|
||||
{
|
||||
name: "InvalidJSON_SpecialCharacters",
|
||||
jsonContent: `{"ThemeId": "theme\n\t\r\"\\\/\b\f", "UsePinYin": true}`,
|
||||
expectError: false,
|
||||
expectDefaults: true,
|
||||
description: "Special characters should be handled correctly",
|
||||
},
|
||||
{
|
||||
name: "ValidJSON_FloatAsInt",
|
||||
jsonContent: `{"AppWidth": 800.5, "MaxResultCount": 10.9}`,
|
||||
expectError: false,
|
||||
expectDefaults: true,
|
||||
description: "Float values for int fields should be converted",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
suite.runLoadWoxSettingTest(t, tc.name, tc.jsonContent, tc.expectError, tc.expectDefaults, tc.description)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// runLoadWoxSettingTest runs a single loadWoxSetting blackbox test
|
||||
func (suite *ConfigBlackboxTestSuite) runLoadWoxSettingTest(t *testing.T, name, jsonContent string, expectError, expectDefaults bool, description string) {
|
||||
// Create a temporary setting file in the test location
|
||||
testLocation := GetTestLocation()
|
||||
if testLocation == nil {
|
||||
t.Fatalf("Test location not available")
|
||||
}
|
||||
|
||||
settingPath := testLocation.GetWoxSettingPath()
|
||||
|
||||
// Ensure the settings directory exists
|
||||
settingsDir := testLocation.GetPluginSettingDirectory()
|
||||
err := os.MkdirAll(settingsDir, 0755)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create settings directory: %v", err)
|
||||
}
|
||||
|
||||
// Write the test JSON content
|
||||
err = os.WriteFile(settingPath, []byte(jsonContent), 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to write test setting file: %v", err)
|
||||
}
|
||||
defer os.Remove(settingPath)
|
||||
|
||||
t.Logf("Testing: %s - %s", name, description)
|
||||
t.Logf("JSON content: %s", jsonContent)
|
||||
t.Logf("Setting path: %s", settingPath)
|
||||
|
||||
// Create a new manager and try to initialize it (which calls loadWoxSetting internally)
|
||||
manager := &setting.Manager{}
|
||||
ctx := context.Background()
|
||||
|
||||
err = manager.Init(ctx)
|
||||
|
||||
if expectError {
|
||||
if err == nil {
|
||||
t.Errorf("Expected error but got none for test case: %s", name)
|
||||
} else {
|
||||
t.Logf("Got expected error: %v", err)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error for test case %s: %v", name, err)
|
||||
} else {
|
||||
t.Logf("Successfully loaded setting")
|
||||
|
||||
// Verify the loaded setting
|
||||
loadedSetting := manager.GetWoxSetting(ctx)
|
||||
suite.validateLoadedWoxSetting(t, loadedSetting, expectDefaults, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// validateLoadedWoxSetting validates that a loaded WoxSetting has reasonable values
|
||||
func (suite *ConfigBlackboxTestSuite) validateLoadedWoxSetting(t *testing.T, woxSetting *setting.WoxSetting, expectDefaults bool, testName string) {
|
||||
if woxSetting == nil {
|
||||
t.Errorf("Loaded setting is nil for test %s", testName)
|
||||
return
|
||||
}
|
||||
|
||||
defaultSetting := setting.GetDefaultWoxSetting(context.Background())
|
||||
|
||||
// Check critical fields have reasonable values
|
||||
if expectDefaults {
|
||||
// When expecting defaults, missing fields should be filled with default values
|
||||
if woxSetting.AppWidth == 0 {
|
||||
t.Errorf("AppWidth should have default value, got 0 in test %s", testName)
|
||||
}
|
||||
if woxSetting.MaxResultCount == 0 {
|
||||
t.Errorf("MaxResultCount should have default value, got 0 in test %s", testName)
|
||||
}
|
||||
if woxSetting.ThemeId == "" {
|
||||
t.Errorf("ThemeId should have default value, got empty string in test %s", testName)
|
||||
}
|
||||
if woxSetting.LangCode == "" {
|
||||
t.Errorf("LangCode should have default value, got empty string in test %s", testName)
|
||||
}
|
||||
if woxSetting.MainHotkey.Get() == "" {
|
||||
t.Errorf("MainHotkey should have default value, got empty string in test %s", testName)
|
||||
}
|
||||
}
|
||||
|
||||
// Log the loaded values for debugging
|
||||
t.Logf("Loaded setting values for %s:", testName)
|
||||
t.Logf(" AppWidth: %d (default: %d)", woxSetting.AppWidth, defaultSetting.AppWidth)
|
||||
t.Logf(" MaxResultCount: %d (default: %d)", woxSetting.MaxResultCount, defaultSetting.MaxResultCount)
|
||||
t.Logf(" UsePinYin: %t (default: %t)", woxSetting.UsePinYin, defaultSetting.UsePinYin)
|
||||
t.Logf(" ShowTray: %t (default: %t)", woxSetting.ShowTray, defaultSetting.ShowTray)
|
||||
t.Logf(" LangCode: %s (default: %s)", woxSetting.LangCode, defaultSetting.LangCode)
|
||||
t.Logf(" ThemeId: %s (default: %s)", woxSetting.ThemeId, defaultSetting.ThemeId)
|
||||
t.Logf(" MainHotkey: %s (default: %s)", woxSetting.MainHotkey.Get(), defaultSetting.MainHotkey.Get())
|
||||
}
|
||||
|
||||
// cleanup cleans up test resources
|
||||
func (suite *ConfigBlackboxTestSuite) cleanup() {
|
||||
if suite.testDir != "" {
|
||||
os.RemoveAll(suite.testDir)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue