Skip to content

Commit 0185f29

Browse files
committed
test: add unit tests and integration tests for coverage
Unit tests: - db/import_test.go: TestEscapeCopyValue (COPY format escaping) - fuse/index_ddl_test.go: TestExtractColumnCandidates (index name parsing) - nfs/filesystem_test.go: TestHasFormatExtension (format detection) Integration tests: - TestFormats_YAML, TestFormats_YAML_NULL - TestMetadata_InfoSchema, InfoColumns, InfoCount, InfoDDL, InfoIndexes
1 parent f26dd21 commit 0185f29

File tree

5 files changed

+789
-1
lines changed

5 files changed

+789
-1
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ The filesystem interface is simple and predictable. The database handles durabil
2929
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
3030
│ Unix Tools │────▶│ Filesystem │────▶│ TigerFS │────▶│ PostgreSQL │
3131
│ ls, cat, │ │ Backend │ │ Daemon │ │ Database │
32-
│ echo, rm │◀────│ (FUSE/NFS) │◀────│ │◀────│ │
32+
│ echo, rm │◀────│ (FUSE/NFS) │◀────│ │◀────│ │
3333
└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
3434
```
3535

internal/tigerfs/db/import_test.go

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package db
2+
3+
import "testing"
4+
5+
// TestEscapeCopyValue tests the escapeCopyValue helper function
6+
// which escapes values for PostgreSQL COPY text format.
7+
func TestEscapeCopyValue(t *testing.T) {
8+
tests := []struct {
9+
name string
10+
input interface{}
11+
expected string
12+
}{
13+
// Basic strings - no escaping needed
14+
{
15+
name: "simple_string",
16+
input: "hello",
17+
expected: "hello",
18+
},
19+
{
20+
name: "empty_string",
21+
input: "",
22+
expected: "",
23+
},
24+
{
25+
name: "string_with_spaces",
26+
input: "hello world",
27+
expected: "hello world",
28+
},
29+
30+
// Backslash escaping
31+
{
32+
name: "single_backslash",
33+
input: `a\b`,
34+
expected: `a\\b`,
35+
},
36+
{
37+
name: "multiple_backslashes",
38+
input: `a\\b`,
39+
expected: `a\\\\b`,
40+
},
41+
{
42+
name: "backslash_at_end",
43+
input: `path\`,
44+
expected: `path\\`,
45+
},
46+
47+
// Newline escaping
48+
{
49+
name: "newline",
50+
input: "line1\nline2",
51+
expected: `line1\nline2`,
52+
},
53+
{
54+
name: "multiple_newlines",
55+
input: "a\nb\nc",
56+
expected: `a\nb\nc`,
57+
},
58+
59+
// Carriage return escaping
60+
{
61+
name: "carriage_return",
62+
input: "line1\rline2",
63+
expected: `line1\rline2`,
64+
},
65+
{
66+
name: "crlf",
67+
input: "line1\r\nline2",
68+
expected: `line1\r\nline2`,
69+
},
70+
71+
// Tab escaping
72+
{
73+
name: "tab",
74+
input: "col1\tcol2",
75+
expected: `col1\tcol2`,
76+
},
77+
{
78+
name: "multiple_tabs",
79+
input: "a\tb\tc",
80+
expected: `a\tb\tc`,
81+
},
82+
83+
// Combined special characters
84+
{
85+
name: "all_special_chars",
86+
input: "a\\b\nc\rd\te",
87+
expected: `a\\b\nc\rd\te`,
88+
},
89+
{
90+
name: "backslash_before_newline",
91+
input: "a\\\nb",
92+
expected: `a\\\nb`,
93+
},
94+
95+
// Non-string types (converted via fmt.Sprintf)
96+
{
97+
name: "integer",
98+
input: 42,
99+
expected: "42",
100+
},
101+
{
102+
name: "float",
103+
input: 3.14,
104+
expected: "3.14",
105+
},
106+
{
107+
name: "boolean_true",
108+
input: true,
109+
expected: "true",
110+
},
111+
{
112+
name: "boolean_false",
113+
input: false,
114+
expected: "false",
115+
},
116+
{
117+
name: "nil",
118+
input: nil,
119+
expected: "<nil>",
120+
},
121+
122+
// Unicode - should pass through unchanged
123+
{
124+
name: "unicode",
125+
input: "日本語",
126+
expected: "日本語",
127+
},
128+
{
129+
name: "emoji",
130+
input: "hello 👋 world",
131+
expected: "hello 👋 world",
132+
},
133+
}
134+
135+
for _, tt := range tests {
136+
t.Run(tt.name, func(t *testing.T) {
137+
result := escapeCopyValue(tt.input)
138+
if result != tt.expected {
139+
t.Errorf("escapeCopyValue(%#v) = %q, want %q", tt.input, result, tt.expected)
140+
}
141+
})
142+
}
143+
}

internal/tigerfs/fuse/index_ddl_test.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,3 +542,153 @@ func TestIndexCreateDirNode_Lookup(t *testing.T) {
542542
}
543543

544544
// Note: contains helper is defined in control_files_test.go
545+
546+
// TestExtractColumnCandidates tests the extractColumnCandidates helper function
547+
func TestExtractColumnCandidates(t *testing.T) {
548+
tests := []struct {
549+
name string
550+
indexName string
551+
tableName string
552+
hasTableNameColumn bool
553+
expected []string
554+
}{
555+
// Prefix patterns: idx_<column>, ix_<column>, index_<column>
556+
{
557+
name: "idx_prefix",
558+
indexName: "idx_email",
559+
tableName: "users",
560+
expected: []string{"email"},
561+
},
562+
{
563+
name: "ix_prefix",
564+
indexName: "ix_name",
565+
tableName: "users",
566+
expected: []string{"name"},
567+
},
568+
{
569+
name: "index_prefix",
570+
indexName: "index_status",
571+
tableName: "orders",
572+
expected: []string{"status"},
573+
},
574+
575+
// Suffix patterns: <column>_idx, <column>_ix, <column>_index
576+
{
577+
name: "idx_suffix",
578+
indexName: "email_idx",
579+
tableName: "users",
580+
expected: []string{"email"},
581+
},
582+
{
583+
name: "ix_suffix",
584+
indexName: "name_ix",
585+
tableName: "users",
586+
expected: []string{"name"},
587+
},
588+
{
589+
name: "index_suffix",
590+
indexName: "status_index",
591+
tableName: "orders",
592+
expected: []string{"status"},
593+
},
594+
595+
// Table prefix patterns: <table>_<column>_idx
596+
{
597+
name: "table_column_idx",
598+
indexName: "users_email_idx",
599+
tableName: "users",
600+
expected: []string{"email"},
601+
},
602+
{
603+
name: "table_column_no_suffix",
604+
indexName: "users_email",
605+
tableName: "users",
606+
expected: []string{"email"},
607+
},
608+
609+
// Table prefix disabled when column has same name as table
610+
{
611+
name: "table_column_idx_with_table_name_column",
612+
indexName: "users_email_idx",
613+
tableName: "users",
614+
hasTableNameColumn: true,
615+
expected: []string{}, // No inference when ambiguous
616+
},
617+
618+
// Just the column name
619+
{
620+
name: "bare_column_name",
621+
indexName: "email",
622+
tableName: "users",
623+
expected: []string{"email"},
624+
},
625+
626+
// Edge cases - no match
627+
{
628+
name: "underscore_in_column_prefix",
629+
indexName: "idx_created_at",
630+
tableName: "users",
631+
expected: []string{}, // Has underscore after prefix
632+
},
633+
{
634+
name: "underscore_in_column_suffix",
635+
indexName: "created_at_idx",
636+
tableName: "users",
637+
expected: []string{}, // Has underscore before suffix
638+
},
639+
{
640+
name: "empty_after_prefix",
641+
indexName: "idx_",
642+
tableName: "users",
643+
expected: []string{},
644+
},
645+
{
646+
name: "random_underscored_name",
647+
indexName: "some_random_name",
648+
tableName: "users",
649+
expected: []string{},
650+
},
651+
652+
// Case insensitivity
653+
{
654+
name: "uppercase_prefix",
655+
indexName: "IDX_EMAIL",
656+
tableName: "users",
657+
expected: []string{"email"},
658+
},
659+
{
660+
name: "mixed_case_table",
661+
indexName: "Users_Status_idx",
662+
tableName: "Users",
663+
expected: []string{"status"},
664+
},
665+
}
666+
667+
for _, tt := range tests {
668+
t.Run(tt.name, func(t *testing.T) {
669+
result := extractColumnCandidates(tt.indexName, tt.tableName, tt.hasTableNameColumn)
670+
671+
// Compare results
672+
if len(result) != len(tt.expected) {
673+
t.Errorf("extractColumnCandidates(%q, %q, %v) = %v, want %v",
674+
tt.indexName, tt.tableName, tt.hasTableNameColumn, result, tt.expected)
675+
return
676+
}
677+
678+
// Check each expected value is present
679+
for _, exp := range tt.expected {
680+
found := false
681+
for _, got := range result {
682+
if got == exp {
683+
found = true
684+
break
685+
}
686+
}
687+
if !found {
688+
t.Errorf("extractColumnCandidates(%q, %q, %v) = %v, missing %q",
689+
tt.indexName, tt.tableName, tt.hasTableNameColumn, result, exp)
690+
}
691+
}
692+
})
693+
}
694+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package nfs
2+
3+
import "testing"
4+
5+
// TestHasFormatExtension tests the hasFormatExtension helper function
6+
func TestHasFormatExtension(t *testing.T) {
7+
tests := []struct {
8+
name string
9+
filename string
10+
expected bool
11+
}{
12+
// Valid format extensions
13+
{"csv_extension", "123.csv", true},
14+
{"json_extension", "123.json", true},
15+
{"tsv_extension", "123.tsv", true},
16+
{"yaml_extension", "123.yaml", true},
17+
18+
// No extension
19+
{"no_extension", "123", false},
20+
{"directory_name", "users", false},
21+
22+
// Non-format extensions
23+
{"txt_extension", "file.txt", false},
24+
{"bin_extension", "data.bin", false},
25+
{"sql_extension", "query.sql", false},
26+
{"md_extension", "readme.md", false},
27+
28+
// Edge cases
29+
{"empty_string", "", false},
30+
{"dot_only", ".", false},
31+
{"hidden_file", ".json", true}, // .json is the extension
32+
{"double_extension", "file.tar.json", true},
33+
{"uppercase_ignored", "123.JSON", false}, // case sensitive
34+
{"uppercase_csv", "123.CSV", false},
35+
36+
// Primary key values that might look like extensions
37+
{"pk_with_dot", "user.name", false},
38+
{"uuid_style", "550e8400-e29b-41d4-a716-446655440000", false},
39+
{"numeric", "12345", false},
40+
}
41+
42+
for _, tt := range tests {
43+
t.Run(tt.name, func(t *testing.T) {
44+
result := hasFormatExtension(tt.filename)
45+
if result != tt.expected {
46+
t.Errorf("hasFormatExtension(%q) = %v, want %v", tt.filename, result, tt.expected)
47+
}
48+
})
49+
}
50+
}

0 commit comments

Comments
 (0)