546 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			546 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package xpath
 | 
						|
 | 
						|
import (
 | 
						|
	"reflect"
 | 
						|
	"testing"
 | 
						|
)
 | 
						|
 | 
						|
// XML test data as a string for our tests
 | 
						|
var testXML = `
 | 
						|
<store>
 | 
						|
  <book category="fiction">
 | 
						|
    <title lang="en">The Fellowship of the Ring</title>
 | 
						|
    <author>J.R.R. Tolkien</author>
 | 
						|
    <year>1954</year>
 | 
						|
    <price>22.99</price>
 | 
						|
  </book>
 | 
						|
  <book category="fiction">
 | 
						|
    <title lang="en">The Two Towers</title>
 | 
						|
    <author>J.R.R. Tolkien</author>
 | 
						|
    <year>1954</year>
 | 
						|
    <price>23.45</price>
 | 
						|
  </book>
 | 
						|
  <book category="technical">
 | 
						|
    <title lang="en">Learning XML</title>
 | 
						|
    <author>Erik T. Ray</author>
 | 
						|
    <year>2003</year>
 | 
						|
    <price>39.95</price>
 | 
						|
  </book>
 | 
						|
  <bicycle>
 | 
						|
    <color>red</color>
 | 
						|
    <price>199.95</price>
 | 
						|
  </bicycle>
 | 
						|
</store>
 | 
						|
`
 | 
						|
 | 
						|
