package redis import ( "math" "testing" ) // TestParseFTInfo tests the parseFTInfo function with a comprehensive FT.INFO response // This test uses the actual response structure from Redis with vector fields func TestParseFTInfo(t *testing.T) { // This is the data structure that would be returned by Redis for FT.INFO // Based on the redis-cli output provided by the user data := map[string]interface{}{ "index_name": "rand:42d1f2820b3048b6bc783d4dcdb9094a", "index_options": []interface{}{}, "index_definition": []interface{}{ "key_type", "HASH", "prefixes", []interface{}{"rand:42d1f2820b3048b6bc783d4dcdb9094a"}, "default_score", "1", "indexes_all", false, }, "attributes": []interface{}{ // prompt field (TEXT) []interface{}{ "identifier", "prompt", "attribute", "prompt", "type", "TEXT", "WEIGHT", "1", }, // response field (TEXT) []interface{}{ "identifier", "response", "attribute", "response", "type", "TEXT", "WEIGHT", "1", }, // exact_digest field (TAG) []interface{}{ "identifier", "exact_digest", "attribute", "exact_digest", "type", "TAG", "SEPARATOR", ",", }, // prompt_vector field (VECTOR) []interface{}{ "identifier", "prompt_vector", "attribute", "prompt_vector", "type", "VECTOR", "algorithm", "HNSW", "data_type", "FLOAT32", "dim", int64(1536), "distance_metric", "COSINE", "M", int64(16), "ef_construction", int64(64), }, }, "num_docs": int64(0), "max_doc_id": int64(0), "num_terms": int64(0), "num_records": int64(0), "inverted_sz_mb": "0", "vector_index_sz_mb": "0", "total_inverted_index_blocks": int64(0), "offset_vectors_sz_mb": "0", "doc_table_size_mb": "0.01532745361328125", "sortable_values_size_mb": "0", "key_table_size_mb": "2.288818359375e-5", "tag_overhead_sz_mb": "0", "text_overhead_sz_mb": "0", "total_index_memory_sz_mb": "0.015350341796875", "geoshapes_sz_mb": "0", "records_per_doc_avg": "nan", "bytes_per_record_avg": "nan", "offsets_per_term_avg": "nan", "offset_bits_per_record_avg": "nan", "hash_indexing_failures": int64(0), "total_indexing_time": "0", "indexing": int64(0), "percent_indexed": "1", "number_of_uses": int64(2), "cleaning": int64(0), "gc_stats": []interface{}{ "bytes_collected", "0", "total_ms_run", "0", "total_cycles", "0", "average_cycle_time_ms", "nan", "last_run_time_ms", "0", "gc_numeric_trees_missed", "0", "gc_blocks_denied", "0", }, "cursor_stats": []interface{}{ "global_idle", int64(0), "global_total", int64(0), "index_capacity", int64(128), "index_total", int64(0), }, "dialect_stats": []interface{}{ "dialect_1", int64(0), "dialect_2", int64(0), "dialect_3", int64(0), "dialect_4", int64(0), }, "Index Errors": []interface{}{ "indexing failures", int64(0), "last indexing error", "N/A", "last indexing error key", "N/A", "background indexing status", "OK", }, "field statistics": []interface{}{ []interface{}{ "identifier", "prompt", "attribute", "prompt", "Index Errors", []interface{}{ "indexing failures", int64(0), "last indexing error", "N/A", "last indexing error key", "N/A", }, }, []interface{}{ "identifier", "response", "attribute", "response", "Index Errors", []interface{}{ "indexing failures", int64(0), "last indexing error", "N/A", "last indexing error key", "N/A", }, }, []interface{}{ "identifier", "exact_digest", "attribute", "exact_digest", "Index Errors", []interface{}{ "indexing failures", int64(0), "last indexing error", "N/A", "last indexing error key", "N/A", }, }, []interface{}{ "identifier", "prompt_vector", "attribute", "prompt_vector", "Index Errors", []interface{}{ "indexing failures", int64(0), "last indexing error", "N/A", "last indexing error key", "N/A", }, "memory", int64(0), "marked_deleted", int64(0), }, }, } // Parse the data result, err := parseFTInfo(data) if err != nil { t.Fatalf("parseFTInfo failed: %v", err) } // Validate index name if result.IndexName != "rand:42d1f2820b3048b6bc783d4dcdb9094a" { t.Errorf("IndexName = %v, want %v", result.IndexName, "rand:42d1f2820b3048b6bc783d4dcdb9094a") } // Validate index definition if result.IndexDefinition.KeyType != "HASH" { t.Errorf("IndexDefinition.KeyType = %v, want HASH", result.IndexDefinition.KeyType) } if len(result.IndexDefinition.Prefixes) != 1 || result.IndexDefinition.Prefixes[0] != "rand:42d1f2820b3048b6bc783d4dcdb9094a" { t.Errorf("IndexDefinition.Prefixes = %v, want [rand:42d1f2820b3048b6bc783d4dcdb9094a]", result.IndexDefinition.Prefixes) } if result.IndexDefinition.DefaultScore != 1.0 { t.Errorf("IndexDefinition.DefaultScore = %v, want 1.0", result.IndexDefinition.DefaultScore) } // Validate attributes if len(result.Attributes) != 4 { t.Fatalf("len(Attributes) = %v, want 4", len(result.Attributes)) } // Check prompt field (TEXT) promptAttr := result.Attributes[0] if promptAttr.Identifier != "prompt" { t.Errorf("Attributes[0].Identifier = %v, want prompt", promptAttr.Identifier) } if promptAttr.Attribute != "prompt" { t.Errorf("Attributes[0].Attribute = %v, want prompt", promptAttr.Attribute) } if promptAttr.Type != "TEXT" { t.Errorf("Attributes[0].Type = %v, want TEXT", promptAttr.Type) } if promptAttr.Weight != 1.0 { t.Errorf("Attributes[0].Weight = %v, want 1.0", promptAttr.Weight) } // Check response field (TEXT) responseAttr := result.Attributes[1] if responseAttr.Identifier != "response" { t.Errorf("Attributes[1].Identifier = %v, want response", responseAttr.Identifier) } if responseAttr.Attribute != "response" { t.Errorf("Attributes[1].Attribute = %v, want response", responseAttr.Attribute) } if responseAttr.Type != "TEXT" { t.Errorf("Attributes[1].Type = %v, want TEXT", responseAttr.Type) } // Check exact_digest field (TAG) tagAttr := result.Attributes[2] if tagAttr.Identifier != "exact_digest" { t.Errorf("Attributes[2].Identifier = %v, want exact_digest", tagAttr.Identifier) } if tagAttr.Attribute != "exact_digest" { t.Errorf("Attributes[2].Attribute = %v, want exact_digest", tagAttr.Attribute) } if tagAttr.Type != "TAG" { t.Errorf("Attributes[2].Type = %v, want TAG", tagAttr.Type) } // Check prompt_vector field (VECTOR) vectorAttr := result.Attributes[3] if vectorAttr.Identifier != "prompt_vector" { t.Errorf("Attributes[3].Identifier = %v, want prompt_vector", vectorAttr.Identifier) } if vectorAttr.Attribute != "prompt_vector" { t.Errorf("Attributes[3].Attribute = %v, want prompt_vector", vectorAttr.Attribute) } if vectorAttr.Type != "VECTOR" { t.Errorf("Attributes[3].Type = %v, want VECTOR", vectorAttr.Type) } if vectorAttr.Algorithm != "HNSW" { t.Errorf("Attributes[3].Algorithm = %v, want HNSW", vectorAttr.Algorithm) } if vectorAttr.DataType != "FLOAT32" { t.Errorf("Attributes[3].DataType = %v, want FLOAT32", vectorAttr.DataType) } if vectorAttr.Dim != 1536 { t.Errorf("Attributes[3].Dim = %v, want 1536", vectorAttr.Dim) } if vectorAttr.DistanceMetric != "COSINE" { t.Errorf("Attributes[3].DistanceMetric = %v, want COSINE", vectorAttr.DistanceMetric) } if vectorAttr.M != 16 { t.Errorf("Attributes[3].M = %v, want 16", vectorAttr.M) } if vectorAttr.EFConstruction != 64 { t.Errorf("Attributes[3].EFConstruction = %v, want 64", vectorAttr.EFConstruction) } // Validate numeric fields if result.NumDocs != 0 { t.Errorf("NumDocs = %v, want 0", result.NumDocs) } if result.MaxDocID != 0 { t.Errorf("MaxDocID = %v, want 0", result.MaxDocID) } if result.NumTerms != 0 { t.Errorf("NumTerms = %v, want 0", result.NumTerms) } if result.NumRecords != 0 { t.Errorf("NumRecords = %v, want 0", result.NumRecords) } if result.Indexing != 0 { t.Errorf("Indexing = %v, want 0", result.Indexing) } if result.PercentIndexed != 1.0 { t.Errorf("PercentIndexed = %v, want 1.0", result.PercentIndexed) } if result.HashIndexingFailures != 0 { t.Errorf("HashIndexingFailures = %v, want 0", result.HashIndexingFailures) } if result.Cleaning != 0 { t.Errorf("Cleaning = %v, want 0", result.Cleaning) } if result.NumberOfUses != 2 { t.Errorf("NumberOfUses = %v, want 2", result.NumberOfUses) } // Validate average stats (should be "nan" for empty index) if result.RecordsPerDocAvg != "nan" { t.Errorf("RecordsPerDocAvg = %v, want nan", result.RecordsPerDocAvg) } if result.BytesPerRecordAvg != "nan" { t.Errorf("BytesPerRecordAvg = %v, want nan", result.BytesPerRecordAvg) } if result.OffsetsPerTermAvg != "nan" { t.Errorf("OffsetsPerTermAvg = %v, want nan", result.OffsetsPerTermAvg) } if result.OffsetBitsPerRecordAvg != "nan" { t.Errorf("OffsetBitsPerRecordAvg = %v, want nan", result.OffsetBitsPerRecordAvg) } // Validate cursor stats if result.CursorStats.GlobalIdle != 0 { t.Errorf("CursorStats.GlobalIdle = %v, want 0", result.CursorStats.GlobalIdle) } if result.CursorStats.GlobalTotal != 0 { t.Errorf("CursorStats.GlobalTotal = %v, want 0", result.CursorStats.GlobalTotal) } if result.CursorStats.IndexCapacity != 128 { t.Errorf("CursorStats.IndexCapacity = %v, want 128", result.CursorStats.IndexCapacity) } if result.CursorStats.IndexTotal != 0 { t.Errorf("CursorStats.IndexTotal = %v, want 0", result.CursorStats.IndexTotal) } // Validate dialect stats if result.DialectStats["dialect_1"] != 0 { t.Errorf("DialectStats[dialect_1] = %v, want 0", result.DialectStats["dialect_1"]) } if result.DialectStats["dialect_2"] != 0 { t.Errorf("DialectStats[dialect_2] = %v, want 0", result.DialectStats["dialect_2"]) } if result.DialectStats["dialect_3"] != 0 { t.Errorf("DialectStats[dialect_3] = %v, want 0", result.DialectStats["dialect_3"]) } if result.DialectStats["dialect_4"] != 0 { t.Errorf("DialectStats[dialect_4] = %v, want 0", result.DialectStats["dialect_4"]) } // Validate GC stats if result.GCStats.BytesCollected != 0 { t.Errorf("GCStats.BytesCollected = %v, want 0", result.GCStats.BytesCollected) } if result.GCStats.TotalMsRun != 0 { t.Errorf("GCStats.TotalMsRun = %v, want 0", result.GCStats.TotalMsRun) } if result.GCStats.TotalCycles != 0 { t.Errorf("GCStats.TotalCycles = %v, want 0", result.GCStats.TotalCycles) } if result.GCStats.AverageCycleTimeMs != "nan" { t.Errorf("GCStats.AverageCycleTimeMs = %v, want nan", result.GCStats.AverageCycleTimeMs) } // Validate Index Errors if result.IndexErrors.IndexingFailures != 0 { t.Errorf("IndexErrors.IndexingFailures = %v, want 0", result.IndexErrors.IndexingFailures) } if result.IndexErrors.LastIndexingError != "N/A" { t.Errorf("IndexErrors.LastIndexingError = %v, want N/A", result.IndexErrors.LastIndexingError) } if result.IndexErrors.LastIndexingErrorKey != "N/A" { t.Errorf("IndexErrors.LastIndexingErrorKey = %v, want N/A", result.IndexErrors.LastIndexingErrorKey) } // Validate field statistics if len(result.FieldStatistics) != 4 { t.Fatalf("len(FieldStatistics) = %v, want 4", len(result.FieldStatistics)) } expectedIdentifiers := map[string]bool{ "prompt": true, "response": true, "exact_digest": true, "prompt_vector": true, } for _, fieldStat := range result.FieldStatistics { if !expectedIdentifiers[fieldStat.Identifier] { t.Errorf("Unexpected field statistic identifier: %v", fieldStat.Identifier) } if fieldStat.IndexErrors.IndexingFailures != 0 { t.Errorf("FieldStatistic[%s].IndexErrors.IndexingFailures = %v, want 0", fieldStat.Identifier, fieldStat.IndexErrors.IndexingFailures) } if fieldStat.IndexErrors.LastIndexingError != "N/A" { t.Errorf("FieldStatistic[%s].IndexErrors.LastIndexingError = %v, want N/A", fieldStat.Identifier, fieldStat.IndexErrors.LastIndexingError) } if fieldStat.IndexErrors.LastIndexingErrorKey != "N/A" { t.Errorf("FieldStatistic[%s].IndexErrors.LastIndexingErrorKey = %v, want N/A", fieldStat.Identifier, fieldStat.IndexErrors.LastIndexingErrorKey) } } t.Logf("Successfully parsed FT.INFO response with %d attributes", len(result.Attributes)) } // toInterfaceMap converts map[string]interface{} to map[interface{}]interface{} // This is needed because RESP3 returns map[interface{}]interface{} but test data // is easier to write as map[string]interface{} func toInterfaceMap(m map[string]interface{}) map[interface{}]interface{} { result := make(map[interface{}]interface{}, len(m)) for k, v := range m { switch val := v.(type) { case map[string]interface{}: result[k] = toInterfaceMap(val) case []interface{}: result[k] = toInterfaceSlice(val) default: result[k] = v } } return result } // toInterfaceSlice converts []interface{} elements recursively func toInterfaceSlice(s []interface{}) []interface{} { result := make([]interface{}, len(s)) for i, v := range s { switch val := v.(type) { case map[string]interface{}: result[i] = toInterfaceMap(val) case []interface{}: result[i] = toInterfaceSlice(val) default: result[i] = v } } return result } // TestParseFTSearchRESP3 tests the RESP3 parsing for FT.SEARCH results func TestParseFTSearchRESP3(t *testing.T) { tests := []struct { name string input map[string]interface{} expected FTSearchResult }{ { name: "empty result", input: map[string]interface{}{ "attributes": []interface{}{}, "total_results": int64(0), "format": "STRING", "results": []interface{}{}, "warning": []interface{}{}, }, expected: FTSearchResult{ Total: 0, Docs: []Document{}, Warnings: []string{}, }, }, { name: "simple query result", input: map[string]interface{}{ "attributes": []interface{}{}, "total_results": int64(2), "format": "STRING", "results": []interface{}{ map[string]interface{}{ "id": "usr:david", "extra_attributes": map[string]interface{}{ "first_name": "David", "last_name": "Maier", }, "values": []interface{}{}, }, map[string]interface{}{ "id": "usr:shaya", "extra_attributes": map[string]interface{}{ "first_name": "Shaya", "last_name": "Potter", }, "values": []interface{}{}, }, }, "warning": []interface{}{}, }, expected: FTSearchResult{ Total: 2, Docs: []Document{ { ID: "usr:david", Fields: map[string]string{"first_name": "David", "last_name": "Maier"}, }, { ID: "usr:shaya", Fields: map[string]string{"first_name": "Shaya", "last_name": "Potter"}, }, }, Warnings: []string{}, }, }, { name: "result with scores", input: map[string]interface{}{ "attributes": []interface{}{}, "total_results": int64(1), "format": "STRING", "results": []interface{}{ map[string]interface{}{ "id": "bicycle:hash:1", "score": float64(1.5), "extra_attributes": map[string]interface{}{ "price": "1200", "brand": "Bicyk", }, "values": []interface{}{}, }, }, "warning": []interface{}{}, }, expected: FTSearchResult{ Total: 1, Docs: []Document{ { ID: "bicycle:hash:1", Score: ptrFloat64(1.5), Fields: map[string]string{"price": "1200", "brand": "Bicyk"}, }, }, Warnings: []string{}, }, }, { name: "result with warnings", input: map[string]interface{}{ "attributes": []interface{}{}, "total_results": int64(1), "format": "STRING", "results": []interface{}{ map[string]interface{}{ "id": "doc:1", "extra_attributes": map[string]interface{}{ "field1": "value1", }, "values": []interface{}{}, }, }, "warning": []interface{}{"Timeout limit was reached"}, }, expected: FTSearchResult{ Total: 1, Docs: []Document{ { ID: "doc:1", Fields: map[string]string{"field1": "value1"}, }, }, Warnings: []string{"Timeout limit was reached"}, }, }, { name: "NOCONTENT result", input: map[string]interface{}{ "attributes": []interface{}{}, "total_results": int64(10), "format": "STRING", "results": []interface{}{ map[string]interface{}{ "id": "bicycle:hash:2", "values": []interface{}{}, }, }, "warning": []interface{}{}, }, expected: FTSearchResult{ Total: 10, Docs: []Document{ { ID: "bicycle:hash:2", Fields: map[string]string{}, }, }, Warnings: []string{}, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := parseFTSearchMapRESP3(toInterfaceMap(tt.input)) if err != nil { t.Fatalf("parseFTSearchMapRESP3() error = %v", err) } if result.Total != tt.expected.Total { t.Errorf("Total = %v, want %v", result.Total, tt.expected.Total) } if len(result.Docs) != len(tt.expected.Docs) { t.Errorf("len(Docs) = %v, want %v", len(result.Docs), len(tt.expected.Docs)) } for i, doc := range result.Docs { if doc.ID != tt.expected.Docs[i].ID { t.Errorf("Docs[%d].ID = %v, want %v", i, doc.ID, tt.expected.Docs[i].ID) } if tt.expected.Docs[i].Score != nil { if doc.Score == nil || *doc.Score != *tt.expected.Docs[i].Score { t.Errorf("Docs[%d].Score = %v, want %v", i, doc.Score, tt.expected.Docs[i].Score) } } for k, v := range tt.expected.Docs[i].Fields { if doc.Fields[k] != v { t.Errorf("Docs[%d].Fields[%s] = %v, want %v", i, k, doc.Fields[k], v) } } } if len(result.Warnings) != len(tt.expected.Warnings) { t.Errorf("len(Warnings) = %v, want %v", len(result.Warnings), len(tt.expected.Warnings)) } for i, w := range result.Warnings { if w != tt.expected.Warnings[i] { t.Errorf("Warnings[%d] = %v, want %v", i, w, tt.expected.Warnings[i]) } } }) } } // TestParseFTAggregateRESP3 tests the RESP3 parsing for FT.AGGREGATE results func TestParseFTAggregateRESP3(t *testing.T) { tests := []struct { name string input map[string]interface{} expected FTAggregateResult }{ { name: "empty result", input: map[string]interface{}{ "attributes": []interface{}{}, "total_results": int64(0), "format": "STRING", "results": []interface{}{}, "warning": []interface{}{}, }, expected: FTAggregateResult{ Total: 0, Rows: []AggregateRow{}, Warnings: []string{}, }, }, { name: "aggregation with groupby", input: map[string]interface{}{ "attributes": []interface{}{}, "total_results": int64(3), "format": "STRING", "results": []interface{}{ map[string]interface{}{ "extra_attributes": map[string]interface{}{ "condition": "refurbished", "num_affordable": "158", }, "values": []interface{}{}, }, map[string]interface{}{ "extra_attributes": map[string]interface{}{ "condition": "used", "num_affordable": "156", }, "values": []interface{}{}, }, map[string]interface{}{ "extra_attributes": map[string]interface{}{ "condition": "new", "num_affordable": "158", }, "values": []interface{}{}, }, }, "warning": []interface{}{}, }, expected: FTAggregateResult{ Total: 3, Rows: []AggregateRow{ {Fields: map[string]interface{}{"condition": "refurbished", "num_affordable": "158"}}, {Fields: map[string]interface{}{"condition": "used", "num_affordable": "156"}}, {Fields: map[string]interface{}{"condition": "new", "num_affordable": "158"}}, }, Warnings: []string{}, }, }, { name: "aggregation with warnings", input: map[string]interface{}{ "attributes": []interface{}{}, "total_results": int64(1), "format": "STRING", "results": []interface{}{ map[string]interface{}{ "extra_attributes": map[string]interface{}{ "__key": "bicycle:0", "price": "270", "discounted": "243", }, "values": []interface{}{}, }, }, "warning": []interface{}{"Timeout limit was reached"}, }, expected: FTAggregateResult{ Total: 1, Rows: []AggregateRow{ {Fields: map[string]interface{}{"__key": "bicycle:0", "price": "270", "discounted": "243"}}, }, Warnings: []string{"Timeout limit was reached"}, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := parseFTAggregateMapRESP3(toInterfaceMap(tt.input)) if err != nil { t.Fatalf("parseFTAggregateMapRESP3() error = %v", err) } if result.Total != tt.expected.Total { t.Errorf("Total = %v, want %v", result.Total, tt.expected.Total) } if len(result.Rows) != len(tt.expected.Rows) { t.Errorf("len(Rows) = %v, want %v", len(result.Rows), len(tt.expected.Rows)) } for i, row := range result.Rows { for k, v := range tt.expected.Rows[i].Fields { if row.Fields[k] != v { t.Errorf("Rows[%d].Fields[%s] = %v, want %v", i, k, row.Fields[k], v) } } } if len(result.Warnings) != len(tt.expected.Warnings) { t.Errorf("len(Warnings) = %v, want %v", len(result.Warnings), len(tt.expected.Warnings)) } for i, w := range result.Warnings { if w != tt.expected.Warnings[i] { t.Errorf("Warnings[%d] = %v, want %v", i, w, tt.expected.Warnings[i]) } } }) } } // Helper function to create a pointer to a float64 func ptrFloat64(f float64) *float64 { return &f } // TestFTSearchRESP2AndRESP3Equivalence validates that RESP2 and RESP3 parsing // produce semantically equivalent results as per the spec requirement R.1.1 func TestFTSearchRESP2AndRESP3Equivalence(t *testing.T) { tests := []struct { name string resp2Data []interface{} resp3Data map[string]interface{} noContent bool withScores bool }{ { name: "simple hash query", resp2Data: []interface{}{ int64(2), "usr:david", []interface{}{"first_name", "David", "last_name", "Maier"}, "usr:shaya", []interface{}{"first_name", "Shaya", "last_name", "Potter"}, }, resp3Data: map[string]interface{}{ "attributes": []interface{}{}, "total_results": int64(2), "format": "STRING", "results": []interface{}{ map[string]interface{}{ "id": "usr:david", "extra_attributes": map[string]interface{}{ "first_name": "David", "last_name": "Maier", }, "values": []interface{}{}, }, map[string]interface{}{ "id": "usr:shaya", "extra_attributes": map[string]interface{}{ "first_name": "Shaya", "last_name": "Potter", }, "values": []interface{}{}, }, }, "warning": []interface{}{}, }, noContent: false, withScores: false, }, { name: "query with scores", resp2Data: []interface{}{ int64(1), "bicycle:hash:1", "1.5", []interface{}{"price", "1200", "brand", "Bicyk"}, }, resp3Data: map[string]interface{}{ "attributes": []interface{}{}, "total_results": int64(1), "format": "STRING", "results": []interface{}{ map[string]interface{}{ "id": "bicycle:hash:1", "score": float64(1.5), "extra_attributes": map[string]interface{}{ "price": "1200", "brand": "Bicyk", }, "values": []interface{}{}, }, }, "warning": []interface{}{}, }, noContent: false, withScores: true, }, { name: "JSON document query", resp2Data: []interface{}{ int64(1), "bicycle:1", []interface{}{"$", `{"brand":"Bicyk","model":"Hillcraft","price":1200}`}, }, resp3Data: map[string]interface{}{ "attributes": []interface{}{}, "total_results": int64(1), "format": "STRING", "results": []interface{}{ map[string]interface{}{ "id": "bicycle:1", "extra_attributes": map[string]interface{}{ "$": `{"brand":"Bicyk","model":"Hillcraft","price":1200}`, }, "values": []interface{}{}, }, }, "warning": []interface{}{}, }, noContent: false, withScores: false, }, { name: "NOCONTENT query", resp2Data: []interface{}{ int64(10), "bicycle:hash:2", }, resp3Data: map[string]interface{}{ "attributes": []interface{}{}, "total_results": int64(10), "format": "STRING", "results": []interface{}{ map[string]interface{}{ "id": "bicycle:hash:2", "values": []interface{}{}, }, }, "warning": []interface{}{}, }, noContent: true, withScores: false, }, { name: "empty result", resp2Data: []interface{}{ int64(0), }, resp3Data: map[string]interface{}{ "attributes": []interface{}{}, "total_results": int64(0), "format": "STRING", "results": []interface{}{}, "warning": []interface{}{}, }, noContent: false, withScores: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Parse RESP2 resp2Result, err := parseFTSearch(tt.resp2Data, tt.noContent, tt.withScores, false, false) if err != nil { t.Fatalf("parseFTSearch() error = %v", err) } // Parse RESP3 resp3Result, err := parseFTSearchMapRESP3(toInterfaceMap(tt.resp3Data)) if err != nil { t.Fatalf("parseFTSearchMapRESP3() error = %v", err) } // Compare Total if resp2Result.Total != resp3Result.Total { t.Errorf("Total mismatch: RESP2=%d, RESP3=%d", resp2Result.Total, resp3Result.Total) } // Compare number of documents if len(resp2Result.Docs) != len(resp3Result.Docs) { t.Errorf("Docs count mismatch: RESP2=%d, RESP3=%d", len(resp2Result.Docs), len(resp3Result.Docs)) return } // Compare each document for i, resp2Doc := range resp2Result.Docs { resp3Doc := resp3Result.Docs[i] if resp2Doc.ID != resp3Doc.ID { t.Errorf("Docs[%d].ID mismatch: RESP2=%s, RESP3=%s", i, resp2Doc.ID, resp3Doc.ID) } // Compare scores (if present) if tt.withScores { if resp2Doc.Score == nil && resp3Doc.Score != nil { t.Errorf("Docs[%d].Score mismatch: RESP2=nil, RESP3=%v", i, *resp3Doc.Score) } else if resp2Doc.Score != nil && resp3Doc.Score == nil { t.Errorf("Docs[%d].Score mismatch: RESP2=%v, RESP3=nil", i, *resp2Doc.Score) } else if resp2Doc.Score != nil && resp3Doc.Score != nil && *resp2Doc.Score != *resp3Doc.Score { t.Errorf("Docs[%d].Score mismatch: RESP2=%v, RESP3=%v", i, *resp2Doc.Score, *resp3Doc.Score) } } // Compare fields if len(resp2Doc.Fields) != len(resp3Doc.Fields) { t.Errorf("Docs[%d].Fields count mismatch: RESP2=%d, RESP3=%d", i, len(resp2Doc.Fields), len(resp3Doc.Fields)) } for k, v := range resp2Doc.Fields { if resp3Doc.Fields[k] != v { t.Errorf("Docs[%d].Fields[%s] mismatch: RESP2=%s, RESP3=%s", i, k, v, resp3Doc.Fields[k]) } } } }) } } // TestFTAggregateRESP2AndRESP3Equivalence validates that RESP2 and RESP3 parsing // produce semantically equivalent results for FT.AGGREGATE func TestFTAggregateRESP2AndRESP3Equivalence(t *testing.T) { tests := []struct { name string resp2Data []interface{} resp3Data map[string]interface{} }{ { name: "aggregation with groupby", resp2Data: []interface{}{ int64(3), []interface{}{"condition", "refurbished", "num_affordable", "158"}, []interface{}{"condition", "used", "num_affordable", "156"}, []interface{}{"condition", "new", "num_affordable", "158"}, }, resp3Data: map[string]interface{}{ "attributes": []interface{}{}, "total_results": int64(3), "format": "STRING", "results": []interface{}{ map[string]interface{}{ "extra_attributes": map[string]interface{}{ "condition": "refurbished", "num_affordable": "158", }, "values": []interface{}{}, }, map[string]interface{}{ "extra_attributes": map[string]interface{}{ "condition": "used", "num_affordable": "156", }, "values": []interface{}{}, }, map[string]interface{}{ "extra_attributes": map[string]interface{}{ "condition": "new", "num_affordable": "158", }, "values": []interface{}{}, }, }, "warning": []interface{}{}, }, }, { name: "aggregation with apply", resp2Data: []interface{}{ int64(1), []interface{}{"__key", "bicycle:0", "price", "270", "discounted", "243"}, }, resp3Data: map[string]interface{}{ "attributes": []interface{}{}, "total_results": int64(1), "format": "STRING", "results": []interface{}{ map[string]interface{}{ "extra_attributes": map[string]interface{}{ "__key": "bicycle:0", "price": "270", "discounted": "243", }, "values": []interface{}{}, }, }, "warning": []interface{}{}, }, }, { name: "empty result", resp2Data: []interface{}{ int64(0), }, resp3Data: map[string]interface{}{ "attributes": []interface{}{}, "total_results": int64(0), "format": "STRING", "results": []interface{}{}, "warning": []interface{}{}, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Parse RESP2 resp2Result, err := ProcessAggregateResult(tt.resp2Data) if err != nil { t.Fatalf("ProcessAggregateResult() error = %v", err) } // Parse RESP3 resp3Result, err := parseFTAggregateMapRESP3(toInterfaceMap(tt.resp3Data)) if err != nil { t.Fatalf("parseFTAggregateMapRESP3() error = %v", err) } // Compare Total if resp2Result.Total != resp3Result.Total { t.Errorf("Total mismatch: RESP2=%d, RESP3=%d", resp2Result.Total, resp3Result.Total) } // Compare number of rows if len(resp2Result.Rows) != len(resp3Result.Rows) { t.Errorf("Rows count mismatch: RESP2=%d, RESP3=%d", len(resp2Result.Rows), len(resp3Result.Rows)) return } // Compare each row for i, resp2Row := range resp2Result.Rows { resp3Row := resp3Result.Rows[i] // Compare fields count if len(resp2Row.Fields) != len(resp3Row.Fields) { t.Errorf("Rows[%d].Fields count mismatch: RESP2=%d, RESP3=%d", i, len(resp2Row.Fields), len(resp3Row.Fields)) } // Compare field values for k, v := range resp2Row.Fields { resp3Val, ok := resp3Row.Fields[k] if !ok { t.Errorf("Rows[%d].Fields[%s] missing in RESP3", i, k) continue } if v != resp3Val { t.Errorf("Rows[%d].Fields[%s] mismatch: RESP2=%v, RESP3=%v", i, k, v, resp3Val) } } } }) } } // TestParseFTSpellCheckRESP3 tests the RESP3 parsing for FT.SPELLCHECK results func TestParseFTSpellCheckRESP3(t *testing.T) { tests := []struct { name string input map[interface{}]interface{} expected []SpellCheckResult }{ { name: "empty result", input: map[interface{}]interface{}{ "results": map[interface{}]interface{}{}, }, expected: []SpellCheckResult{}, }, { name: "single misspelled term with one suggestion", input: map[interface{}]interface{}{ "results": map[interface{}]interface{}{ "impornant": []interface{}{ map[interface{}]interface{}{"important": float64(0.5)}, }, }, }, expected: []SpellCheckResult{ { Term: "impornant", Suggestions: []SpellCheckSuggestion{ {Score: 0.5, Suggestion: "important"}, }, }, }, }, { name: "single misspelled term with multiple suggestions", input: map[interface{}]interface{}{ "results": map[interface{}]interface{}{ "helo": []interface{}{ map[interface{}]interface{}{"hello": float64(0.8)}, map[interface{}]interface{}{"help": float64(0.3)}, }, }, }, expected: []SpellCheckResult{ { Term: "helo", Suggestions: []SpellCheckSuggestion{ {Score: 0.8, Suggestion: "hello"}, {Score: 0.3, Suggestion: "help"}, }, }, }, }, { name: "no results key", input: map[interface{}]interface{}{}, expected: []SpellCheckResult{}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := parseFTSpellCheckRESP3(tt.input) if err != nil { t.Fatalf("parseFTSpellCheckRESP3 returned error: %v", err) } if len(result) != len(tt.expected) { t.Fatalf("expected %d results, got %d", len(tt.expected), len(result)) } // For single result tests, verify the content if len(tt.expected) == 1 && len(result) == 1 { if result[0].Term != tt.expected[0].Term { t.Errorf("expected term %q, got %q", tt.expected[0].Term, result[0].Term) } if len(result[0].Suggestions) != len(tt.expected[0].Suggestions) { t.Errorf("expected %d suggestions, got %d", len(tt.expected[0].Suggestions), len(result[0].Suggestions)) } for i, sugg := range tt.expected[0].Suggestions { if i < len(result[0].Suggestions) { if result[0].Suggestions[i].Suggestion != sugg.Suggestion { t.Errorf("suggestion[%d]: expected %q, got %q", i, sugg.Suggestion, result[0].Suggestions[i].Suggestion) } if result[0].Suggestions[i].Score != sugg.Score { t.Errorf("suggestion[%d] score: expected %f, got %f", i, sugg.Score, result[0].Suggestions[i].Score) } } } } }) } } // TestParseFTSynDumpRESP3 tests the RESP3 parsing for FT.SYNDUMP results func TestParseFTSynDumpRESP3(t *testing.T) { tests := []struct { name string input map[interface{}]interface{} expected []FTSynDumpResult }{ { name: "empty result", input: map[interface{}]interface{}{}, expected: []FTSynDumpResult{}, }, { name: "single term with single synonym group", input: map[interface{}]interface{}{ "baby": []interface{}{"id1"}, }, expected: []FTSynDumpResult{ {Term: "baby", Synonyms: []string{"id1"}}, }, }, { name: "single term with multiple synonym groups", input: map[interface{}]interface{}{ "child": []interface{}{"id1", "id2"}, }, expected: []FTSynDumpResult{ {Term: "child", Synonyms: []string{"id1", "id2"}}, }, }, { name: "multiple terms", input: map[interface{}]interface{}{ "baby": []interface{}{"id1"}, "child": []interface{}{"id1"}, "offspring": []interface{}{"id1"}, }, expected: []FTSynDumpResult{ {Term: "baby", Synonyms: []string{"id1"}}, {Term: "child", Synonyms: []string{"id1"}}, {Term: "offspring", Synonyms: []string{"id1"}}, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := parseFTSynDumpRESP3(tt.input) if err != nil { t.Fatalf("parseFTSynDumpRESP3 returned error: %v", err) } if len(result) != len(tt.expected) { t.Fatalf("expected %d results, got %d", len(tt.expected), len(result)) } // Create a map for easier lookup since map iteration order is not guaranteed resultMap := make(map[string][]string) for _, r := range result { resultMap[r.Term] = r.Synonyms } for _, exp := range tt.expected { synonyms, ok := resultMap[exp.Term] if !ok { t.Errorf("expected term %q not found in result", exp.Term) continue } if len(synonyms) != len(exp.Synonyms) { t.Errorf("term %q: expected %d synonyms, got %d", exp.Term, len(exp.Synonyms), len(synonyms)) continue } for i, syn := range exp.Synonyms { if synonyms[i] != syn { t.Errorf("term %q synonym[%d]: expected %q, got %q", exp.Term, i, syn, synonyms[i]) } } } }) } } // TestFTInfoRESP3Parsing tests that FTInfoCmd correctly parses RESP3 format func TestFTInfoRESP3Parsing(t *testing.T) { // Test data simulating RESP3 response (map[interface{}]interface{}) input := map[interface{}]interface{}{ "index_name": "test_idx", "index_options": []interface{}{}, "index_definition": map[interface{}]interface{}{ "key_type": "HASH", "prefixes": []interface{}{"doc:"}, "default_score": float64(1), }, "attributes": []interface{}{ map[interface{}]interface{}{ "identifier": "txt", "attribute": "txt", "type": "TEXT", "WEIGHT": float64(1), "flags": []interface{}{"SORTABLE"}, }, }, "num_docs": int64(100), "max_doc_id": int64(100), "num_terms": int64(500), "num_records": int64(1000), "inverted_sz_mb": float64(0.5), "total_inverted_index_blocks": int64(50), "offset_vectors_sz_mb": float64(0.1), "doc_table_size_mb": float64(0.2), "sortable_values_size_mb": float64(0.05), "key_table_size_mb": float64(0.01), "records_per_doc_avg": float64(10), "bytes_per_record_avg": float64(50), "offsets_per_term_avg": float64(2), "offset_bits_per_record_avg": float64(8), "hash_indexing_failures": int64(0), "total_indexing_time": float64(123), "indexing": int64(0), "percent_indexed": float64(1), "number_of_uses": int64(10), "cleaning": int64(0), // RESP3 emits gc_stats as a map of doubles (REPLY_KVNUM in RediSearch). "gc_stats": map[interface{}]interface{}{ "bytes_collected": float64(1024), "total_ms_run": float64(50), "total_cycles": float64(3), "average_cycle_time_ms": float64(16.5), "last_run_time_ms": float64(20), "gc_numeric_trees_missed": float64(0), "gc_blocks_denied": float64(2), }, } // Convert to map[string]interface{} as parseFTInfo expects data := make(map[string]interface{}, len(input)) for k, v := range input { if kStr, ok := k.(string); ok { data[kStr] = v } } result, err := parseFTInfo(data) if err != nil { t.Fatalf("parseFTInfo returned error: %v", err) } if result.IndexName != "test_idx" { t.Errorf("expected IndexName 'test_idx', got %q", result.IndexName) } if result.NumDocs != 100 { t.Errorf("expected NumDocs 100, got %d", result.NumDocs) } if len(result.Attributes) != 1 { t.Fatalf("expected 1 attribute, got %d", len(result.Attributes)) } if result.Attributes[0].Attribute != "txt" { t.Errorf("expected attribute 'txt', got %q", result.Attributes[0].Attribute) } // RediSearch emits the *_avg fields via REPLY_KVNUM (doubles in RESP3). // The Go struct types them as string, so they must be stringified during parse. if result.RecordsPerDocAvg != "10" { t.Errorf("RecordsPerDocAvg = %q, want %q", result.RecordsPerDocAvg, "10") } if result.BytesPerRecordAvg != "50" { t.Errorf("BytesPerRecordAvg = %q, want %q", result.BytesPerRecordAvg, "50") } if result.OffsetsPerTermAvg != "2" { t.Errorf("OffsetsPerTermAvg = %q, want %q", result.OffsetsPerTermAvg, "2") } if result.OffsetBitsPerRecordAvg != "8" { t.Errorf("OffsetBitsPerRecordAvg = %q, want %q", result.OffsetBitsPerRecordAvg, "8") } // total_indexing_time is also a REPLY_KVNUM (double) but typed as int. if result.TotalIndexingTime != 123 { t.Errorf("TotalIndexingTime = %d, want 123", result.TotalIndexingTime) } // gc_stats numeric fields are typed as int but emitted as doubles in RESP3. if result.GCStats.BytesCollected != 1024 { t.Errorf("GCStats.BytesCollected = %d, want 1024", result.GCStats.BytesCollected) } if result.GCStats.TotalMsRun != 50 { t.Errorf("GCStats.TotalMsRun = %d, want 50", result.GCStats.TotalMsRun) } if result.GCStats.TotalCycles != 3 { t.Errorf("GCStats.TotalCycles = %d, want 3", result.GCStats.TotalCycles) } if result.GCStats.LastRunTimeMs != 20 { t.Errorf("GCStats.LastRunTimeMs = %d, want 20", result.GCStats.LastRunTimeMs) } if result.GCStats.GCBlocksDenied != 2 { t.Errorf("GCStats.GCBlocksDenied = %d, want 2", result.GCStats.GCBlocksDenied) } if result.GCStats.AverageCycleTimeMs != "16.5" { t.Errorf("GCStats.AverageCycleTimeMs = %q, want %q", result.GCStats.AverageCycleTimeMs, "16.5") } } // TestFTInfoRESP3NaN verifies that RediSearch's NaN doubles (emitted for empty // indexes) survive parsing as the lowercase "nan" sentinel, matching RESP2. func TestFTInfoRESP3NaN(t *testing.T) { data := map[string]interface{}{ "index_name": "empty_idx", "records_per_doc_avg": math.NaN(), "bytes_per_record_avg": math.NaN(), "offsets_per_term_avg": math.NaN(), "offset_bits_per_record_avg": math.NaN(), } result, err := parseFTInfo(data) if err != nil { t.Fatalf("parseFTInfo returned error: %v", err) } for _, tc := range []struct { name, got string }{ {"RecordsPerDocAvg", result.RecordsPerDocAvg}, {"BytesPerRecordAvg", result.BytesPerRecordAvg}, {"OffsetsPerTermAvg", result.OffsetsPerTermAvg}, {"OffsetBitsPerRecordAvg", result.OffsetBitsPerRecordAvg}, } { if tc.got != "nan" { t.Errorf("%s = %q, want %q", tc.name, tc.got, "nan") } } } // TestFTSpellCheckRESP2AndRESP3Equivalence validates that RESP2 and RESP3 parsing // produce semantically equivalent results for FT.SPELLCHECK func TestFTSpellCheckRESP2AndRESP3Equivalence(t *testing.T) { tests := []struct { name string resp2Data []interface{} resp3Data map[interface{}]interface{} }{ { name: "single misspelled term with suggestions", // RESP2: [["TERM", "impornant", [["0.5", "important"]]]] resp2Data: []interface{}{ []interface{}{ "TERM", "impornant", []interface{}{ []interface{}{"0.5", "important"}, }, }, }, // RESP3: {"results": {"impornant": [{"important": 0.5}]}} resp3Data: map[interface{}]interface{}{ "results": map[interface{}]interface{}{ "impornant": []interface{}{ map[interface{}]interface{}{"important": float64(0.5)}, }, }, }, }, { name: "multiple suggestions", // RESP2: [["TERM", "helo", [["0.8", "hello"], ["0.3", "help"]]]] resp2Data: []interface{}{ []interface{}{ "TERM", "helo", []interface{}{ []interface{}{"0.8", "hello"}, []interface{}{"0.3", "help"}, }, }, }, // RESP3: {"results": {"helo": [{"hello": 0.8}, {"help": 0.3}]}} resp3Data: map[interface{}]interface{}{ "results": map[interface{}]interface{}{ "helo": []interface{}{ map[interface{}]interface{}{"hello": float64(0.8)}, map[interface{}]interface{}{"help": float64(0.3)}, }, }, }, }, { name: "empty result", resp2Data: []interface{}{}, resp3Data: map[interface{}]interface{}{ "results": map[interface{}]interface{}{}, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Parse RESP2 resp2Result, err := parseFTSpellCheck(tt.resp2Data) if err != nil { t.Fatalf("parseFTSpellCheck (RESP2) returned error: %v", err) } // Parse RESP3 resp3Result, err := parseFTSpellCheckRESP3(tt.resp3Data) if err != nil { t.Fatalf("parseFTSpellCheckRESP3 returned error: %v", err) } // Compare results if len(resp2Result) != len(resp3Result) { t.Fatalf("result count mismatch: RESP2=%d, RESP3=%d", len(resp2Result), len(resp3Result)) } // For non-empty results, compare content if len(resp2Result) > 0 { // Create maps for comparison since order may differ resp2Map := make(map[string][]SpellCheckSuggestion) for _, r := range resp2Result { resp2Map[r.Term] = r.Suggestions } resp3Map := make(map[string][]SpellCheckSuggestion) for _, r := range resp3Result { resp3Map[r.Term] = r.Suggestions } for term, resp2Suggs := range resp2Map { resp3Suggs, ok := resp3Map[term] if !ok { t.Errorf("term %q found in RESP2 but not in RESP3", term) continue } if len(resp2Suggs) != len(resp3Suggs) { t.Errorf("term %q: suggestion count mismatch: RESP2=%d, RESP3=%d", term, len(resp2Suggs), len(resp3Suggs)) continue } // Compare suggestions (order may differ) resp2SuggMap := make(map[string]float64) for _, s := range resp2Suggs { resp2SuggMap[s.Suggestion] = s.Score } for _, s := range resp3Suggs { if score, ok := resp2SuggMap[s.Suggestion]; !ok { t.Errorf("term %q: suggestion %q found in RESP3 but not in RESP2", term, s.Suggestion) } else if score != s.Score { t.Errorf("term %q suggestion %q: score mismatch: RESP2=%f, RESP3=%f", term, s.Suggestion, score, s.Score) } } } } }) } } // TestFTSynDumpRESP2AndRESP3Equivalence validates that RESP2 and RESP3 parsing // produce semantically equivalent results for FT.SYNDUMP func TestFTSynDumpRESP2AndRESP3Equivalence(t *testing.T) { tests := []struct { name string resp2Data []interface{} resp3Data map[interface{}]interface{} }{ { name: "single term with single synonym group", // RESP2: ["baby", ["id1"]] resp2Data: []interface{}{ "baby", []interface{}{"id1"}, }, // RESP3: {"baby": ["id1"]} resp3Data: map[interface{}]interface{}{ "baby": []interface{}{"id1"}, }, }, { name: "multiple terms", // RESP2: ["baby", ["id1"], "child", ["id1"], "offspring", ["id1"]] resp2Data: []interface{}{ "baby", []interface{}{"id1"}, "child", []interface{}{"id1"}, "offspring", []interface{}{"id1"}, }, // RESP3: {"baby": ["id1"], "child": ["id1"], "offspring": ["id1"]} resp3Data: map[interface{}]interface{}{ "baby": []interface{}{"id1"}, "child": []interface{}{"id1"}, "offspring": []interface{}{"id1"}, }, }, { name: "empty result", resp2Data: []interface{}{}, resp3Data: map[interface{}]interface{}{}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Parse RESP2 manually (simulating what readReply does) var resp2Result []FTSynDumpResult for i := 0; i < len(tt.resp2Data); i += 2 { term, ok := tt.resp2Data[i].(string) if !ok { t.Fatalf("invalid RESP2 term format") } synonyms, ok := tt.resp2Data[i+1].([]interface{}) if !ok { t.Fatalf("invalid RESP2 synonyms format") } synonymList := make([]string, len(synonyms)) for j, syn := range synonyms { synonymList[j] = syn.(string) } resp2Result = append(resp2Result, FTSynDumpResult{ Term: term, Synonyms: synonymList, }) } // Parse RESP3 resp3Result, err := parseFTSynDumpRESP3(tt.resp3Data) if err != nil { t.Fatalf("parseFTSynDumpRESP3 returned error: %v", err) } // Compare results if len(resp2Result) != len(resp3Result) { t.Fatalf("result count mismatch: RESP2=%d, RESP3=%d", len(resp2Result), len(resp3Result)) } // Create maps for comparison since order may differ resp2Map := make(map[string][]string) for _, r := range resp2Result { resp2Map[r.Term] = r.Synonyms } resp3Map := make(map[string][]string) for _, r := range resp3Result { resp3Map[r.Term] = r.Synonyms } for term, resp2Syns := range resp2Map { resp3Syns, ok := resp3Map[term] if !ok { t.Errorf("term %q found in RESP2 but not in RESP3", term) continue } if len(resp2Syns) != len(resp3Syns) { t.Errorf("term %q: synonym count mismatch: RESP2=%d, RESP3=%d", term, len(resp2Syns), len(resp3Syns)) continue } for i, syn := range resp2Syns { if resp3Syns[i] != syn { t.Errorf("term %q synonym[%d]: RESP2=%q, RESP3=%q", term, i, syn, resp3Syns[i]) } } } }) } } // TestFTInfoRESP2AndRESP3Equivalence validates that RESP2 and RESP3 parsing // produce semantically equivalent results for FT.INFO func TestFTInfoRESP2AndRESP3Equivalence(t *testing.T) { // RESP2 format: attributes are arrays like ["identifier", "txt", "attribute", "txt", "type", "TEXT", ...] resp2Data := map[string]interface{}{ "index_name": "test_idx", "index_options": []interface{}{}, "attributes": []interface{}{ []interface{}{ "identifier", "txt", "attribute", "txt", "type", "TEXT", "WEIGHT", float64(1), "SORTABLE", "NOSTEM", }, }, "num_docs": int64(100), "num_terms": int64(500), "num_records": int64(1000), } // RESP3 format: attributes are maps resp3Data := map[string]interface{}{ "index_name": "test_idx", "index_options": []interface{}{}, "attributes": []interface{}{ map[interface{}]interface{}{ "identifier": "txt", "attribute": "txt", "type": "TEXT", "WEIGHT": float64(1), "flags": []interface{}{"SORTABLE", "NOSTEM"}, }, }, "num_docs": int64(100), "num_terms": int64(500), "num_records": int64(1000), } // Parse RESP2 resp2Result, err := parseFTInfo(resp2Data) if err != nil { t.Fatalf("parseFTInfo (RESP2) returned error: %v", err) } // Parse RESP3 resp3Result, err := parseFTInfo(resp3Data) if err != nil { t.Fatalf("parseFTInfo (RESP3) returned error: %v", err) } // Compare results if resp2Result.IndexName != resp3Result.IndexName { t.Errorf("IndexName mismatch: RESP2=%q, RESP3=%q", resp2Result.IndexName, resp3Result.IndexName) } if resp2Result.NumDocs != resp3Result.NumDocs { t.Errorf("NumDocs mismatch: RESP2=%d, RESP3=%d", resp2Result.NumDocs, resp3Result.NumDocs) } if resp2Result.NumTerms != resp3Result.NumTerms { t.Errorf("NumTerms mismatch: RESP2=%d, RESP3=%d", resp2Result.NumTerms, resp3Result.NumTerms) } if len(resp2Result.Attributes) != len(resp3Result.Attributes) { t.Fatalf("Attributes count mismatch: RESP2=%d, RESP3=%d", len(resp2Result.Attributes), len(resp3Result.Attributes)) } if len(resp2Result.Attributes) > 0 { attr2 := resp2Result.Attributes[0] attr3 := resp3Result.Attributes[0] if attr2.Attribute != attr3.Attribute { t.Errorf("Attribute name mismatch: RESP2=%q, RESP3=%q", attr2.Attribute, attr3.Attribute) } if attr2.Identifier != attr3.Identifier { t.Errorf("Identifier mismatch: RESP2=%q, RESP3=%q", attr2.Identifier, attr3.Identifier) } if attr2.Type != attr3.Type { t.Errorf("Type mismatch: RESP2=%q, RESP3=%q", attr2.Type, attr3.Type) } if attr2.Sortable != attr3.Sortable { t.Errorf("Sortable mismatch: RESP2=%v, RESP3=%v", attr2.Sortable, attr3.Sortable) } if attr2.NoStem != attr3.NoStem { t.Errorf("NoStem mismatch: RESP2=%v, RESP3=%v", attr2.NoStem, attr3.NoStem) } } }