diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index 0c5e262..54395f9 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -26,7 +26,9 @@ jobs: uses: golangci/golangci-lint-action@v6 with: version: latest - - name: Tests + - name: Unit Tests + run: make test-unit-codecov + - name: Integration Tests env: SQLITE_CONNECTION_STRING: ${{ vars.SQLITE_CONNECTION_STRING }} SQLITE_USER: ${{ secrets.SQLITE_USER }} @@ -40,4 +42,4 @@ jobs: uses: codecov/codecov-action@v4.0.1 with: token: ${{ secrets.CODECOV_TOKEN }} - files: ./test/coverage.out + files: ./coverage-unit.out,./test/coverage.out diff --git a/Makefile b/Makefile index 3b3ae20..4591f5f 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,14 @@ setup-ide: cd test; go mod tidy cd cli; go mod tidy -# Test SDK +# Unit tests (root package) +test-unit: + go test -v . + +test-unit-codecov: + go test -v -race -coverprofile=coverage-unit.out -covermode=atomic . + +# Integration tests (test/ directory) test: cd test; go mod tidy && go test -v . diff --git a/chunk.go b/chunk.go index 7eb120c..8140367 100644 --- a/chunk.go +++ b/chunk.go @@ -22,6 +22,7 @@ import ( "fmt" "io" "net" + "reflect" "strconv" "strings" "time" @@ -221,20 +222,49 @@ func (this *Value) readBufferAt(chunk *Chunk, offset uint64) (uint64, error) { return 0, errors.New("Unsuported type") } -func protocolBufferFromValue(v interface{}) [][]byte { +func protocolBufferFromValue(v interface{}) ([][]byte, error) { switch v := v.(type) { case nil: - return protocolBufferFromNull() - case int, int8, int16, int32, int64: - return protocolBufferFromInt(v) - case float32, float64: - return protocolBufferFromFloat(v) + return protocolBufferFromNull(), nil + case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: + return protocolBufferFromInt(v), nil + case float32: + return protocolBufferFromFloat(float64(v)), nil + case float64: + return protocolBufferFromFloat(v), nil case string: - return protocolBufferFromString(v, true) + return protocolBufferFromString(v, true), nil case []byte: - return protocolBufferFromBytes(v) + return protocolBufferFromBytes(v), nil default: - return make([][]byte, 0) + rv := reflect.ValueOf(v) + if !rv.IsValid() { + return protocolBufferFromNull(), nil + } + if rv.Kind() == reflect.Pointer { + if rv.IsNil() { + return protocolBufferFromNull(), nil + } + return protocolBufferFromValue(rv.Elem().Interface()) + } + + switch rv.Kind() { + case reflect.String: + return protocolBufferFromString(rv.String(), true), nil + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return protocolBufferFromInt(rv.Int()), nil + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return protocolBufferFromInt(rv.Uint()), nil + case reflect.Float32, reflect.Float64: + return protocolBufferFromFloat(rv.Convert(reflect.TypeOf(float64(0))).Float()), nil + case reflect.Bool: + if rv.Bool() { + return protocolBufferFromInt(1), nil + } + return protocolBufferFromInt(0), nil + default: + return nil, fmt.Errorf("unsupported parameter type %T", v) + } } } @@ -371,7 +401,11 @@ func (this *SQCloud) sendArray(command string, values []interface{}) (int, error // convert values to buffers encoded with whe sqlitecloud protocol buffers := [][]byte{protocolBufferFromString(command, true)[0]} for _, v := range values { - buffers = append(buffers, protocolBufferFromValue(v)...) + valueBuffers, err := protocolBufferFromValue(v) + if err != nil { + return 0, err + } + buffers = append(buffers, valueBuffers...) } // calculate the array header diff --git a/chunk_internal_test.go b/chunk_internal_test.go new file mode 100644 index 0000000..e1a5021 --- /dev/null +++ b/chunk_internal_test.go @@ -0,0 +1,158 @@ +package sqlitecloud + +import ( + "fmt" + "strings" + "testing" +) + +type testStringEnum string +type testIntEnum int + +func TestProtocolBufferFromValue(t *testing.T) { + type unsupported struct{} + intVal := 42 + strVal := "hello" + + tests := []struct { + name string + value interface{} + wantLen int + wantType byte + wantError bool + }{ + {"nil", nil, 1, CMD_NULL, false}, + {"string", "hello", 1, CMD_ZEROSTRING, false}, + {"int", int(42), 1, CMD_INT, false}, + {"int8", int8(8), 1, CMD_INT, false}, + {"int16", int16(16), 1, CMD_INT, false}, + {"int32", int32(32), 1, CMD_INT, false}, + {"int64", int64(64), 1, CMD_INT, false}, + {"uint", uint(1), 1, CMD_INT, false}, + {"uint8", uint8(1), 1, CMD_INT, false}, + {"uint16", uint16(1), 1, CMD_INT, false}, + {"uint32", uint32(1), 1, CMD_INT, false}, + {"uint64", uint64(1), 1, CMD_INT, false}, + {"float32", float32(3.14), 1, CMD_FLOAT, false}, + {"float64", float64(2.71), 1, CMD_FLOAT, false}, + {"[]byte", []byte("blob"), 2, CMD_BLOB, false}, + {"bool true", true, 1, CMD_INT, false}, + {"bool false", false, 1, CMD_INT, false}, + {"*int", &intVal, 1, CMD_INT, false}, + {"*string", &strVal, 1, CMD_ZEROSTRING, false}, + {"*int nil", (*int)(nil), 1, CMD_NULL, false}, + {"*string nil", (*string)(nil), 1, CMD_NULL, false}, + {"unsupported", unsupported{}, 0, 0, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + buffers, err := protocolBufferFromValue(tt.value) + if tt.wantError { + if err == nil { + t.Fatalf("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(buffers) != tt.wantLen { + t.Fatalf("got %d buffers, want %d", len(buffers), tt.wantLen) + } + if tt.wantLen > 0 && buffers[0][0] != tt.wantType { + t.Fatalf("got first buffer type %q, want %q", buffers[0][0], tt.wantType) + } + }) + } +} + +func TestProtocolBufferFromValueSupportsStringAlias(t *testing.T) { + val := testStringEnum("active") + buffers, err := protocolBufferFromValue(val) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if len(buffers) != 1 { + t.Fatalf("expected 1 buffer, got %d", len(buffers)) + } + got := string(buffers[0]) + want := fmt.Sprintf("%c%d %s\x00", CMD_ZEROSTRING, len("active")+1, "active") + if got != want { + t.Fatalf("unexpected encoded value: want %q got %q", want, got) + } +} + +func TestProtocolBufferFromValueSupportsIntAliasPointer(t *testing.T) { + raw := testIntEnum(7) + buffers, err := protocolBufferFromValue(&raw) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if len(buffers) != 1 { + t.Fatalf("expected 1 buffer, got %d", len(buffers)) + } + got := string(buffers[0]) + want := fmt.Sprintf("%c%d ", CMD_INT, 7) + if got != want { + t.Fatalf("unexpected encoded value: want %q got %q", want, got) + } +} + +func TestProtocolBufferFromValueSupportsFloat32(t *testing.T) { + buffers, err := protocolBufferFromValue(float32(2.5)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if len(buffers) != 1 { + t.Fatalf("expected 1 buffer, got %d", len(buffers)) + } + got := string(buffers[0]) + if !strings.HasPrefix(got, fmt.Sprintf("%c", CMD_FLOAT)) { + t.Fatalf("expected float buffer prefix, got %q", got) + } +} + +func TestProtocolBufferFromValueUnsupportedTypeReturnsError(t *testing.T) { + type unsupported struct { + Name string + } + + _, err := protocolBufferFromValue(unsupported{Name: "x"}) + if err == nil { + t.Fatalf("expected error for unsupported type") + } +} + +func TestProtocolBufferFromValueMixedArrayNoSilentDrops(t *testing.T) { + pInt := 99 + values := []interface{}{ + "hello", + int(42), + nil, + &pInt, + float64(3), + uint(7), + []byte("x"), + true, + } + + buffers := [][]byte{} + for i, v := range values { + valueBuffers, err := protocolBufferFromValue(v) + if err != nil { + t.Fatalf("unexpected error at index %d (%T): %v", i, v, err) + } + if len(valueBuffers) == 0 { + t.Fatalf("value at index %d produced zero buffers", i) + } + buffers = append(buffers, valueBuffers...) + } + + if len(buffers) < len(values) { + t.Fatalf("got %d total buffers, expected at least %d", len(buffers), len(values)) + } +}