func TestParser(t *testing.T) {
 | 
						|
	tests := []struct {
 | 
						|
		path    string
 | 
						|
		steps   []XPathStep
 | 
						|
		wantErr bool
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			path: "/store/bicycle/color",
 | 
						|
			steps: []XPathStep{
 | 
						|
				{Type: RootStep},
 | 
						|
				{Type: ChildStep, Name: "store"},
 | 
						|
				{Type: ChildStep, Name: "bicycle"},
 | 
						|
				{Type: ChildStep, Name: "color"},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			path: "//price",
 | 
						|
			steps: []XPathStep{
 | 
						|
				{Type: RootStep},
 | 
						|
				{Type: RecursiveDescentStep, Name: "price"},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			path: "/store/book/*",
 | 
						|
			steps: []XPathStep{
 | 
						|
				{Type: RootStep},
 | 
						|
				{Type: ChildStep, Name: "store"},
 | 
						|
				{Type: ChildStep, Name: "book"},
 | 
						|
				{Type: WildcardStep},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			path: "/store/book[1]/title",
 | 
						|
			steps: []XPathStep{
 | 
						|
				{Type: RootStep},
 | 
						|
				{Type: ChildStep, Name: "store"},
 | 
						|
				{Type: ChildStep, Name: "book"},
 | 
						|
				{Type: PredicateStep, Predicate: &Predicate{Type: IndexPredicate, Index: 1}},
 | 
						|
				{Type: ChildStep, Name: "title"},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			path: "//title[@lang]",
 | 
						|
			steps: []XPathStep{
 | 
						|
				{Type: RootStep},
 | 
						|
				{Type: RecursiveDescentStep, Name: "title"},
 | 
						|
				{Type: PredicateStep, Predicate: &Predicate{Type: AttributeExistsPredicate, Attribute: "lang"}},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			path: "//title[@lang='en']",
 | 
						|
			steps: []XPathStep{
 | 
						|
				{Type: RootStep},
 | 
						|
				{Type: RecursiveDescentStep, Name: "title"},
 | 
						|
				{Type: PredicateStep, Predicate: &Predicate{
 | 
						|
					Type:      AttributeEqualsPredicate,
 | 
						|
					Attribute: "lang",
 | 
						|
					Value:     "en",
 | 
						|
				}},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			path: "/store/book[price>35.00]/title",
 | 
						|
			steps: []XPathStep{
 | 
						|
				{Type: RootStep},
 | 
						|
				{Type: ChildStep, Name: "store"},
 | 
						|
				{Type: ChildStep, Name: "book"},
 | 
						|
				{Type: PredicateStep, Predicate: &Predicate{
 | 
						|
					Type:       ComparisonPredicate,
 | 
						|
					Expression: "price>35.00",
 | 
						|
				}},
 | 
						|
				{Type: ChildStep, Name: "title"},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			path: "/store/book[last()]",
 | 
						|
			steps: []XPathStep{
 | 
						|
				{Type: RootStep},
 | 
						|
				{Type: ChildStep, Name: "store"},
 | 
						|
				{Type: ChildStep, Name: "book"},
 | 
						|
				{Type: PredicateStep, Predicate: &Predicate{Type: LastPredicate}},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			path: "/store/book[last()-1]",
 | 
						|
			steps: []XPathStep{
 | 
						|
				{Type: RootStep},
 | 
						|
				{Type: ChildStep, Name: "store"},
 | 
						|
				{Type: ChildStep, Name: "book"},
 | 
						|
				{Type: PredicateStep, Predicate: &Predicate{
 | 
						|
					Type:   LastMinusPredicate,
 | 
						|
					Offset: 1,
 | 
						|
				}},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			path: "/store/book[position()<3]",
 | 
						|
			steps: []XPathStep{
 | 
						|
				{Type: RootStep},
 | 
						|
				{Type: ChildStep, Name: "store"},
 | 
						|
				{Type: ChildStep, Name: "book"},
 | 
						|
				{Type: PredicateStep, Predicate: &Predicate{
 | 
						|
					Type:       PositionPredicate,
 | 
						|
					Expression: "position()<3",
 | 
						|
				}},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			path:    "invalid/path",
 | 
						|
			wantErr: true,
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	for _, tt := range tests {
 | 
						|
		t.Run(tt.path, func(t *testing.T) {
 | 
						|
			steps, err := ParseXPath(tt.path)
 | 
						|
			if (err != nil) != tt.wantErr {
 | 
						|
				t.Fatalf("ParseXPath() error = %v, wantErr %v", err, tt.wantErr)
 | 
						|
			}
 | 
						|
			if !tt.wantErr && !reflect.DeepEqual(steps, tt.steps) {
 | 
						|
				t.Errorf("ParseXPath() steps = %+v, want %+v", steps, tt.steps)
 | 
						|
			}
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestEvaluator(t *testing.T) {
 | 
						|
	tests := []struct {
 | 
						|
		name     string
 | 
						|
		path     string
 | 
						|
		expected []XMLNode
 | 
						|
		error    bool
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			name: "simple_element_access",
 | 
						|
			path: "/store/bicycle/color",
 | 
						|
			expected: []XMLNode{
 | 
						|
				{Value: "red", Path: "/store/bicycle/color"},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "recursive_element_access",
 | 
						|
			path: "//price",
 | 
						|
			expected: []XMLNode{
 | 
						|
				{Value: "22.99", Path: "/store/book[1]/price"},
 | 
						|
				{Value: "23.45", Path: "/store/book[2]/price"},
 | 
						|
				{Value: "39.95", Path: "/store/book[3]/price"},
 | 
						|
				{Value: "199.95", Path: "/store/bicycle/price"},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "wildcard_element_access",
 | 
						|
			path: "/store/book[1]/*",
 | 
						|
			expected: []XMLNode{
 | 
						|
				{Value: "The Fellowship of the Ring", Path: "/store/book[1]/title"},
 | 
						|
				{Value: "J.R.R. Tolkien", Path: "/store/book[1]/author"},
 | 
						|
				{Value: "1954", Path: "/store/book[1]/year"},
 | 
						|
				{Value: "22.99", Path: "/store/book[1]/price"},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "indexed_element_access",
 | 
						|
			path: "/store/book[1]/title",
 | 
						|
			expected: []XMLNode{
 | 
						|
				{Value: "The Fellowship of the Ring", Path: "/store/book[1]/title"},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "attribute_exists_predicate",
 | 
						|
			path: "//title[@lang]",
 | 
						|
			expected: []XMLNode{
 | 
						|
				{Value: "The Fellowship of the Ring", Path: "/store/book[1]/title"},
 | 
						|
				{Value: "The Two Towers", Path: "/store/book[2]/title"},
 | 
						|
				{Value: "Learning XML", Path: "/store/book[3]/title"},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "attribute_equals_predicate",
 | 
						|
			path: "//title[@lang='en']",
 | 
						|
			expected: []XMLNode{
 | 
						|
				{Value: "The Fellowship of the Ring", Path: "/store/book[1]/title"},
 | 
						|
				{Value: "The Two Towers", Path: "/store/book[2]/title"},
 | 
						|
				{Value: "Learning XML", Path: "/store/book[3]/title"},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "value_comparison_predicate",
 | 
						|
			path: "/store/book[price>35.00]/title",
 | 
						|
			expected: []XMLNode{
 | 
						|
				{Value: "Learning XML", Path: "/store/book[3]/title"},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "last_predicate",
 | 
						|
			path: "/store/book[last()]/title",
 | 
						|
			expected: []XMLNode{
 | 
						|
				{Value: "Learning XML", Path: "/store/book[3]/title"},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "last_minus_predicate",
 | 
						|
			path: "/store/book[last()-1]/title",
 | 
						|
			expected: []XMLNode{
 | 
						|
				{Value: "The Two Towers", Path: "/store/book[2]/title"},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "position_predicate",
 | 
						|
			path: "/store/book[position()<3]/title",
 | 
						|
			expected: []XMLNode{
 | 
						|
				{Value: "The Fellowship of the Ring", Path: "/store/book[1]/title"},
 | 
						|
				{Value: "The Two Towers", Path: "/store/book[2]/title"},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:     "all_elements",
 | 
						|
			path:     "//*",
 | 
						|
			expected: []XMLNode{
 | 
						|
				// For brevity, we'll just check the count, not all values
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:     "invalid_index",
 | 
						|
			path:     "/store/book[10]/title",
 | 
						|
			expected: []XMLNode{},
 | 
						|
			error:    true,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:     "nonexistent_element",
 | 
						|
			path:     "/store/nonexistent",
 | 
						|
			expected: []XMLNode{},
 | 
						|
			error:    true,
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	for _, tt := range tests {
 | 
						|
		t.Run(tt.name, func(t *testing.T) {
 | 
						|
			result, err := Get(testXML, tt.path)
 | 
						|
			if err != nil {
 | 
						|
				if !tt.error {
 | 
						|
					t.Errorf("Get() returned error: %v", err)
 | 
						|
				}
 | 
						|
				return
 | 
						|
			}
 | 
						|
 | 
						|
			// Special handling for the "//*" test case
 | 
						|
			if tt.path == "//*" {
 | 
						|
				// Just check that we got multiple elements, not the specific count
 | 
						|
				if len(result) < 10 { // We expect at least 10 elements
 | 
						|
					t.Errorf("Expected multiple elements for '//*', got %d", len(result))
 | 
						|
				}
 | 
						|
				return
 | 
						|
			}
 | 
						|
 | 
						|
			if len(result) != len(tt.expected) {
 | 
						|
				t.Errorf("Expected %d items, got %d", len(tt.expected), len(result))
 | 
						|
				return
 | 
						|
			}
 | 
						|
 | 
						|
			// Validate both values and paths
 | 
						|
			for i, e := range tt.expected {
 | 
						|
				if i < len(result) {
 | 
						|
					if !reflect.DeepEqual(result[i].Value, e.Value) {
 | 
						|
						t.Errorf("Value at [%d]: got %v, expected %v", i, result[i].Value, e.Value)
 | 
						|
					}
 | 
						|
					if result[i].Path != e.Path {
 | 
						|
						t.Errorf("Path at [%d]: got %s, expected %s", i, result[i].Path, e.Path)
 | 
						|
					}
 | 
						|
				}
 | 
						|
			}
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestEdgeCases(t *testing.T) {
 | 
						|
	t.Run("empty_data", func(t *testing.T) {
 | 
						|
		result, err := Get("", "/store/book")
 | 
						|
		if err == nil {
 | 
						|
			t.Errorf("Expected error for empty data")
 | 
						|
			return
 | 
						|
		}
 | 
						|
		if len(result) > 0 {
 | 
						|
			t.Errorf("Expected empty result, got %v", result)
 | 
						|
		}
 | 
						|
	})
 | 
						|
 | 
						|
	t.Run("empty_path", func(t *testing.T) {
 | 
						|
		_, err := ParseXPath("")
 | 
						|
		if err == nil {
 | 
						|
			t.Error("Expected error for empty path")
 | 
						|
		}
 | 
						|
	})
 | 
						|
 | 
						|
	t.Run("invalid_xml", func(t *testing.T) {
 | 
						|
		_, err := Get("<invalid>xml", "/store")
 | 
						|
		if err == nil {
 | 
						|
			t.Error("Expected error for invalid XML")
 | 
						|
		}
 | 
						|
	})
 | 
						|
 | 
						|
	t.Run("current_node", func(t *testing.T) {
 | 
						|
		result, err := Get(testXML, "/store/book[1]/.")
 | 
						|
		if err != nil {
 | 
						|
			t.Errorf("Get() returned error: %v", err)
 | 
						|
			return
 | 
						|
		}
 | 
						|
		if len(result) != 1 {
 | 
						|
			t.Errorf("Expected 1 result, got %d", len(result))
 | 
						|
		}
 | 
						|
	})
 | 
						|
 | 
						|
	t.Run("attributes", func(t *testing.T) {
 | 
						|
		result, err := Get(testXML, "/store/book[1]/title/@lang")
 | 
						|
		if err != nil {
 | 
						|
			t.Errorf("Get() returned error: %v", err)
 | 
						|
			return
 | 
						|
		}
 | 
						|
		if len(result) != 1 || result[0].Value != "en" {
 | 
						|
			t.Errorf("Expected 'en', got %v", result)
 | 
						|
		}
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
func TestGetWithPaths(t *testing.T) {
 | 
						|
	tests := []struct {
 | 
						|
		name     string
 | 
						|
		path     string
 | 
						|
		expected []XMLNode
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			name: "simple_element_access",
 | 
						|
			path: "/store/bicycle/color",
 | 
						|
			expected: []XMLNode{
 | 
						|
				{Value: "red", Path: "/store/bicycle/color"},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "indexed_element_access",
 | 
						|
			path: "/store/book[1]/title",
 | 
						|
			expected: []XMLNode{
 | 
						|
				{Value: "The Fellowship of the Ring", Path: "/store/book[1]/title"},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "recursive_element_access",
 | 
						|
			path: "//price",
 | 
						|
			expected: []XMLNode{
 | 
						|
				{Value: "22.99", Path: "/store/book[1]/price"},
 | 
						|
				{Value: "23.45", Path: "/store/book[2]/price"},
 | 
						|
				{Value: "39.95", Path: "/store/book[3]/price"},
 | 
						|
				{Value: "199.95", Path: "/store/bicycle/price"},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "attribute_access",
 | 
						|
			path: "/store/book[1]/title/@lang",
 | 
						|
			expected: []XMLNode{
 | 
						|
				{Value: "en", Path: "/store/book[1]/title/@lang"},
 | 
						|
			},
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	for _, tt := range tests {
 | 
						|
		t.Run(tt.name, func(t *testing.T) {
 | 
						|
			result, err := Get(testXML, tt.path)
 | 
						|
			if err != nil {
 | 
						|
				t.Errorf("Get() returned error: %v", err)
 | 
						|
				return
 | 
						|
			}
 | 
						|
 | 
						|
			// Check if lengths match
 | 
						|
			if len(result) != len(tt.expected) {
 | 
						|
				t.Errorf("Get() returned %d items, expected %d", len(result), len(tt.expected))
 | 
						|
				return
 | 
						|
			}
 | 
						|
 | 
						|
			// For each expected item, find its match in the results and verify both value and path
 | 
						|
			for _, expected := range tt.expected {
 | 
						|
				found := false
 | 
						|
				for _, r := range result {
 | 
						|
					// First verify the value matches
 | 
						|
					if reflect.DeepEqual(r.Value, expected.Value) {
 | 
						|
						found = true
 | 
						|
						// Then verify the path matches
 | 
						|
						if r.Path != expected.Path {
 | 
						|
							t.Errorf("Path mismatch for value %v: got %s, expected %s", r.Value, r.Path, expected.Path)
 | 
						|
						}
 | 
						|
						break
 | 
						|
					}
 | 
						|
				}
 | 
						|
				if !found {
 | 
						|
					t.Errorf("Expected node with value %v and path %s not found in results", expected.Value, expected.Path)
 | 
						|
				}
 | 
						|
			}
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestSet(t *testing.T) {
 | 
						|
	t.Run("simple element", func(t *testing.T) {
 | 
						|
		xmlData := `<root><name>John</name></root>`
 | 
						|
		newXML, err := Set(xmlData, "/root/name", "Jane")
 | 
						|
		if err != nil {
 | 
						|
			t.Errorf("Set() returned error: %v", err)
 | 
						|
			return
 | 
						|
		}
 | 
						|
 | 
						|
		// Verify the change
 | 
						|
		result, err := Get(newXML, "/root/name")
 | 
						|
		if err != nil {
 | 
						|
			t.Errorf("Get() returned error: %v", err)
 | 
						|
			return
 | 
						|
		}
 | 
						|
		if len(result) != 1 || result[0].Value != "Jane" {
 | 
						|
			t.Errorf("Set() failed: expected name to be 'Jane', got %v", result)
 | 
						|
		}
 | 
						|
	})
 | 
						|
 | 
						|
	t.Run("attribute", func(t *testing.T) {
 | 
						|
		xmlData := `<root><element id="123"></element></root>`
 | 
						|
		newXML, err := Set(xmlData, "/root/element/@id", "456")
 | 
						|
		if err != nil {
 | 
						|
			t.Errorf("Set() returned error: %v", err)
 | 
						|
			return
 | 
						|
		}
 | 
						|
 | 
						|
		// Verify the change
 | 
						|
		result, err := Get(newXML, "/root/element/@id")
 | 
						|
		if err != nil {
 | 
						|
			t.Errorf("Get() returned error: %v", err)
 | 
						|
			return
 | 
						|
		}
 | 
						|
		if len(result) != 1 || result[0].Value != "456" {
 | 
						|
			t.Errorf("Set() failed: expected id to be '456', got %v", result)
 | 
						|
		}
 | 
						|
	})
 | 
						|
 | 
						|
	t.Run("indexed element", func(t *testing.T) {
 | 
						|
		xmlData := `<root><items><item>first</item><item>second</item></items></root>`
 | 
						|
		newXML, err := Set(xmlData, "/root/items/item[1]", "changed")
 | 
						|
		if err != nil {
 | 
						|
			t.Errorf("Set() returned error: %v", err)
 | 
						|
			return
 | 
						|
		}
 | 
						|
 | 
						|
		// Verify the change
 | 
						|
		result, err := Get(newXML, "/root/items/item[1]")
 | 
						|
		if err != nil {
 | 
						|
			t.Errorf("Get() returned error: %v", err)
 | 
						|
			return
 | 
						|
		}
 | 
						|
		if len(result) != 1 || result[0].Value != "changed" {
 | 
						|
			t.Errorf("Set() failed: expected item to be 'changed', got %v", result)
 | 
						|
		}
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
func TestSetAll(t *testing.T) {
 | 
						|
	t.Run("multiple elements", func(t *testing.T) {
 | 
						|
		xmlData := `<root><items><item>first</item><item>second</item></items></root>`
 | 
						|
		newXML, err := SetAll(xmlData, "//item", "changed")
 | 
						|
		if err != nil {
 | 
						|
			t.Errorf("SetAll() returned error: %v", err)
 | 
						|
			return
 | 
						|
		}
 | 
						|
 | 
						|
		// Verify all items are changed
 | 
						|
		result, err := Get(newXML, "//item")
 | 
						|
		if err != nil {
 | 
						|
			t.Errorf("Get() returned error: %v", err)
 | 
						|
			return
 | 
						|
		}
 | 
						|
		if len(result) != 2 {
 | 
						|
			t.Errorf("Expected 2 results, got %d", len(result))
 | 
						|
			return
 | 
						|
		}
 | 
						|
 | 
						|
		for i, node := range result {
 | 
						|
			if node.Value != "changed" {
 | 
						|
				t.Errorf("Item %d not changed, got %v", i+1, node.Value)
 | 
						|
			}
 | 
						|
		}
 | 
						|
	})
 | 
						|
 | 
						|
	t.Run("attributes", func(t *testing.T) {
 | 
						|
		xmlData := `<root><item id="1"/><item id="2"/></root>`
 | 
						|
		newXML, err := SetAll(xmlData, "//item/@id", "new")
 | 
						|
		if err != nil {
 | 
						|
			t.Errorf("SetAll() returned error: %v", err)
 | 
						|
			return
 | 
						|
		}
 | 
						|
 | 
						|
		// Verify all attributes are changed
 | 
						|
		result, err := Get(newXML, "//item/@id")
 | 
						|
		if err != nil {
 | 
						|
			t.Errorf("Get() returned error: %v", err)
 | 
						|
			return
 | 
						|
		}
 | 
						|
		if len(result) != 2 {
 | 
						|
			t.Errorf("Expected 2 results, got %d", len(result))
 | 
						|
			return
 | 
						|
		}
 | 
						|
 | 
						|
		for i, node := range result {
 | 
						|
			if node.Value != "new" {
 | 
						|
				t.Errorf("Attribute %d not changed, got %v", i+1, node.Value)
 | 
						|
			}
 | 
						|
		}
 | 
						|
	})
 | 
						|
}
 |