Hallucinate up an xml parser implementation
Who knows if this will work...
This commit is contained in:
447
processor/xml.go
Normal file
447
processor/xml.go
Normal 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)
|
||||||
|
}
|
||||||
346
processor/xml_integration_test.go
Normal file
346
processor/xml_integration_test.go
Normal 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
621
processor/xml_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user