Hallucinate up an xml parser implementation

Who knows if this will work...
This commit is contained in:
2025-12-19 11:36:03 +01:00
parent b309e3e6f0
commit a0c5a5f18c
3 changed files with 1414 additions and 0 deletions

447
processor/xml.go Normal file
View File

@@ -0,0 +1,447 @@
package processor
import (
"cook/utils"
"encoding/xml"
"fmt"
"io"
"sort"
"strconv"
"strings"
logger "git.site.quack-lab.dev/dave/cylogger"
)
var xmlLogger = logger.Default.WithPrefix("processor/xml")
// XMLElement represents a parsed XML element with position tracking
type XMLElement struct {
Tag string
Attributes map[string]XMLAttribute
Text string
Children []*XMLElement
StartPos int64
EndPos int64
TextStart int64
TextEnd int64
}
// XMLAttribute represents an attribute with its position in the source
type XMLAttribute struct {
Value string
ValueStart int64
ValueEnd int64
}
// parseXMLWithPositions parses XML while tracking byte positions of all elements and attributes
func parseXMLWithPositions(content string) (*XMLElement, error) {
decoder := xml.NewDecoder(strings.NewReader(content))
var root *XMLElement
var stack []*XMLElement
var lastPos int64
for {
token, err := decoder.Token()
if err == io.EOF {
break
}
if err != nil {
return nil, fmt.Errorf("failed to parse XML: %v", err)
}
offset := decoder.InputOffset()
switch t := token.(type) {
case xml.StartElement:
// Find the actual start position of this element by searching for "<tagname"
tagSearchPattern := "<" + t.Name.Local
startPos := int64(strings.LastIndex(content[:offset], tagSearchPattern))
element := &XMLElement{
Tag: t.Name.Local,
Attributes: make(map[string]XMLAttribute),
StartPos: startPos,
Children: []*XMLElement{},
}
// Parse attributes - search within the tag boundaries
if len(t.Attr) > 0 {
tagEnd := offset
tagSection := content[startPos:tagEnd]
for _, attr := range t.Attr {
// Find attribute in the tag section: attrname="value"
attrPattern := attr.Name.Local + `="`
attrIdx := strings.Index(tagSection, attrPattern)
if attrIdx >= 0 {
valueStart := startPos + int64(attrIdx) + int64(len(attrPattern))
valueEnd := valueStart + int64(len(attr.Value))
element.Attributes[attr.Name.Local] = XMLAttribute{
Value: attr.Value,
ValueStart: valueStart,
ValueEnd: valueEnd,
}
}
}
}
if len(stack) > 0 {
parent := stack[len(stack)-1]
parent.Children = append(parent.Children, element)
} else {
root = element
}
stack = append(stack, element)
lastPos = offset
case xml.CharData:
rawText := string(t)
text := strings.TrimSpace(rawText)
if len(stack) > 0 && text != "" {
current := stack[len(stack)-1]
current.Text = text
// The text content is between lastPos (after >) and offset (before </)
// Search for the trimmed text within the raw content
textInContent := content[lastPos:offset]
trimmedStart := strings.Index(textInContent, text)
if trimmedStart >= 0 {
current.TextStart = lastPos + int64(trimmedStart)
current.TextEnd = current.TextStart + int64(len(text))
}
}
lastPos = offset
case xml.EndElement:
if len(stack) > 0 {
current := stack[len(stack)-1]
current.EndPos = offset
stack = stack[:len(stack)-1]
}
lastPos = offset
}
}
return root, nil
}
// xmlElementToMap converts XMLElement to a map for comparison
func xmlElementToMap(elem *XMLElement) map[string]interface{} {
result := make(map[string]interface{})
result["_tag"] = elem.Tag
if len(elem.Attributes) > 0 {
attrs := make(map[string]interface{})
for k, v := range elem.Attributes {
attrs[k] = v.Value
}
result["_attr"] = attrs
}
if elem.Text != "" {
result["_text"] = elem.Text
}
if len(elem.Children) > 0 {
children := make([]interface{}, len(elem.Children))
for i, child := range elem.Children {
children[i] = xmlElementToMap(child)
}
result["_children"] = children
}
return result
}
// XMLChange represents a detected difference between original and modified XML structures
type XMLChange struct {
Type string // "text", "attribute", "add_element", "remove_element"
Path string
OldValue string
NewValue string
StartPos int64
EndPos int64
InsertText string
}
func findXMLChanges(original, modified *XMLElement, path string) []XMLChange {
var changes []XMLChange
// Check text content changes
if original.Text != modified.Text {
changes = append(changes, XMLChange{
Type: "text",
Path: path,
OldValue: original.Text,
NewValue: modified.Text,
StartPos: original.TextStart,
EndPos: original.TextEnd,
})
}
// Check attribute changes
for attrName, origAttr := range original.Attributes {
if modAttr, exists := modified.Attributes[attrName]; exists {
if origAttr.Value != modAttr.Value {
changes = append(changes, XMLChange{
Type: "attribute",
Path: path + "/@" + attrName,
OldValue: origAttr.Value,
NewValue: modAttr.Value,
StartPos: origAttr.ValueStart,
EndPos: origAttr.ValueEnd,
})
}
} else {
// Attribute removed
changes = append(changes, XMLChange{
Type: "remove_attribute",
Path: path + "/@" + attrName,
OldValue: origAttr.Value,
StartPos: origAttr.ValueStart - int64(len(attrName)+2), // Include attr=" part
EndPos: origAttr.ValueEnd + 1, // Include closing "
})
}
}
// Check for added attributes
for attrName, modAttr := range modified.Attributes {
if _, exists := original.Attributes[attrName]; !exists {
changes = append(changes, XMLChange{
Type: "add_attribute",
Path: path + "/@" + attrName,
NewValue: modAttr.Value,
StartPos: original.StartPos, // Will be adjusted to insert after tag name
InsertText: fmt.Sprintf(` %s="%s"`, attrName, modAttr.Value),
})
}
}
// Check children recursively
origChildMap := make(map[string][]*XMLElement)
for _, child := range original.Children {
origChildMap[child.Tag] = append(origChildMap[child.Tag], child)
}
modChildMap := make(map[string][]*XMLElement)
for _, child := range modified.Children {
modChildMap[child.Tag] = append(modChildMap[child.Tag], child)
}
// Compare children by tag name
processedTags := make(map[string]bool)
for tag, origChildren := range origChildMap {
processedTags[tag] = true
modChildren := modChildMap[tag]
// Match children by index
maxLen := len(origChildren)
if len(modChildren) > maxLen {
maxLen = len(modChildren)
}
for i := 0; i < maxLen; i++ {
childPath := fmt.Sprintf("%s/%s[%d]", path, tag, i)
if i < len(origChildren) && i < len(modChildren) {
// Both exist, compare recursively
childChanges := findXMLChanges(origChildren[i], modChildren[i], childPath)
changes = append(changes, childChanges...)
} else if i < len(origChildren) {
// Child removed
changes = append(changes, XMLChange{
Type: "remove_element",
Path: childPath,
StartPos: origChildren[i].StartPos,
EndPos: origChildren[i].EndPos,
})
}
}
// Handle added children
if len(modChildren) > len(origChildren) {
for i := len(origChildren); i < len(modChildren); i++ {
childPath := fmt.Sprintf("%s/%s[%d]", path, tag, i)
// Generate XML text for the new element
xmlText := serializeXMLElement(modChildren[i], " ")
changes = append(changes, XMLChange{
Type: "add_element",
Path: childPath,
InsertText: xmlText,
StartPos: original.EndPos - int64(len(original.Tag)+3), // Before closing tag
})
}
}
}
// Handle completely new tag types
for tag, modChildren := range modChildMap {
if !processedTags[tag] {
for i, child := range modChildren {
childPath := fmt.Sprintf("%s/%s[%d]", path, tag, i)
xmlText := serializeXMLElement(child, " ")
changes = append(changes, XMLChange{
Type: "add_element",
Path: childPath,
InsertText: xmlText,
StartPos: original.EndPos - int64(len(original.Tag)+3),
})
}
}
}
return changes
}
// serializeXMLElement converts an XMLElement back to XML text
func serializeXMLElement(elem *XMLElement, indent string) string {
var sb strings.Builder
sb.WriteString(indent)
sb.WriteString("<")
sb.WriteString(elem.Tag)
// Write attributes
attrNames := make([]string, 0, len(elem.Attributes))
for name := range elem.Attributes {
attrNames = append(attrNames, name)
}
sort.Strings(attrNames)
for _, name := range attrNames {
attr := elem.Attributes[name]
sb.WriteString(fmt.Sprintf(` %s="%s"`, name, attr.Value))
}
if elem.Text == "" && len(elem.Children) == 0 {
sb.WriteString(" />")
return sb.String()
}
sb.WriteString(">")
if elem.Text != "" {
sb.WriteString(elem.Text)
}
if len(elem.Children) > 0 {
sb.WriteString("\n")
for _, child := range elem.Children {
sb.WriteString(serializeXMLElement(child, indent+" "))
sb.WriteString("\n")
}
sb.WriteString(indent)
}
sb.WriteString("</")
sb.WriteString(elem.Tag)
sb.WriteString(">")
return sb.String()
}
// applyXMLChanges generates ReplaceCommands from detected XML changes
func applyXMLChanges(changes []XMLChange) []utils.ReplaceCommand {
var commands []utils.ReplaceCommand
for _, change := range changes {
switch change.Type {
case "text":
commands = append(commands, utils.ReplaceCommand{
From: int(change.StartPos),
To: int(change.EndPos),
With: change.NewValue,
})
case "attribute":
commands = append(commands, utils.ReplaceCommand{
From: int(change.StartPos),
To: int(change.EndPos),
With: change.NewValue,
})
case "add_attribute":
// Insert after tag name, before > or />
commands = append(commands, utils.ReplaceCommand{
From: int(change.StartPos),
To: int(change.StartPos),
With: change.InsertText,
})
case "remove_attribute":
commands = append(commands, utils.ReplaceCommand{
From: int(change.StartPos),
To: int(change.EndPos),
With: "",
})
case "add_element":
commands = append(commands, utils.ReplaceCommand{
From: int(change.StartPos),
To: int(change.StartPos),
With: "\n" + change.InsertText,
})
case "remove_element":
commands = append(commands, utils.ReplaceCommand{
From: int(change.StartPos),
To: int(change.EndPos),
With: "",
})
}
}
return commands
}
// modifyXMLElement applies modifications to an XMLElement based on a modification function
func modifyXMLElement(elem *XMLElement, modifyFunc func(*XMLElement)) *XMLElement {
// Deep copy the element
copied := deepCopyXMLElement(elem)
modifyFunc(copied)
return copied
}
// deepCopyXMLElement creates a deep copy of an XMLElement
func deepCopyXMLElement(elem *XMLElement) *XMLElement {
if elem == nil {
return nil
}
copied := &XMLElement{
Tag: elem.Tag,
Text: elem.Text,
StartPos: elem.StartPos,
EndPos: elem.EndPos,
TextStart: elem.TextStart,
TextEnd: elem.TextEnd,
Attributes: make(map[string]XMLAttribute),
Children: make([]*XMLElement, len(elem.Children)),
}
for k, v := range elem.Attributes {
copied.Attributes[k] = v
}
for i, child := range elem.Children {
copied.Children[i] = deepCopyXMLElement(child)
}
return copied
}
// Helper function to parse numeric values
func parseNumeric(s string) (float64, bool) {
if f, err := strconv.ParseFloat(s, 64); err == nil {
return f, true
}
return 0, false
}
// Helper function to format numeric values
func formatNumeric(f float64) string {
if f == float64(int64(f)) {
return strconv.FormatInt(int64(f), 10)
}
return strconv.FormatFloat(f, 'f', -1, 64)
}

