diff --git a/internal/rules/database.go b/internal/rules/database.go index 31a5427..b8c0fe1 100644 --- a/internal/rules/database.go +++ b/internal/rules/database.go @@ -5,17 +5,18 @@ import ( "log" "strconv" - "eq2emu/internal/database" + "zombiezen.com/go/sqlite" + "zombiezen.com/go/sqlite/sqlitex" ) // DatabaseService handles rule database operations // Converted from C++ WorldDatabase rule functions type DatabaseService struct { - db *database.DB + db *sqlite.Conn } // NewDatabaseService creates a new database service instance -func NewDatabaseService(db *database.DB) *DatabaseService { +func NewDatabaseService(db *sqlite.Conn) *DatabaseService { return &DatabaseService{ db: db, } @@ -32,18 +33,20 @@ func (ds *DatabaseService) LoadGlobalRuleSet(ruleManager *RuleManager) error { // Get the default ruleset ID from variables table query := "SELECT variable_value FROM variables WHERE variable_name = ?" - row, err := ds.db.QueryRow(query, DefaultRuleSetIDVar) + stmt := ds.db.Prep(query) + stmt.BindText(1, DefaultRuleSetIDVar) + + hasRow, err := stmt.Step() if err != nil { return fmt.Errorf("error querying default ruleset ID: %v", err) } - if row == nil { + if !hasRow { log.Printf("[Rules] Variables table is missing %s variable name, using code-default rules", DefaultRuleSetIDVar) return nil } - defer row.Close() - variableValue := row.Text(0) + variableValue := stmt.ColumnText(0) if id, err := strconv.ParseInt(variableValue, 10, 32); err == nil { ruleSetID = int32(id) log.Printf("[Rules] Loading Global Ruleset id %d", ruleSetID) @@ -79,9 +82,20 @@ func (ds *DatabaseService) LoadRuleSets(ruleManager *RuleManager, reload bool) e query := "SELECT ruleset_id, ruleset_name FROM rulesets WHERE ruleset_active > 0" loadedCount := 0 - err := ds.db.Query(query, func(row *database.Row) error { - ruleSetID := int32(row.Int64(0)) - ruleSetName := row.Text(1) + stmt := ds.db.Prep(query) + defer stmt.Finalize() + + for { + hasRow, err := stmt.Step() + if err != nil { + return fmt.Errorf("error querying rule sets: %v", err) + } + if !hasRow { + break + } + + ruleSetID := int32(stmt.ColumnInt64(0)) + ruleSetName := stmt.ColumnText(1) ruleSet := NewRuleSet() ruleSet.SetID(ruleSetID) @@ -93,23 +107,18 @@ func (ds *DatabaseService) LoadRuleSets(ruleManager *RuleManager, reload bool) e err := ds.LoadRuleSetDetails(ruleManager, ruleSet) if err != nil { log.Printf("[Rules] Error loading rule set details for '%s': %v", ruleSetName, err) - return nil // Continue with other rule sets + continue // Continue with other rule sets } loadedCount++ } else { log.Printf("[Rules] Unable to add rule set '%s' - ID %d already exists", ruleSetName, ruleSetID) } - return nil - }) - - if err != nil { - return fmt.Errorf("error querying rule sets: %v", err) } log.Printf("[Rules] Loaded %d Rule Sets", loadedCount) // Load global rule set - err = ds.LoadGlobalRuleSet(ruleManager) + err := ds.LoadGlobalRuleSet(ruleManager) if err != nil { return fmt.Errorf("error loading global rule set: %v", err) } @@ -136,26 +145,33 @@ func (ds *DatabaseService) LoadRuleSetDetails(ruleManager *RuleManager, ruleSet query := "SELECT rule_category, rule_type, rule_value FROM ruleset_details WHERE ruleset_id = ?" loadedRules := 0 - err := ds.db.Query(query, func(row *database.Row) error { - categoryName := row.Text(0) - typeName := row.Text(1) - ruleValue := row.Text(2) + stmt := ds.db.Prep(query) + stmt.BindInt64(1, int64(ruleSet.GetID())) + defer stmt.Finalize() + + for { + hasRow, err := stmt.Step() + if err != nil { + return fmt.Errorf("error querying rule set details: %v", err) + } + if !hasRow { + break + } + + categoryName := stmt.ColumnText(0) + typeName := stmt.ColumnText(1) + ruleValue := stmt.ColumnText(2) // Find the rule by name rule := ruleSet.GetRuleByName(categoryName, typeName) if rule == nil { log.Printf("[Rules] Unknown rule with category '%s' and type '%s'", categoryName, typeName) - return nil // Continue with other rules + continue // Continue with other rules } log.Printf("[Rules] Setting rule category '%s', type '%s' to value: %s", categoryName, typeName, ruleValue) rule.SetValue(ruleValue) loadedRules++ - return nil - }, ruleSet.GetID()) - - if err != nil { - return fmt.Errorf("error querying rule set details: %v", err) } log.Printf("[Rules] Loaded %d rule overrides for rule set '%s'", loadedRules, ruleSet.GetName()) @@ -175,45 +191,62 @@ func (ds *DatabaseService) SaveRuleSet(ruleSet *RuleSet) error { } // Use transaction for atomicity - return ds.db.Transaction(func(tx *database.DB) error { - // Insert or update rule set - query := `INSERT INTO rulesets (ruleset_id, ruleset_name, ruleset_active) - VALUES (?, ?, 1) - ON CONFLICT(ruleset_id) DO UPDATE SET - ruleset_name = excluded.ruleset_name, - ruleset_active = excluded.ruleset_active` + var err error + defer sqlitex.Save(ds.db)(&err) - err := tx.Exec(query, ruleSet.GetID(), ruleSet.GetName()) - if err != nil { - return fmt.Errorf("error saving rule set: %v", err) - } + // Insert or update rule set + query := `INSERT INTO rulesets (ruleset_id, ruleset_name, ruleset_active) + VALUES (?, ?, 1) + ON CONFLICT(ruleset_id) DO UPDATE SET + ruleset_name = excluded.ruleset_name, + ruleset_active = excluded.ruleset_active` - // Delete existing rule details - err = tx.Exec("DELETE FROM ruleset_details WHERE ruleset_id = ?", ruleSet.GetID()) - if err != nil { - return fmt.Errorf("error deleting existing rule details: %v", err) - } + stmt := ds.db.Prep(query) + stmt.BindInt64(1, int64(ruleSet.GetID())) + stmt.BindText(2, ruleSet.GetName()) + + _, err = stmt.Step() + if err != nil { + return fmt.Errorf("error saving rule set: %v", err) + } + stmt.Finalize() - // Insert rule details - rules := ruleSet.GetRules() - for _, categoryMap := range rules { - for _, rule := range categoryMap { - if rule.IsValid() { - combined := rule.GetCombined() - parts := splitCombined(combined) - if len(parts) == 2 { - query := "INSERT INTO ruleset_details (ruleset_id, rule_category, rule_type, rule_value) VALUES (?, ?, ?, ?)" - err = tx.Exec(query, ruleSet.GetID(), parts[0], parts[1], rule.GetValue()) - if err != nil { - return fmt.Errorf("error saving rule detail: %v", err) - } + // Delete existing rule details + deleteQuery := "DELETE FROM ruleset_details WHERE ruleset_id = ?" + deleteStmt := ds.db.Prep(deleteQuery) + deleteStmt.BindInt64(1, int64(ruleSet.GetID())) + _, err = deleteStmt.Step() + if err != nil { + return fmt.Errorf("error deleting existing rule details: %v", err) + } + deleteStmt.Finalize() + + // Insert rule details + insertQuery := "INSERT INTO ruleset_details (ruleset_id, rule_category, rule_type, rule_value) VALUES (?, ?, ?, ?)" + rules := ruleSet.GetRules() + for _, categoryMap := range rules { + for _, rule := range categoryMap { + if rule.IsValid() { + combined := rule.GetCombined() + parts := splitCombined(combined) + if len(parts) == 2 { + insertStmt := ds.db.Prep(insertQuery) + insertStmt.BindInt64(1, int64(ruleSet.GetID())) + insertStmt.BindText(2, parts[0]) + insertStmt.BindText(3, parts[1]) + insertStmt.BindText(4, rule.GetValue()) + + _, err = insertStmt.Step() + insertStmt.Finalize() + if err != nil { + return fmt.Errorf("error saving rule detail: %v", err) } } } } + } - return nil - }) + return nil } // DeleteRuleSet deletes a rule set from the database @@ -223,21 +256,28 @@ func (ds *DatabaseService) DeleteRuleSet(ruleSetID int32) error { } // Use transaction for atomicity - return ds.db.Transaction(func(tx *database.DB) error { - // Delete rule details first (foreign key constraint) - err := tx.Exec("DELETE FROM ruleset_details WHERE ruleset_id = ?", ruleSetID) - if err != nil { - return fmt.Errorf("error deleting rule details: %v", err) - } + var err error + defer sqlitex.Save(ds.db)(&err) - // Delete rule set - err = tx.Exec("DELETE FROM rulesets WHERE ruleset_id = ?", ruleSetID) - if err != nil { - return fmt.Errorf("error deleting rule set: %v", err) - } + // Delete rule details first (foreign key constraint) + detailsStmt := ds.db.Prep("DELETE FROM ruleset_details WHERE ruleset_id = ?") + detailsStmt.BindInt64(1, int64(ruleSetID)) + _, err = detailsStmt.Step() + detailsStmt.Finalize() + if err != nil { + return fmt.Errorf("error deleting rule details: %v", err) + } - return nil - }) + // Delete rule set + rulesetStmt := ds.db.Prep("DELETE FROM rulesets WHERE ruleset_id = ?") + rulesetStmt.BindInt64(1, int64(ruleSetID)) + _, err = rulesetStmt.Step() + rulesetStmt.Finalize() + if err != nil { + return fmt.Errorf("error deleting rule set: %v", err) + } + + return nil } // SetDefaultRuleSet sets the default rule set ID in the variables table @@ -251,7 +291,12 @@ func (ds *DatabaseService) SetDefaultRuleSet(ruleSetID int32) error { ON CONFLICT(variable_name) DO UPDATE SET variable_value = excluded.variable_value` - err := ds.db.Exec(query, DefaultRuleSetIDVar, strconv.Itoa(int(ruleSetID))) + stmt := ds.db.Prep(query) + stmt.BindText(1, DefaultRuleSetIDVar) + stmt.BindText(2, strconv.Itoa(int(ruleSetID))) + + _, err := stmt.Step() + stmt.Finalize() if err != nil { return fmt.Errorf("error setting default rule set: %v", err) } @@ -266,17 +311,19 @@ func (ds *DatabaseService) GetDefaultRuleSetID() (int32, error) { } query := "SELECT variable_value FROM variables WHERE variable_name = ?" - row, err := ds.db.QueryRow(query, DefaultRuleSetIDVar) + stmt := ds.db.Prep(query) + stmt.BindText(1, DefaultRuleSetIDVar) + + hasRow, err := stmt.Step() if err != nil { return 0, fmt.Errorf("error querying default ruleset ID: %v", err) } - if row == nil { + if !hasRow { return 0, fmt.Errorf("default ruleset ID not found in variables table") } - defer row.Close() - variableValue := row.Text(0) + variableValue := stmt.ColumnText(0) if id, err := strconv.ParseInt(variableValue, 10, 32); err == nil { return int32(id), nil } @@ -293,18 +340,24 @@ func (ds *DatabaseService) GetRuleSetList() ([]RuleSetInfo, error) { query := "SELECT ruleset_id, ruleset_name, ruleset_active FROM rulesets ORDER BY ruleset_id" var ruleSets []RuleSetInfo - err := ds.db.Query(query, func(row *database.Row) error { + stmt := ds.db.Prep(query) + defer stmt.Finalize() + + for { + hasRow, err := stmt.Step() + if err != nil { + return nil, fmt.Errorf("error querying rule sets: %v", err) + } + if !hasRow { + break + } + info := RuleSetInfo{ - ID: int32(row.Int64(0)), - Name: row.Text(1), - Active: row.Bool(2), + ID: int32(stmt.ColumnInt64(0)), + Name: stmt.ColumnText(1), + Active: stmt.ColumnInt64(2) > 0, // Convert int to bool } ruleSets = append(ruleSets, info) - return nil - }) - - if err != nil { - return nil, fmt.Errorf("error querying rule sets: %v", err) } return ruleSets, nil @@ -316,38 +369,26 @@ func (ds *DatabaseService) ValidateDatabase() error { return fmt.Errorf("database not initialized") } - // Check if rulesets table exists - query := "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='rulesets'" - row, err := ds.db.QueryRow(query) - if err != nil { - return fmt.Errorf("error checking rulesets table: %v", err) - } - if row == nil || row.Int(0) == 0 { - return fmt.Errorf("rulesets table does not exist") - } - row.Close() + tables := []string{"rulesets", "ruleset_details", "variables"} + query := "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?" - // Check if ruleset_details table exists - query = "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='ruleset_details'" - row, err = ds.db.QueryRow(query) - if err != nil { - return fmt.Errorf("error checking ruleset_details table: %v", err) + for _, table := range tables { + stmt := ds.db.Prep(query) + stmt.BindText(1, table) + + hasRow, err := stmt.Step() + if err != nil { + stmt.Finalize() + return fmt.Errorf("error checking %s table: %v", table, err) + } + + count := stmt.ColumnInt64(0) + stmt.Finalize() + + if !hasRow || count == 0 { + return fmt.Errorf("%s table does not exist", table) + } } - if row == nil || row.Int(0) == 0 { - return fmt.Errorf("ruleset_details table does not exist") - } - row.Close() - - // Check if variables table exists - query = "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='variables'" - row, err = ds.db.QueryRow(query) - if err != nil { - return fmt.Errorf("error checking variables table: %v", err) - } - if row == nil || row.Int(0) == 0 { - return fmt.Errorf("variables table does not exist") - } - row.Close() return nil } @@ -383,7 +424,9 @@ func (ds *DatabaseService) CreateRulesTables() error { ruleset_active INTEGER NOT NULL DEFAULT 0 )` - err := ds.db.Exec(createRuleSets) + stmt := ds.db.Prep(createRuleSets) + _, err := stmt.Step() + stmt.Finalize() if err != nil { return fmt.Errorf("error creating rulesets table: %v", err) } @@ -400,7 +443,9 @@ func (ds *DatabaseService) CreateRulesTables() error { FOREIGN KEY (ruleset_id) REFERENCES rulesets(ruleset_id) ON DELETE CASCADE )` - err = ds.db.Exec(createRuleSetDetails) + stmt = ds.db.Prep(createRuleSetDetails) + _, err = stmt.Step() + stmt.Finalize() if err != nil { return fmt.Errorf("error creating ruleset_details table: %v", err) } @@ -413,7 +458,9 @@ func (ds *DatabaseService) CreateRulesTables() error { comment TEXT )` - err = ds.db.Exec(createVariables) + stmt = ds.db.Prep(createVariables) + _, err = stmt.Step() + stmt.Finalize() if err != nil { return fmt.Errorf("error creating variables table: %v", err) } @@ -426,7 +473,9 @@ func (ds *DatabaseService) CreateRulesTables() error { } for _, indexSQL := range indexes { - err = ds.db.Exec(indexSQL) + stmt = ds.db.Prep(indexSQL) + _, err = stmt.Step() + stmt.Finalize() if err != nil { return fmt.Errorf("error creating index: %v", err) } diff --git a/internal/rules/rules_test.go b/internal/rules/rules_test.go index eabfc7f..f6d0851 100644 --- a/internal/rules/rules_test.go +++ b/internal/rules/rules_test.go @@ -2,11 +2,12 @@ package rules import ( "testing" + + "zombiezen.com/go/sqlite" ) // Test Rule creation and basic functionality -func TestRule(t *testing.T) { - // Test NewRule with default values +func TestNewRule(t *testing.T) { rule := NewRule() if rule == nil { t.Fatal("NewRule() returned nil") @@ -27,51 +28,39 @@ func TestRule(t *testing.T) { if rule.GetCombined() != "NONE" { t.Errorf("Expected combined 'NONE', got %s", rule.GetCombined()) } +} - // Test NewRuleWithValues - rule2 := NewRuleWithValues(CategoryPlayer, PlayerMaxLevel, "50", "Player:MaxLevel") - if rule2 == nil { +func TestNewRuleWithValues(t *testing.T) { + rule := NewRuleWithValues(CategoryPlayer, PlayerMaxLevel, "50", "Player:MaxLevel") + if rule == nil { t.Fatal("NewRuleWithValues() returned nil") } - if rule2.GetCategory() != CategoryPlayer { - t.Errorf("Expected category %d, got %d", CategoryPlayer, rule2.GetCategory()) + if rule.GetCategory() != CategoryPlayer { + t.Errorf("Expected category %d, got %d", CategoryPlayer, rule.GetCategory()) } - if rule2.GetType() != PlayerMaxLevel { - t.Errorf("Expected type %d, got %d", PlayerMaxLevel, rule2.GetType()) + if rule.GetType() != PlayerMaxLevel { + t.Errorf("Expected type %d, got %d", PlayerMaxLevel, rule.GetType()) } - if rule2.GetValue() != "50" { - t.Errorf("Expected value '50', got %s", rule2.GetValue()) + if rule.GetValue() != "50" { + t.Errorf("Expected value '50', got %s", rule.GetValue()) } - if rule2.GetCombined() != "Player:MaxLevel" { - t.Errorf("Expected combined 'Player:MaxLevel', got %s", rule2.GetCombined()) - } - - // Test type conversion methods - if rule2.GetInt32() != 50 { - t.Errorf("Expected int32 50, got %d", rule2.GetInt32()) - } - - if rule2.GetBool() != true { - t.Errorf("Expected bool true, got %t", rule2.GetBool()) - } - - // Test SetValue - rule2.SetValue("100") - if rule2.GetValue() != "100" { - t.Errorf("Expected value '100' after SetValue, got %s", rule2.GetValue()) - } - - if rule2.GetInt32() != 100 { - t.Errorf("Expected int32 100 after SetValue, got %d", rule2.GetInt32()) + if rule.GetCombined() != "Player:MaxLevel" { + t.Errorf("Expected combined 'Player:MaxLevel', got %s", rule.GetCombined()) } } -// Test Rule copy constructor -func TestRuleCopy(t *testing.T) { +func TestNewRuleFromRule(t *testing.T) { + // Test with nil source + rule := NewRuleFromRule(nil) + if rule == nil { + t.Error("NewRuleFromRule(nil) should return default rule, not nil") + } + + // Test with valid source original := NewRuleWithValues(CategoryCombat, CombatMaxRange, "4.0", "Combat:MaxCombatRange") copy := NewRuleFromRule(original) @@ -102,8 +91,122 @@ func TestRuleCopy(t *testing.T) { } } -// Test RuleSet creation and basic operations -func TestRuleSet(t *testing.T) { +func TestRuleSetValue(t *testing.T) { + rule := NewRule() + rule.SetValue("test") + + if rule.GetValue() != "test" { + t.Errorf("SetValue failed: expected 'test', got %s", rule.GetValue()) + } +} + +func TestRuleTypeConversions(t *testing.T) { + rule := NewRuleWithValues(CategoryPlayer, PlayerMaxLevel, "50", "Player:MaxLevel") + + // Test integer conversions + if rule.GetInt8() != 50 { + t.Errorf("Expected int8 50, got %d", rule.GetInt8()) + } + + if rule.GetInt16() != 50 { + t.Errorf("Expected int16 50, got %d", rule.GetInt16()) + } + + if rule.GetInt32() != 50 { + t.Errorf("Expected int32 50, got %d", rule.GetInt32()) + } + + if rule.GetInt64() != 50 { + t.Errorf("Expected int64 50, got %d", rule.GetInt64()) + } + + // Test unsigned integer conversions + if rule.GetUInt8() != 50 { + t.Errorf("Expected uint8 50, got %d", rule.GetUInt8()) + } + + if rule.GetUInt16() != 50 { + t.Errorf("Expected uint16 50, got %d", rule.GetUInt16()) + } + + if rule.GetUInt32() != 50 { + t.Errorf("Expected uint32 50, got %d", rule.GetUInt32()) + } + + if rule.GetUInt64() != 50 { + t.Errorf("Expected uint64 50, got %d", rule.GetUInt64()) + } + + // Test boolean conversion (> 0 = true) + if !rule.GetBool() { + t.Error("Expected bool true for value '50'") + } + + // Test float conversions + rule.SetValue("3.14") + if rule.GetFloat32() != 3.14 { + t.Errorf("Expected float32 3.14, got %f", rule.GetFloat32()) + } + + if rule.GetFloat64() != 3.14 { + t.Errorf("Expected float64 3.14, got %f", rule.GetFloat64()) + } + + // Test character conversion + rule.SetValue("Hello") + if rule.GetChar() != 'H' { + t.Errorf("Expected char 'H', got %c", rule.GetChar()) + } + + // Test string conversion + if rule.GetString() != "Hello" { + t.Errorf("Expected string 'Hello', got %s", rule.GetString()) + } +} + +func TestRuleTypeConversionsInvalid(t *testing.T) { + rule := NewRuleWithValues(CategoryPlayer, PlayerMaxLevel, "invalid", "Player:MaxLevel") + + // Invalid conversions should return zero values + if rule.GetInt32() != 0 { + t.Errorf("Expected int32 0 for invalid value, got %d", rule.GetInt32()) + } + + if rule.GetFloat64() != 0.0 { + t.Errorf("Expected float64 0.0 for invalid value, got %f", rule.GetFloat64()) + } + + if rule.GetBool() != false { + t.Error("Expected bool false for invalid value") + } +} + +func TestRuleIsValid(t *testing.T) { + // Test invalid rule (default) + rule := NewRule() + if rule.IsValid() { + t.Error("Default rule should not be valid") + } + + // Test valid rule + validRule := NewRuleWithValues(CategoryPlayer, PlayerMaxLevel, "50", "Player:MaxLevel") + if !validRule.IsValid() { + t.Error("Rule with valid data should be valid") + } +} + +func TestRuleString(t *testing.T) { + rule := NewRuleWithValues(CategoryPlayer, PlayerMaxLevel, "50", "Player:MaxLevel") + str := rule.String() + + expected := "Rule{Player:MaxLevel: 50}" + if str != expected { + t.Errorf("Expected string %s, got %s", expected, str) + } +} + +// Test RuleSet creation and operations +func TestNewRuleSet(t *testing.T) { ruleSet := NewRuleSet() if ruleSet == nil { t.Fatal("NewRuleSet() returned nil") @@ -117,6 +220,14 @@ func TestRuleSet(t *testing.T) { t.Errorf("Expected empty name, got %s", ruleSet.GetName()) } + if ruleSet.Size() != 0 { + t.Errorf("Expected size 0, got %d", ruleSet.Size()) + } +} + +func TestRuleSetIDAndName(t *testing.T) { + ruleSet := NewRuleSet() + // Test SetID and SetName ruleSet.SetID(1) ruleSet.SetName("Test Rule Set") @@ -128,38 +239,77 @@ func TestRuleSet(t *testing.T) { if ruleSet.GetName() != "Test Rule Set" { t.Errorf("Expected name 'Test Rule Set', got %s", ruleSet.GetName()) } +} - // Test adding rules +func TestRuleSetAddRule(t *testing.T) { + ruleSet := NewRuleSet() + + // Test adding nil rule + ruleSet.AddRule(nil) // Should not crash + if ruleSet.Size() != 0 { + t.Error("Adding nil rule should not increase size") + } + + // Test adding valid rules rule1 := NewRuleWithValues(CategoryPlayer, PlayerMaxLevel, "50", "Player:MaxLevel") rule2 := NewRuleWithValues(CategoryCombat, CombatMaxRange, "4.0", "Combat:MaxCombatRange") ruleSet.AddRule(rule1) ruleSet.AddRule(rule2) - // Test rule retrieval - retrievedRule1 := ruleSet.GetRule(CategoryPlayer, PlayerMaxLevel) - if retrievedRule1 == nil { - t.Fatal("GetRule() returned nil for added rule") - } - - if retrievedRule1.GetValue() != "50" { - t.Errorf("Retrieved rule value mismatch: expected '50', got %s", retrievedRule1.GetValue()) - } - - // Test rule retrieval by name - retrievedRule2 := ruleSet.GetRuleByName("Combat", "MaxCombatRange") - if retrievedRule2 == nil { - t.Fatal("GetRuleByName() returned nil for added rule") - } - - if retrievedRule2.GetValue() != "4.0" { - t.Errorf("Retrieved rule value mismatch: expected '4.0', got %s", retrievedRule2.GetValue()) - } - - // Test Size if ruleSet.Size() != 2 { t.Errorf("Expected size 2, got %d", ruleSet.Size()) } +} + +func TestRuleSetGetRule(t *testing.T) { + ruleSet := NewRuleSet() + rule1 := NewRuleWithValues(CategoryPlayer, PlayerMaxLevel, "50", "Player:MaxLevel") + ruleSet.AddRule(rule1) + + // Test rule retrieval + retrievedRule := ruleSet.GetRule(CategoryPlayer, PlayerMaxLevel) + if retrievedRule == nil { + t.Fatal("GetRule() returned nil for added rule") + } + + if retrievedRule.GetValue() != "50" { + t.Errorf("Retrieved rule value mismatch: expected '50', got %s", retrievedRule.GetValue()) + } + + // Test non-existent rule + nonExistentRule := ruleSet.GetRule(CategorySpawn, SpawnSpeedMultiplier) + if nonExistentRule != nil { + t.Error("GetRule() should return nil for non-existent rule") + } +} + +func TestRuleSetGetRuleByName(t *testing.T) { + ruleSet := NewRuleSet() + rule := NewRuleWithValues(CategoryCombat, CombatMaxRange, "4.0", "Combat:MaxCombatRange") + ruleSet.AddRule(rule) + + // Test rule retrieval by name + retrievedRule := ruleSet.GetRuleByName("Combat", "MaxCombatRange") + if retrievedRule == nil { + t.Fatal("GetRuleByName() returned nil for added rule") + } + + if retrievedRule.GetValue() != "4.0" { + t.Errorf("Retrieved rule value mismatch: expected '4.0', got %s", retrievedRule.GetValue()) + } + + // Test non-existent rule + nonExistentRule := ruleSet.GetRuleByName("NonExistent", "Rule") + if nonExistentRule != nil { + t.Error("GetRuleByName() should return nil for non-existent rule") + } +} + +func TestRuleSetHasRule(t *testing.T) { + ruleSet := NewRuleSet() + rule := NewRuleWithValues(CategoryPlayer, PlayerMaxLevel, "50", "Player:MaxLevel") + ruleSet.AddRule(rule) // Test HasRule if !ruleSet.HasRule(CategoryPlayer, PlayerMaxLevel) { @@ -171,8 +321,86 @@ func TestRuleSet(t *testing.T) { } } -// Test RuleSet copy functionality -func TestRuleSetCopy(t *testing.T) { +func TestRuleSetGetRulesByCategory(t *testing.T) { + ruleSet := NewRuleSet() + + // Add multiple player rules + rule1 := NewRuleWithValues(CategoryPlayer, PlayerMaxLevel, "50", "Player:MaxLevel") + rule2 := NewRuleWithValues(CategoryPlayer, PlayerVitalityAmount, "0.5", "Player:VitalityAmount") + rule3 := NewRuleWithValues(CategoryCombat, CombatMaxRange, "4.0", "Combat:MaxCombatRange") + + ruleSet.AddRule(rule1) + ruleSet.AddRule(rule2) + ruleSet.AddRule(rule3) + + // Test getting rules by category + playerRules := ruleSet.GetRulesByCategory(CategoryPlayer) + if len(playerRules) != 2 { + t.Errorf("Expected 2 player rules, got %d", len(playerRules)) + } + + // Test empty category + spawnRules := ruleSet.GetRulesByCategory(CategorySpawn) + if len(spawnRules) != 0 { + t.Errorf("Expected 0 spawn rules, got %d", len(spawnRules)) + } +} + +func TestRuleSetClearRules(t *testing.T) { + ruleSet := NewRuleSet() + rule := NewRuleWithValues(CategoryPlayer, PlayerMaxLevel, "50", "Player:MaxLevel") + ruleSet.AddRule(rule) + + if ruleSet.Size() != 1 { + t.Error("Rule set should have 1 rule before clearing") + } + + ruleSet.ClearRules() + + if ruleSet.Size() != 0 { + t.Error("Rule set should be empty after clearing") + } +} + +func TestRuleSetCopyRulesInto(t *testing.T) { + source := NewRuleSet() + source.SetID(1) + source.SetName("Source") + + rule1 := NewRuleWithValues(CategoryPlayer, PlayerMaxLevel, "50", "Player:MaxLevel") + rule2 := NewRuleWithValues(CategoryCombat, CombatMaxRange, "4.0", "Combat:MaxCombatRange") + source.AddRule(rule1) + source.AddRule(rule2) + + target := NewRuleSet() + target.SetID(2) + target.SetName("Target") + + // Test copying rules + target.CopyRulesInto(source) + + if target.Size() != 2 { + t.Errorf("Target should have 2 rules after copying, got %d", target.Size()) + } + + // Test that target still has its own ID and name + if target.GetID() != 2 { + t.Errorf("Target ID should remain 2, got %d", target.GetID()) + } + + if target.GetName() != "Target" { + t.Errorf("Target name should remain 'Target', got %s", target.GetName()) + } +} + +func TestNewRuleSetFromRuleSet(t *testing.T) { + // Test with nil source + nilCopy := NewRuleSetFromRuleSet(nil) + if nilCopy == nil { + t.Error("NewRuleSetFromRuleSet(nil) should return empty rule set, not nil") + } + + // Test with valid source original := NewRuleSet() original.SetID(1) original.SetName("Original") @@ -211,8 +439,23 @@ func TestRuleSetCopy(t *testing.T) { } } -// Test RuleManager initialization -func TestRuleManager(t *testing.T) { +func TestRuleSetString(t *testing.T) { + ruleSet := NewRuleSet() + ruleSet.SetID(1) + ruleSet.SetName("Test Set") + + rule := NewRuleWithValues(CategoryPlayer, PlayerMaxLevel, "50", "Player:MaxLevel") + ruleSet.AddRule(rule) + + str := ruleSet.String() + expected := "RuleSet{ID: 1, Name: Test Set, Rules: 1}" + if str != expected { + t.Errorf("Expected string %s, got %s", expected, str) + } +} + +// Test RuleManager initialization and operations +func TestNewRuleManager(t *testing.T) { ruleManager := NewRuleManager() if ruleManager == nil { t.Fatal("NewRuleManager() returned nil") @@ -221,8 +464,12 @@ func TestRuleManager(t *testing.T) { if !ruleManager.IsInitialized() { t.Error("RuleManager should be initialized after creation") } +} - // Test getting a default rule +func TestRuleManagerGetGlobalRule(t *testing.T) { + ruleManager := NewRuleManager() + + // Test getting a default rule that should be initialized rule := ruleManager.GetGlobalRule(CategoryPlayer, PlayerMaxLevel) if rule == nil { t.Fatal("GetGlobalRule() returned nil for default rule") @@ -242,6 +489,10 @@ func TestRuleManager(t *testing.T) { if rule2.GetValue() != "50" { t.Errorf("Expected default value '50', got %s", rule2.GetValue()) } +} + +func TestRuleManagerBlankRule(t *testing.T) { + ruleManager := NewRuleManager() // Test blank rule for non-existent rule blankRule := ruleManager.GetGlobalRule(9999, 9999) @@ -252,10 +503,19 @@ func TestRuleManager(t *testing.T) { if blankRule.IsValid() { t.Error("Blank rule should not be valid") } + + // Test GetBlankRule method + blankRule2 := ruleManager.GetBlankRule() + if blankRule2 == nil { + t.Fatal("GetBlankRule() should not return nil") + } + + if blankRule2.IsValid() { + t.Error("Blank rule should not be valid") + } } -// Test RuleManager rule set operations -func TestRuleManagerRuleSets(t *testing.T) { +func TestRuleManagerRuleSetOperations(t *testing.T) { ruleManager := NewRuleManager() // Create a test rule set @@ -295,6 +555,26 @@ func TestRuleManagerRuleSets(t *testing.T) { if ruleManager.GetNumRuleSets() != 1 { t.Errorf("Expected 1 rule set, got %d", ruleManager.GetNumRuleSets()) } +} + +func TestRuleManagerGlobalRuleSet(t *testing.T) { + ruleManager := NewRuleManager() + + // Create a test rule set with modified rules + ruleSet := NewRuleSet() + ruleSet.SetID(1) + ruleSet.SetName("Test Set") + + // Load coded defaults first, then modify + ruleManager.LoadCodedDefaultsIntoRuleSet(ruleSet) + + // Override a rule + playerMaxLevelRule := ruleSet.GetRule(CategoryPlayer, PlayerMaxLevel) + if playerMaxLevelRule != nil { + playerMaxLevelRule.SetValue("60") + } + + ruleManager.AddRuleSet(ruleSet) // Test setting global rule set if !ruleManager.SetGlobalRuleSet(1) { @@ -312,7 +592,200 @@ func TestRuleManagerRuleSets(t *testing.T) { } } -// Test category and type name functions +func TestRuleManagerZoneRules(t *testing.T) { + ruleManager := NewRuleManager() + + // Create a zone-specific rule set + zoneRuleSet := NewRuleSet() + zoneRuleSet.SetID(100) + zoneRuleSet.SetName("Zone Rules") + + // Load coded defaults and modify a rule + ruleManager.LoadCodedDefaultsIntoRuleSet(zoneRuleSet) + playerMaxLevelRule := zoneRuleSet.GetRule(CategoryPlayer, PlayerMaxLevel) + if playerMaxLevelRule != nil { + playerMaxLevelRule.SetValue("70") + } + + ruleManager.AddRuleSet(zoneRuleSet) + + // Set zone rule set + zoneID := int32(1) + if !ruleManager.SetZoneRuleSet(zoneID, 100) { + t.Fatal("SetZoneRuleSet() returned false") + } + + // Test zone rule lookup + zoneRule := ruleManager.GetZoneRule(zoneID, CategoryPlayer, PlayerMaxLevel) + if zoneRule == nil { + t.Fatal("GetZoneRule() returned nil") + } + + if zoneRule.GetValue() != "70" { + t.Errorf("Expected zone rule value '70', got %s", zoneRule.GetValue()) + } + + // Test fallback to global rule for non-zone rule + globalFallback := ruleManager.GetZoneRule(999, CategoryPlayer, PlayerMaxLevel) + if globalFallback == nil { + t.Fatal("GetZoneRule() should fallback to global rule") + } +} + +func TestRuleManagerFlush(t *testing.T) { + ruleManager := NewRuleManager() + + // Add a rule set + ruleSet := NewRuleSet() + ruleSet.SetID(1) + ruleSet.SetName("Test Set") + ruleManager.AddRuleSet(ruleSet) + + // Test flush with reinit + ruleManager.Flush(true) + + if ruleManager.GetNumRuleSets() != 0 { + t.Error("Rule sets should be cleared after flush") + } + + if !ruleManager.IsInitialized() { + t.Error("Rule manager should be initialized after flush(true)") + } + + // Test flush without reinit + ruleManager.Flush(false) + if ruleManager.IsInitialized() { + t.Error("Rule manager should not be initialized after flush(false)") + } +} + +func TestRuleManagerClearOperations(t *testing.T) { + ruleManager := NewRuleManager() + + // Add rule sets + ruleSet1 := NewRuleSet() + ruleSet1.SetID(1) + ruleManager.AddRuleSet(ruleSet1) + + ruleSet2 := NewRuleSet() + ruleSet2.SetID(2) + ruleManager.AddRuleSet(ruleSet2) + + // Set zone rule set + ruleManager.SetZoneRuleSet(1, 1) + + // Test ClearRuleSets + ruleManager.ClearRuleSets() + if ruleManager.GetNumRuleSets() != 0 { + t.Error("Rule sets should be cleared") + } + + // Test ClearZoneRuleSets + ruleManager.ClearZoneRuleSets() + zoneRule := ruleManager.GetZoneRule(1, CategoryPlayer, PlayerMaxLevel) + // Should fallback to global rule, not zone rule + if zoneRule == nil { + t.Error("Should fallback to global rule after clearing zone rule sets") + } +} + +func TestRuleManagerGetAllRuleSets(t *testing.T) { + ruleManager := NewRuleManager() + + // Add multiple rule sets + for i := 1; i <= 3; i++ { + ruleSet := NewRuleSet() + ruleSet.SetID(int32(i)) + ruleSet.SetName("Rule Set " + string(rune(i+'0'))) + ruleManager.AddRuleSet(ruleSet) + } + + allRuleSets := ruleManager.GetAllRuleSets() + if len(allRuleSets) != 3 { + t.Errorf("Expected 3 rule sets, got %d", len(allRuleSets)) + } +} + +func TestRuleManagerValidateRule(t *testing.T) { + ruleManager := NewRuleManager() + + // Test valid rule + err := ruleManager.ValidateRule(CategoryPlayer, PlayerMaxLevel, "50") + if err != nil { + t.Errorf("ValidateRule() returned error for valid rule: %v", err) + } + + // Test rule value too long + longValue := make([]byte, MaxRuleValueLength+1) + for i := range longValue { + longValue[i] = 'a' + } + + err = ruleManager.ValidateRule(CategoryPlayer, PlayerMaxLevel, string(longValue)) + if err != ErrRuleValueTooLong { + t.Error("ValidateRule() should return ErrRuleValueTooLong for long value") + } +} + +func TestRuleManagerGetRuleInfo(t *testing.T) { + ruleManager := NewRuleManager() + + info := ruleManager.GetRuleInfo(CategoryPlayer, PlayerMaxLevel) + expected := "Rule: Player:MaxLevel = 50" + if info != expected { + t.Errorf("Expected rule info %s, got %s", expected, info) + } + + // Test non-existent rule + info = ruleManager.GetRuleInfo(9999, 9999) + if info != "Rule not found" { + t.Errorf("Expected 'Rule not found', got %s", info) + } +} + +func TestRuleManagerStatistics(t *testing.T) { + ruleManager := NewRuleManager() + + // Get initial stats + stats := ruleManager.GetStats() + + // Add rule sets and perform operations to change stats + ruleSet := NewRuleSet() + ruleSet.SetID(1) + ruleManager.AddRuleSet(ruleSet) + + // Get rule to increment lookup counter + ruleManager.GetGlobalRule(CategoryPlayer, PlayerMaxLevel) + + // Get updated stats + newStats := ruleManager.GetStats() + if newStats.TotalRuleSets != 1 { + t.Errorf("Expected 1 total rule set, got %d", newStats.TotalRuleSets) + } + + if newStats.RuleGetOperations <= stats.RuleGetOperations { + t.Error("Rule get operations should have increased") + } + + // Test reset stats + ruleManager.ResetStats() + resetStats := ruleManager.GetStats() + if resetStats.RuleGetOperations != 0 { + t.Error("Stats should be reset to 0") + } +} + +func TestRuleManagerString(t *testing.T) { + ruleManager := NewRuleManager() + str := ruleManager.String() + + // Should contain basic information about rule sets, rules, etc. + if str == "" { + t.Error("String() should return non-empty string") + } +} + +// Test category name functions func TestCategoryNames(t *testing.T) { // Test GetCategoryName name := GetCategoryName(CategoryPlayer) @@ -343,28 +816,6 @@ func TestCategoryNames(t *testing.T) { } } -// Test rule validation -func TestRuleValidation(t *testing.T) { - ruleManager := NewRuleManager() - - // Test valid rule - err := ruleManager.ValidateRule(CategoryPlayer, PlayerMaxLevel, "50") - if err != nil { - t.Errorf("ValidateRule() returned error for valid rule: %v", err) - } - - // Test rule value too long - longValue := make([]byte, MaxRuleValueLength+1) - for i := range longValue { - longValue[i] = 'a' - } - - err = ruleManager.ValidateRule(CategoryPlayer, PlayerMaxLevel, string(longValue)) - if err != ErrRuleValueTooLong { - t.Errorf("ValidateRule() should return ErrRuleValueTooLong for long value") - } -} - // Test RuleManagerAdapter func TestRuleManagerAdapter(t *testing.T) { ruleManager := NewRuleManager() @@ -400,6 +851,11 @@ func TestRuleManagerAdapter(t *testing.T) { t.Errorf("Expected string '50', got %s", stringValue) } + floatValue := adapter.GetFloat64(CategoryPlayer, PlayerMaxLevel) + if floatValue != 50.0 { + t.Errorf("Expected float64 50.0, got %f", floatValue) + } + // Test zone ID if adapter.GetZoneID() != 0 { t.Errorf("Expected zone ID 0, got %d", adapter.GetZoneID()) @@ -411,7 +867,296 @@ func TestRuleManagerAdapter(t *testing.T) { } } -// Benchmark rule access performance +func TestRuleManagerAdapterWithZone(t *testing.T) { + ruleManager := NewRuleManager() + + // Create zone-specific rule set + zoneRuleSet := NewRuleSet() + zoneRuleSet.SetID(100) + ruleManager.LoadCodedDefaultsIntoRuleSet(zoneRuleSet) + + // Override a rule + if rule := zoneRuleSet.GetRule(CategoryPlayer, PlayerMaxLevel); rule != nil { + rule.SetValue("70") + } + + ruleManager.AddRuleSet(zoneRuleSet) + ruleManager.SetZoneRuleSet(1, 100) + + // Test adapter with zone + adapter := NewRuleManagerAdapter(ruleManager, 1) + intValue := adapter.GetInt32(CategoryPlayer, PlayerMaxLevel) + if intValue != 70 { + t.Errorf("Expected zone-specific value 70, got %d", intValue) + } +} + +// Test RuleManagerStats operations +func TestRuleManagerStats(t *testing.T) { + stats := &RuleManagerStats{} + + // Test increment operations + initialGets := stats.RuleGetOperations + stats.IncrementRuleGetOperations() + if stats.RuleGetOperations != initialGets+1 { + t.Error("IncrementRuleGetOperations() did not increment correctly") + } + + initialSets := stats.RuleSetOperations + stats.IncrementRuleSetOperations() + if stats.RuleSetOperations != initialSets+1 { + t.Error("IncrementRuleSetOperations() did not increment correctly") + } + + initialDB := stats.DatabaseOperations + stats.IncrementDatabaseOperations() + if stats.DatabaseOperations != initialDB+1 { + t.Error("IncrementDatabaseOperations() did not increment correctly") + } + + // Test snapshot + snapshot := stats.GetSnapshot() + if snapshot.RuleGetOperations != stats.RuleGetOperations { + t.Error("Snapshot should match current stats") + } + + // Test reset + stats.Reset() + if stats.RuleGetOperations != 0 || stats.RuleSetOperations != 0 || stats.DatabaseOperations != 0 { + t.Error("Reset() should zero all counters") + } +} + +// Test error constants +func TestErrorConstants(t *testing.T) { + errors := []error{ + ErrRuleNotFound, + ErrRuleSetNotFound, + ErrInvalidRuleCategory, + ErrInvalidRuleType, + ErrInvalidRuleValue, + ErrDuplicateRuleSet, + ErrRuleSetNotActive, + ErrGlobalRuleSetNotSet, + ErrZoneRuleSetNotFound, + ErrRuleValueTooLong, + ErrRuleNameTooLong, + } + + for _, err := range errors { + if err == nil { + t.Error("Error constant should not be nil") + } + if err.Error() == "" { + t.Error("Error message should not be empty") + } + } +} + +// Test constants +func TestConstants(t *testing.T) { + // Test some rule categories + if CategoryClient != 0 { + t.Errorf("CategoryClient should be 0, got %d", CategoryClient) + } + if CategoryPlayer != 3 { + t.Errorf("CategoryPlayer should be 3, got %d", CategoryPlayer) + } + + // Test some rule types + if PlayerMaxLevel != 0 { + t.Errorf("PlayerMaxLevel should be 0, got %d", PlayerMaxLevel) + } + + // Test validation constants + if MaxRuleValueLength != 1024 { + t.Errorf("MaxRuleValueLength should be 1024, got %d", MaxRuleValueLength) + } + if MaxRuleCombinedLength != 2048 { + t.Errorf("MaxRuleCombinedLength should be 2048, got %d", MaxRuleCombinedLength) + } + + // Test database constants + if TableRuleSets != "rulesets" { + t.Errorf("TableRuleSets should be 'rulesets', got %s", TableRuleSets) + } + if DefaultRuleSetIDVar != "default_ruleset_id" { + t.Errorf("DefaultRuleSetIDVar should be 'default_ruleset_id', got %s", DefaultRuleSetIDVar) + } +} + +// Test DatabaseService with in-memory SQLite +func TestDatabaseService(t *testing.T) { + // Create in-memory database + conn, err := sqlite.OpenConn(":memory:", 0) + if err != nil { + t.Fatalf("Failed to create in-memory database: %v", err) + } + defer conn.Close() + + ds := NewDatabaseService(conn) + if ds == nil { + t.Fatal("NewDatabaseService() returned nil") + } + + // Test CreateRulesTables + err = ds.CreateRulesTables() + if err != nil { + t.Fatalf("CreateRulesTables() failed: %v", err) + } + + // Test ValidateDatabase + err = ds.ValidateDatabase() + if err != nil { + t.Fatalf("ValidateDatabase() failed after creating tables: %v", err) + } + + // Test SetDefaultRuleSet and GetDefaultRuleSetID + testRuleSetID := int32(42) + err = ds.SetDefaultRuleSet(testRuleSetID) + if err != nil { + t.Fatalf("SetDefaultRuleSet() failed: %v", err) + } + + retrievedID, err := ds.GetDefaultRuleSetID() + if err != nil { + t.Fatalf("GetDefaultRuleSetID() failed: %v", err) + } + + if retrievedID != testRuleSetID { + t.Errorf("Expected rule set ID %d, got %d", testRuleSetID, retrievedID) + } +} + +func TestDatabaseServiceRuleSetOperations(t *testing.T) { + // Create in-memory database + conn, err := sqlite.OpenConn(":memory:", 0) + if err != nil { + t.Fatalf("Failed to create in-memory database: %v", err) + } + defer conn.Close() + + ds := NewDatabaseService(conn) + ds.CreateRulesTables() + + // Create a test rule set + ruleSet := NewRuleSet() + ruleSet.SetID(1) + ruleSet.SetName("Test Rule Set") + + // Add some rules + rule1 := NewRuleWithValues(CategoryPlayer, PlayerMaxLevel, "60", "Player:MaxLevel") + rule2 := NewRuleWithValues(CategoryCombat, CombatMaxRange, "5.0", "Combat:MaxCombatRange") + ruleSet.AddRule(rule1) + ruleSet.AddRule(rule2) + + // Test SaveRuleSet + err = ds.SaveRuleSet(ruleSet) + if err != nil { + t.Fatalf("SaveRuleSet() failed: %v", err) + } + + // Test GetRuleSetList + ruleSets, err := ds.GetRuleSetList() + if err != nil { + t.Fatalf("GetRuleSetList() failed: %v", err) + } + + if len(ruleSets) != 1 { + t.Errorf("Expected 1 rule set, got %d", len(ruleSets)) + } + + if ruleSets[0].ID != 1 { + t.Errorf("Expected rule set ID 1, got %d", ruleSets[0].ID) + } + + if ruleSets[0].Name != "Test Rule Set" { + t.Errorf("Expected rule set name 'Test Rule Set', got %s", ruleSets[0].Name) + } + + // Test DeleteRuleSet + err = ds.DeleteRuleSet(1) + if err != nil { + t.Fatalf("DeleteRuleSet() failed: %v", err) + } + + // Verify deletion + ruleSets, err = ds.GetRuleSetList() + if err != nil { + t.Fatalf("GetRuleSetList() failed after deletion: %v", err) + } + + if len(ruleSets) != 0 { + t.Errorf("Expected 0 rule sets after deletion, got %d", len(ruleSets)) + } +} + +// Test RuleService functionality +func TestRuleService(t *testing.T) { + config := RuleServiceConfig{ + DatabaseEnabled: false, + CacheEnabled: false, + CacheTTL: 3600, + MaxCacheSize: 1024 * 1024, + } + + service := NewRuleService(config) + if service == nil { + t.Fatal("NewRuleService() returned nil") + } + + err := service.Initialize() + if err != nil { + t.Fatalf("Initialize() failed: %v", err) + } + + ruleManager := service.GetRuleManager() + if ruleManager == nil { + t.Fatal("GetRuleManager() returned nil") + } + + adapter := service.GetAdapter(0) + if adapter == nil { + t.Fatal("GetAdapter() returned nil") + } + + err = service.Shutdown() + if err != nil { + t.Fatalf("Shutdown() failed: %v", err) + } +} + +// Test splitCombined helper function +func TestSplitCombined(t *testing.T) { + // Test valid combined string + parts := splitCombined("Player:MaxLevel") + if len(parts) != 2 { + t.Errorf("Expected 2 parts, got %d", len(parts)) + } + if parts[0] != "Player" || parts[1] != "MaxLevel" { + t.Errorf("Expected ['Player', 'MaxLevel'], got %v", parts) + } + + // Test invalid combined string (no colon) + parts = splitCombined("PlayerMaxLevel") + if len(parts) != 1 { + t.Errorf("Expected 1 part for invalid string, got %d", len(parts)) + } + if parts[0] != "PlayerMaxLevel" { + t.Errorf("Expected ['PlayerMaxLevel'], got %v", parts) + } + + // Test empty string + parts = splitCombined("") + if len(parts) != 1 { + t.Errorf("Expected 1 part for empty string, got %d", len(parts)) + } + if parts[0] != "" { + t.Errorf("Expected [''], got %v", parts) + } +} + +// Benchmark tests func BenchmarkRuleAccess(b *testing.B) { ruleManager := NewRuleManager() @@ -421,10 +1166,43 @@ func BenchmarkRuleAccess(b *testing.B) { } } -// Benchmark rule manager creation func BenchmarkRuleManagerCreation(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { _ = NewRuleManager() } } + +func BenchmarkRuleSetAddRule(b *testing.B) { + ruleSet := NewRuleSet() + rules := make([]*Rule, b.N) + + // Pre-create rules + for i := 0; i < b.N; i++ { + rules[i] = NewRuleWithValues(CategoryPlayer, PlayerMaxLevel+RuleType(i), "50", "Player:MaxLevel") + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + ruleSet.AddRule(rules[i]) + } +} + +func BenchmarkRuleTypeConversion(b *testing.B) { + rule := NewRuleWithValues(CategoryPlayer, PlayerMaxLevel, "12345", "Player:MaxLevel") + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = rule.GetInt32() + } +} + +func BenchmarkRuleManagerAdapter(b *testing.B) { + ruleManager := NewRuleManager() + adapter := NewRuleManagerAdapter(ruleManager, 0) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = adapter.GetInt32(CategoryPlayer, PlayerMaxLevel) + } +} \ No newline at end of file