package xpath import ( "reflect" "testing" ) // XML test data as a string for our tests var testXML = ` The Fellowship of the Ring J.R.R. Tolkien 1954 22.99 The Two Towers J.R.R. Tolkien 1954 23.45 Learning XML Erik T. Ray 2003 39.95 red 199.95 ` 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("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 := `John` 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 := `` 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 := `firstsecond` 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 := `firstsecond` 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 := `` 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) } } }) }