View File

@@ -0,0 +1,346 @@
package processor
import (
"strings"
"testing"
"cook/utils"
)
// TestRealWorldGameXML tests with game-like XML structure
func TestRealWorldGameXML(t *testing.T) {
original := `<?xml version="1.0" encoding="utf-8"?>
<Items>
<Item name="Fiber" identifier="Item_Fiber" category="Resource">
<Icon texture="Items/Fiber.png" />
<Weight value="0.01" />
<MaxStack value="1000" />
<Description text="Soft plant fibers useful for crafting." />
</Item>
<Item name="Wood" identifier="Item_Wood" category="Resource">
<Icon texture="Items/Wood.png" />
<Weight value="0.05" />
<MaxStack value="500" />
<Description text="Basic building material." />
</Item>
</Items>`
// Parse
origElem, err := parseXMLWithPositions(original)
if err != nil {
t.Fatalf("Failed to parse: %v", err)
}
// Modify: Double all MaxStack values and change Wood weight
modElem := deepCopyXMLElement(origElem)
// Fiber MaxStack: 1000 → 2000
fiberItem := modElem.Children[0]
fiberMaxStack := fiberItem.Children[2]
valueAttr := fiberMaxStack.Attributes["value"]
valueAttr.Value = "2000"
fiberMaxStack.Attributes["value"] = valueAttr
// Wood MaxStack: 500 → 1000
woodItem := modElem.Children[1]
woodMaxStack := woodItem.Children[2]
valueAttr2 := woodMaxStack.Attributes["value"]
valueAttr2.Value = "1000"
woodMaxStack.Attributes["value"] = valueAttr2
// Wood Weight: 0.05 → 0.10
woodWeight := woodItem.Children[1]
weightAttr := woodWeight.Attributes["value"]
weightAttr.Value = "0.10"
woodWeight.Attributes["value"] = weightAttr
// Generate changes
changes := findXMLChanges(origElem, modElem, "")
if len(changes) != 3 {
t.Fatalf("Expected 3 changes, got %d", len(changes))
}
// Apply
commands := applyXMLChanges(changes)
result, _ := utils.ExecuteModifications(commands, original)
// Verify changes
if !strings.Contains(result, `<MaxStack value="2000"`) {
t.Errorf("Failed to update Fiber MaxStack")
}
if !strings.Contains(result, `<MaxStack value="1000"`) {
t.Errorf("Failed to update Wood MaxStack")
}
if !strings.Contains(result, `<Weight value="0.10"`) {
t.Errorf("Failed to update Wood Weight")
}
// Verify formatting preserved (check XML declaration and indentation)
if !strings.HasPrefix(result, `<?xml version="1.0" encoding="utf-8"?>`) {
t.Errorf("XML declaration not preserved")
}
if !strings.Contains(result, "\n <Item") {
t.Errorf("Indentation not preserved")
}
}
// TestAddRemoveMultipleChildren tests adding and removing multiple elements
func TestAddRemoveMultipleChildren(t *testing.T) {
original := `<inventory>
<item name="sword" />
<item name="shield" />
<item name="potion" />
<item name="scroll" />
</inventory>`
// Parse
origElem, err := parseXMLWithPositions(original)
if err != nil {
t.Fatalf("Failed to parse: %v", err)
}
// Remove middle two items, add a new one
modElem := deepCopyXMLElement(origElem)
// Remove shield and potion (indices 1 and 2)
modElem.Children = []*XMLElement{
modElem.Children[0], // sword
modElem.Children[3], // scroll
}
// Add a new item
newItem := &XMLElement{
Tag: "item",
Attributes: map[string]XMLAttribute{
"name": {Value: "helmet"},
},
Children: []*XMLElement{},
}
modElem.Children = append(modElem.Children, newItem)
// Generate changes
changes := findXMLChanges(origElem, modElem, "")
// The algorithm compares by matching indices:
// orig[0]=sword vs mod[0]=sword (no change)
// orig[1]=shield vs mod[1]=scroll (treated as replace - shows as attribute changes)
// orig[2]=potion vs mod[2]=helmet (treated as replace)
// orig[3]=scroll (removed)
// This is fine - the actual edits will be correct
if len(changes) == 0 {
t.Fatalf("Expected changes, got none")
}
// Apply
commands := applyXMLChanges(changes)
result, _ := utils.ExecuteModifications(commands, original)
// Verify
if strings.Contains(result, `name="shield"`) {
t.Errorf("Shield not removed")
}
if strings.Contains(result, `name="potion"`) {
t.Errorf("Potion not removed")
}
if !strings.Contains(result, `name="sword"`) {
t.Errorf("Sword incorrectly removed")
}
if !strings.Contains(result, `name="scroll"`) {
t.Errorf("Scroll incorrectly removed")
}
if !strings.Contains(result, `name="helmet"`) {
t.Errorf("Helmet not added")
}
}
// TestModifyAttributesAndText tests changing both attributes and text content
func TestModifyAttributesAndText(t *testing.T) {
original := `<weapon>
<item type="sword" damage="10">Iron Sword</item>
<item type="axe" damage="15">Battle Axe</item>
</weapon>`
// Parse
origElem, err := parseXMLWithPositions(original)
if err != nil {
t.Fatalf("Failed to parse: %v", err)
}
// Modify both items
modElem := deepCopyXMLElement(origElem)
// First item: change damage and text
item1 := modElem.Children[0]
dmgAttr := item1.Attributes["damage"]
dmgAttr.Value = "20"
item1.Attributes["damage"] = dmgAttr
item1.Text = "Steel Sword"
// Second item: change damage and type
item2 := modElem.Children[1]
dmgAttr2 := item2.Attributes["damage"]
dmgAttr2.Value = "30"
item2.Attributes["damage"] = dmgAttr2
typeAttr := item2.Attributes["type"]
typeAttr.Value = "greataxe"
item2.Attributes["type"] = typeAttr
// Generate and apply changes
changes := findXMLChanges(origElem, modElem, "")
commands := applyXMLChanges(changes)
result, _ := utils.ExecuteModifications(commands, original)
// Verify
if !strings.Contains(result, `damage="20"`) {
t.Errorf("First item damage not updated")
}
if !strings.Contains(result, "Steel Sword") {
t.Errorf("First item text not updated")
}
if !strings.Contains(result, `damage="30"`) {
t.Errorf("Second item damage not updated")
}
if !strings.Contains(result, `type="greataxe"`) {
t.Errorf("Second item type not updated")
}
if strings.Contains(result, "Iron Sword") {
t.Errorf("Old text still present")
}
}
// TestSelfClosingTagPreservation tests that self-closing tags work correctly
func TestSelfClosingTagPreservation(t *testing.T) {
original := `<root>
<item name="test" />
<empty></empty>
</root>`
// Parse
origElem, err := parseXMLWithPositions(original)
if err != nil {
t.Fatalf("Failed to parse: %v", err)
}
// Modify first item's attribute
modElem := deepCopyXMLElement(origElem)
item := modElem.Children[0]
nameAttr := item.Attributes["name"]
nameAttr.Value = "modified"
item.Attributes["name"] = nameAttr
// Generate and apply changes
changes := findXMLChanges(origElem, modElem, "")
commands := applyXMLChanges(changes)
result, _ := utils.ExecuteModifications(commands, original)
// Verify the change was made
if !strings.Contains(result, `name="modified"`) {
t.Errorf("Attribute not updated: %s", result)
}
}
// TestNumericAttributeModification tests numeric attribute changes
func TestNumericAttributeModification(t *testing.T) {
original := `<stats health="100" mana="50" stamina="75.5" />`
// Parse
origElem, err := parseXMLWithPositions(original)
if err != nil {
t.Fatalf("Failed to parse: %v", err)
}
// Double all numeric values
modElem := deepCopyXMLElement(origElem)
// Helper to modify numeric attributes
modifyNumericAttr := func(attrName string, multiplier float64) {
if attr, exists := modElem.Attributes[attrName]; exists {
if val, ok := parseNumeric(attr.Value); ok {
attr.Value = formatNumeric(val * multiplier)
modElem.Attributes[attrName] = attr
}
}
}
modifyNumericAttr("health", 2.0)
modifyNumericAttr("mana", 2.0)
modifyNumericAttr("stamina", 2.0)
// Generate and apply changes
changes := findXMLChanges(origElem, modElem, "")
if len(changes) != 3 {
t.Fatalf("Expected 3 changes, got %d", len(changes))
}
commands := applyXMLChanges(changes)
result, _ := utils.ExecuteModifications(commands, original)
// Verify numeric changes
if !strings.Contains(result, `health="200"`) {
t.Errorf("Health not doubled: %s", result)
}
if !strings.Contains(result, `mana="100"`) {
t.Errorf("Mana not doubled: %s", result)
}
if !strings.Contains(result, `stamina="151"`) {
t.Errorf("Stamina not doubled: %s", result)
}
}
// TestMinimalGitDiff verifies that only changed parts are modified
func TestMinimalGitDiff(t *testing.T) {
original := `<config>
<setting name="volume" value="50" />
<setting name="brightness" value="75" />
<setting name="contrast" value="100" />
</config>`
// Parse
origElem, err := parseXMLWithPositions(original)
if err != nil {
t.Fatalf("Failed to parse: %v", err)
}
// Change only brightness
modElem := deepCopyXMLElement(origElem)
brightnessItem := modElem.Children[1]
valueAttr := brightnessItem.Attributes["value"]
valueAttr.Value = "90"
brightnessItem.Attributes["value"] = valueAttr
// Generate changes
changes := findXMLChanges(origElem, modElem, "")
// Should be exactly 1 change
if len(changes) != 1 {
t.Fatalf("Expected exactly 1 change for minimal diff, got %d", len(changes))
}
if changes[0].OldValue != "75" || changes[0].NewValue != "90" {
t.Errorf("Wrong change detected: %v", changes[0])
}
// Apply
commands := applyXMLChanges(changes)
result, _ := utils.ExecuteModifications(commands, original)
// Calculate diff size (rough approximation)
diffChars := len(changes[0].OldValue) + len(changes[0].NewValue)
if diffChars > 10 {
t.Errorf("Diff too large: %d characters changed (expected < 10)", diffChars)
}
// Verify only brightness changed
if !strings.Contains(result, `value="50"`) {
t.Errorf("Volume incorrectly modified")
}
if !strings.Contains(result, `value="90"`) {
t.Errorf("Brightness not modified")
}
if !strings.Contains(result, `value="100"`) {
t.Errorf("Contrast incorrectly modified")
}
}

621
processor/xml_test.go Normal file
View File

@@ -0,0 +1,621 @@
package processor
import (
"strings"
"testing"
"cook/utils"
)
func TestParseXMLWithPositions(t *testing.T) {
xml := `<root><item name="test">Hello</item></root>`
elem, err := parseXMLWithPositions(xml)
if err != nil {
t.Fatalf("Failed to parse XML: %v", err)
}
if elem.Tag != "root" {
t.Errorf("Expected root tag 'root', got '%s'", elem.Tag)
}
if len(elem.Children) != 1 {
t.Fatalf("Expected 1 child, got %d", len(elem.Children))
}
child := elem.Children[0]
if child.Tag != "item" {
t.Errorf("Expected child tag 'item', got '%s'", child.Tag)
}
if child.Attributes["name"].Value != "test" {
t.Errorf("Expected attribute 'name' to be 'test', got '%s'", child.Attributes["name"].Value)
}
if child.Text != "Hello" {
t.Errorf("Expected text 'Hello', got '%s'", child.Text)
}
}
func TestSurgicalTextChange(t *testing.T) {
original := `<root>
<item name="sword" weight="10">A sword</item>
</root>`
expected := `<root>
<item name="sword" weight="10">A modified sword</item>
</root>`
// Parse original
origElem, err := parseXMLWithPositions(original)
if err != nil {
t.Fatalf("Failed to parse original XML: %v", err)
}
// Create modified version
modElem := deepCopyXMLElement(origElem)
modElem.Children[0].Text = "A modified sword"
// Find changes
changes := findXMLChanges(origElem, modElem, "")
if len(changes) != 1 {
t.Fatalf("Expected 1 change, got %d", len(changes))
}
if changes[0].Type != "text" {
t.Errorf("Expected change type 'text', got '%s'", changes[0].Type)
}
// Apply changes
commands := applyXMLChanges(changes)
result, _ := utils.ExecuteModifications(commands, original)
if result != expected {
t.Errorf("Text change failed.\nExpected:\n%s\n\nGot:\n%s", expected, result)
}
}
func TestSurgicalAttributeChange(t *testing.T) {
original := `<root>
<item name="sword" weight="10" />
</root>`
expected := `<root>
<item name="sword" weight="20" />
</root>`
// Parse original
origElem, err := parseXMLWithPositions(original)
if err != nil {
t.Fatalf("Failed to parse original XML: %v", err)
}
// Create modified version
modElem := deepCopyXMLElement(origElem)
attr := modElem.Children[0].Attributes["weight"]
attr.Value = "20"
modElem.Children[0].Attributes["weight"] = attr
// Find changes
changes := findXMLChanges(origElem, modElem, "")
if len(changes) != 1 {
t.Fatalf("Expected 1 change, got %d", len(changes))
}
if changes[0].Type != "attribute" {
t.Errorf("Expected change type 'attribute', got '%s'", changes[0].Type)
}
// Apply changes
commands := applyXMLChanges(changes)
result, _ := utils.ExecuteModifications(commands, original)
if result != expected {
t.Errorf("Attribute change failed.\nExpected:\n%s\n\nGot:\n%s", expected, result)
}
}
func TestSurgicalMultipleAttributeChanges(t *testing.T) {
original := `<item name="sword" weight="10" damage="5" />`
expected := `<item name="greatsword" weight="20" damage="15" />`
// Parse original
origElem, err := parseXMLWithPositions(original)
if err != nil {
t.Fatalf("Failed to parse original XML: %v", err)
}
// Create modified version
modElem := deepCopyXMLElement(origElem)
nameAttr := modElem.Attributes["name"]
nameAttr.Value = "greatsword"
modElem.Attributes["name"] = nameAttr
weightAttr := modElem.Attributes["weight"]
weightAttr.Value = "20"
modElem.Attributes["weight"] = weightAttr
damageAttr := modElem.Attributes["damage"]
damageAttr.Value = "15"
modElem.Attributes["damage"] = damageAttr
// Find changes
changes := findXMLChanges(origElem, modElem, "")
if len(changes) != 3 {
t.Fatalf("Expected 3 changes, got %d", len(changes))
}
// Apply changes
commands := applyXMLChanges(changes)
result, _ := utils.ExecuteModifications(commands, original)
if result != expected {
t.Errorf("Multiple attribute changes failed.\nExpected:\n%s\n\nGot:\n%s", expected, result)
}
}
func TestSurgicalAddAttribute(t *testing.T) {
original := `<item name="sword" />`
// Parse original
origElem, err := parseXMLWithPositions(original)
if err != nil {
t.Fatalf("Failed to parse original XML: %v", err)
}
// Create modified version with new attribute
modElem := deepCopyXMLElement(origElem)
modElem.Attributes["weight"] = XMLAttribute{
Value: "10",
}
// Find changes
changes := findXMLChanges(origElem, modElem, "")
if len(changes) != 1 {
t.Fatalf("Expected 1 change, got %d", len(changes))
}
if changes[0].Type != "add_attribute" {
t.Errorf("Expected change type 'add_attribute', got '%s'", changes[0].Type)
}
// Apply changes
commands := applyXMLChanges(changes)
result, _ := utils.ExecuteModifications(commands, original)
// Should contain the new attribute
if !strings.Contains(result, `weight="10"`) {
t.Errorf("Add attribute failed. Result doesn't contain weight=\"10\":\n%s", result)
}
}
func TestSurgicalRemoveAttribute(t *testing.T) {
original := `<item name="sword" weight="10" damage="5" />`
// Parse original
origElem, err := parseXMLWithPositions(original)
if err != nil {
t.Fatalf("Failed to parse original XML: %v", err)
}
// Create modified version without weight attribute
modElem := deepCopyXMLElement(origElem)
delete(modElem.Attributes, "weight")
// Find changes
changes := findXMLChanges(origElem, modElem, "")
if len(changes) != 1 {
t.Fatalf("Expected 1 change, got %d", len(changes))
}
if changes[0].Type != "remove_attribute" {
t.Errorf("Expected change type 'remove_attribute', got '%s'", changes[0].Type)
}
// Apply changes
commands := applyXMLChanges(changes)
result, _ := utils.ExecuteModifications(commands, original)
// Should not contain weight attribute
if strings.Contains(result, "weight=") {
t.Errorf("Remove attribute failed. Result still contains 'weight=':\n%s", result)
}
// Should still contain other attributes
if !strings.Contains(result, `name="sword"`) {
t.Errorf("Remove attribute incorrectly removed other attributes:\n%s", result)
}
}
func TestSurgicalAddElement(t *testing.T) {
original := `<root>
<item name="sword" />
</root>`
// Parse original
origElem, err := parseXMLWithPositions(original)
if err != nil {
t.Fatalf("Failed to parse original XML: %v", err)
}
// Create modified version with new child
modElem := deepCopyXMLElement(origElem)
newChild := &XMLElement{
Tag: "item",
Attributes: map[string]XMLAttribute{
"name": {Value: "shield"},
},
Children: []*XMLElement{},
}
modElem.Children = append(modElem.Children, newChild)
// Find changes
changes := findXMLChanges(origElem, modElem, "")
if len(changes) != 1 {
t.Fatalf("Expected 1 change, got %d", len(changes))
}
if changes[0].Type != "add_element" {
t.Errorf("Expected change type 'add_element', got '%s'", changes[0].Type)
}
// Apply changes
commands := applyXMLChanges(changes)
result, _ := utils.ExecuteModifications(commands, original)
// Should contain the new element
if !strings.Contains(result, `<item name="shield"`) {
t.Errorf("Add element failed. Result doesn't contain new item:\n%s", result)
}
}
func TestSurgicalRemoveElement(t *testing.T) {
original := `<root>
<item name="sword" />
<item name="shield" />
</root>`
expected := `<root>
<item name="sword" />
</root>`
// Parse original
origElem, err := parseXMLWithPositions(original)
if err != nil {
t.Fatalf("Failed to parse original XML: %v", err)
}
// Create modified version without second child
modElem := deepCopyXMLElement(origElem)
modElem.Children = modElem.Children[:1]
// Find changes
changes := findXMLChanges(origElem, modElem, "")
if len(changes) != 1 {
t.Fatalf("Expected 1 change, got %d", len(changes))
}
if changes[0].Type != "remove_element" {
t.Errorf("Expected change type 'remove_element', got '%s'", changes[0].Type)
}
// Apply changes
commands := applyXMLChanges(changes)
result, _ := utils.ExecuteModifications(commands, original)
// Should not contain shield
if strings.Contains(result, "shield") {
t.Errorf("Remove element failed. Result still contains 'shield':\n%s", result)
}
// Should still contain sword
if !strings.Contains(result, "sword") {
t.Errorf("Remove element incorrectly removed other elements:\n%s", result)
}
// Normalize whitespace for comparison
resultNorm := strings.TrimSpace(result)
expectedNorm := strings.TrimSpace(expected)
if resultNorm != expectedNorm {
t.Errorf("Remove element result mismatch.\nExpected:\n%s\n\nGot:\n%s", expectedNorm, resultNorm)
}
}
func TestComplexNestedChanges(t *testing.T) {
original := `<root>
<inventory>
<item name="sword" weight="10">
<stats damage="5" speed="3" />
</item>
<item name="shield" weight="8">
<stats defense="7" />
</item>
</inventory>
</root>`
// Parse original
origElem, err := parseXMLWithPositions(original)
if err != nil {
t.Fatalf("Failed to parse original XML: %v", err)
}
// Create modified version with multiple changes
modElem := deepCopyXMLElement(origElem)
// Change first item's weight
inventory := modElem.Children[0]
item1 := inventory.Children[0]
weightAttr := item1.Attributes["weight"]
weightAttr.Value = "20"
item1.Attributes["weight"] = weightAttr
// Change nested stats damage
stats := item1.Children[0]
damageAttr := stats.Attributes["damage"]
damageAttr.Value = "10"
stats.Attributes["damage"] = damageAttr
// Change second item's name
item2 := inventory.Children[1]
nameAttr := item2.Attributes["name"]
nameAttr.Value = "buckler"
item2.Attributes["name"] = nameAttr
// Find changes
changes := findXMLChanges(origElem, modElem, "")
// Should have 3 changes: weight, damage, name
if len(changes) != 3 {
t.Fatalf("Expected 3 changes, got %d: %+v", len(changes), changes)
}
// Apply changes
commands := applyXMLChanges(changes)
result, _ := utils.ExecuteModifications(commands, original)
// Verify all changes were applied
if !strings.Contains(result, `weight="20"`) {
t.Errorf("Failed to update weight to 20:\n%s", result)
}
if !strings.Contains(result, `damage="10"`) {
t.Errorf("Failed to update damage to 10:\n%s", result)
}
if !strings.Contains(result, `name="buckler"`) {
t.Errorf("Failed to update name to buckler:\n%s", result)
}
// Verify unchanged elements remain
if !strings.Contains(result, `speed="3"`) {
t.Errorf("Incorrectly modified speed attribute:\n%s", result)
}
if !strings.Contains(result, `defense="7"`) {
t.Errorf("Incorrectly modified defense attribute:\n%s", result)
}
}
func TestFormattingPreservation(t *testing.T) {
original := `<root>
<item name="sword" weight="10">
<description>A sharp blade</description>
<stats damage="5" speed="3" />
</item>
</root>`
expected := `<root>
<item name="sword" weight="20">
<description>A sharp blade</description>
<stats damage="5" speed="3" />
</item>
</root>`
// Parse original
origElem, err := parseXMLWithPositions(original)
if err != nil {
t.Fatalf("Failed to parse original XML: %v", err)
}
// Modify only weight
modElem := deepCopyXMLElement(origElem)
item := modElem.Children[0]
weightAttr := item.Attributes["weight"]
weightAttr.Value = "20"
item.Attributes["weight"] = weightAttr
// Find changes
changes := findXMLChanges(origElem, modElem, "")
// Apply changes
commands := applyXMLChanges(changes)
result, _ := utils.ExecuteModifications(commands, original)
if result != expected {
t.Errorf("Formatting preservation failed.\nExpected:\n%s\n\nGot:\n%s", expected, result)
}
}
func TestNumericHelpers(t *testing.T) {
tests := []struct {
input string
expected float64
isNum bool
}{
{"42", 42.0, true},
{"3.14", 3.14, true},
{"0", 0.0, true},
{"-5", -5.0, true},
{"abc", 0.0, false},
{"", 0.0, false},
}
for _, tt := range tests {
val, ok := parseNumeric(tt.input)
if ok != tt.isNum {
t.Errorf("parseNumeric(%q) isNum = %v, expected %v", tt.input, ok, tt.isNum)
}
if ok && val != tt.expected {
t.Errorf("parseNumeric(%q) = %v, expected %v", tt.input, val, tt.expected)
}
}
// Test formatting
formatTests := []struct {
input float64
expected string
}{
{42.0, "42"},
{3.14, "3.14"},
{0.0, "0"},
{-5.0, "-5"},
{100.5, "100.5"},
}
for _, tt := range formatTests {
result := formatNumeric(tt.input)
if result != tt.expected {
t.Errorf("formatNumeric(%v) = %q, expected %q", tt.input, result, tt.expected)
}
}
}
func TestDeepCopyXMLElement(t *testing.T) {
original := &XMLElement{
Tag: "item",
Text: "content",
Attributes: map[string]XMLAttribute{
"name": {Value: "sword"},
},
Children: []*XMLElement{
{Tag: "child", Text: "text"},
},
}
copied := deepCopyXMLElement(original)
// Verify copy is equal
if copied.Tag != original.Tag {
t.Errorf("Tag not copied correctly")
}
if copied.Text != original.Text {
t.Errorf("Text not copied correctly")
}
// Modify copy
copied.Tag = "modified"
copied.Attributes["name"] = XMLAttribute{Value: "shield"}
copied.Children[0].Text = "modified text"
// Verify original unchanged
if original.Tag != "item" {
t.Errorf("Original was modified")
}
if original.Attributes["name"].Value != "sword" {
t.Errorf("Original attributes were modified")
}
if original.Children[0].Text != "text" {
t.Errorf("Original children were modified")
}
}
func TestSerializeXMLElement(t *testing.T) {
elem := &XMLElement{
Tag: "item",
Attributes: map[string]XMLAttribute{
"name": {Value: "sword"},
"weight": {Value: "10"},
},
Children: []*XMLElement{
{
Tag: "stats",
Attributes: map[string]XMLAttribute{
"damage": {Value: "5"},
},
Children: []*XMLElement{},
},
},
}
result := serializeXMLElement(elem, "")
// Check it contains expected parts
if !strings.Contains(result, "<item") {
t.Errorf("Missing opening tag")
}
if !strings.Contains(result, "</item>") {
t.Errorf("Missing closing tag")
}
if !strings.Contains(result, `name="sword"`) {
t.Errorf("Missing name attribute")
}
if !strings.Contains(result, `weight="10"`) {
t.Errorf("Missing weight attribute")
}
if !strings.Contains(result, "<stats") {
t.Errorf("Missing child element")
}
}
func TestEmptyElements(t *testing.T) {
original := `<root>
<item name="sword" />
<item name="shield"></item>
</root>`
// Parse
elem, err := parseXMLWithPositions(original)
if err != nil {
t.Fatalf("Failed to parse XML: %v", err)
}
if len(elem.Children) != 2 {
t.Errorf("Expected 2 children, got %d", len(elem.Children))
}
// Both should be parsed correctly
if elem.Children[0].Tag != "item" {
t.Errorf("First child tag incorrect")
}
if elem.Children[1].Tag != "item" {
t.Errorf("Second child tag incorrect")
}
}
func TestAttributeOrderPreservation(t *testing.T) {
original := `<item name="sword" weight="10" damage="5" speed="3" />`
// Parse original
origElem, err := parseXMLWithPositions(original)
if err != nil {
t.Fatalf("Failed to parse original XML: %v", err)
}
// Modify just weight
modElem := deepCopyXMLElement(origElem)
weightAttr := modElem.Attributes["weight"]
weightAttr.Value = "20"
modElem.Attributes["weight"] = weightAttr
// Find and apply changes
changes := findXMLChanges(origElem, modElem, "")
commands := applyXMLChanges(changes)
result, _ := utils.ExecuteModifications(commands, original)
// Verify attribute order is preserved (weight comes before damage and speed)
weightIdx := strings.Index(result, "weight=")
damageIdx := strings.Index(result, "damage=")
speedIdx := strings.Index(result, "speed=")
if weightIdx > damageIdx || damageIdx > speedIdx {
t.Errorf("Attribute order not preserved:\n%s", result)
}
}