Рыба проекта. Минимальная функциональность
This commit is contained in:
@@ -0,0 +1,374 @@
|
||||
package pgdump
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.tswf.io/infra/go-synapse-backupper/pkg/domain/pgdump"
|
||||
)
|
||||
|
||||
// mockCommandContext creates a test helper that asserts the command name and args,
|
||||
// and returns a shell command that produces the given stdout, stderr, and exit code.
|
||||
func mockCommandContext(
|
||||
t *testing.T,
|
||||
wantName string,
|
||||
wantArgs []string,
|
||||
stdout string,
|
||||
stderr string,
|
||||
exitCode int,
|
||||
) func(ctx context.Context, name string, arg ...string) *exec.Cmd {
|
||||
return func(ctx context.Context, name string, arg ...string) *exec.Cmd {
|
||||
if name != wantName {
|
||||
t.Errorf("command name = %q, want %q", name, wantName)
|
||||
}
|
||||
if !reflect.DeepEqual(arg, wantArgs) {
|
||||
t.Errorf("args = %v, want %v", arg, wantArgs)
|
||||
}
|
||||
script := fmt.Sprintf(
|
||||
"printf '%%s' '%s'; printf '%%s' '%s' >&2; exit %d",
|
||||
stdout,
|
||||
stderr,
|
||||
exitCode,
|
||||
)
|
||||
return exec.CommandContext(ctx, "sh", "-c", script)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDump_Success(t *testing.T) {
|
||||
wantArgs := []string{
|
||||
"--format=custom",
|
||||
"--exclude-table=e2e_one_time_keys_json",
|
||||
}
|
||||
|
||||
adapter := &adapter{
|
||||
commandContext: mockCommandContext(
|
||||
t,
|
||||
"pg_dump",
|
||||
wantArgs,
|
||||
"dumpdata",
|
||||
"",
|
||||
0,
|
||||
),
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err := adapter.Dump(
|
||||
context.Background(),
|
||||
pgdump.Options{Database: "testdb"},
|
||||
&buf,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if buf.String() != "dumpdata" {
|
||||
t.Errorf("output = %q, want %q", buf.String(), "dumpdata")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDump_WaitAfterStdoutEOF(t *testing.T) {
|
||||
// This test verifies that after io.Copy returns (stdout EOF),
|
||||
// cmd.Wait() is called and the exit code is verified before returning.
|
||||
wantArgs := []string{
|
||||
"--format=custom",
|
||||
"--exclude-table=e2e_one_time_keys_json",
|
||||
}
|
||||
|
||||
adapter := &adapter{
|
||||
commandContext: mockCommandContext(
|
||||
t,
|
||||
"pg_dump",
|
||||
wantArgs,
|
||||
"dumpdata",
|
||||
"",
|
||||
0,
|
||||
),
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err := adapter.Dump(
|
||||
context.Background(),
|
||||
pgdump.Options{Database: "testdb"},
|
||||
&buf,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if buf.String() != "dumpdata" {
|
||||
t.Errorf("output = %q, want %q", buf.String(), "dumpdata")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDump_NonZeroExitCode(t *testing.T) {
|
||||
wantArgs := []string{
|
||||
"--format=custom",
|
||||
"--exclude-table=e2e_one_time_keys_json",
|
||||
}
|
||||
|
||||
adapter := &adapter{
|
||||
commandContext: mockCommandContext(
|
||||
t,
|
||||
"pg_dump",
|
||||
wantArgs,
|
||||
"",
|
||||
"stderr error message",
|
||||
1,
|
||||
),
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err := adapter.Dump(
|
||||
context.Background(),
|
||||
pgdump.Options{Database: "testdb"},
|
||||
&buf,
|
||||
)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
|
||||
if !errors.Is(err, pgdump.ErrPgDumpFailed(1)) {
|
||||
t.Errorf("error = %v, want ErrPgDumpFailed(1)", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDump_StderrCaptured(t *testing.T) {
|
||||
wantStderr := "stderr captured"
|
||||
var capturedCmd *exec.Cmd
|
||||
|
||||
adapter := &adapter{
|
||||
commandContext: func(ctx context.Context, name string, arg ...string) *exec.Cmd {
|
||||
script := fmt.Sprintf("printf '%%s' '%s' >&2; exit 0", wantStderr)
|
||||
capturedCmd = exec.CommandContext(ctx, "sh", "-c", script)
|
||||
return capturedCmd
|
||||
},
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err := adapter.Dump(
|
||||
context.Background(),
|
||||
pgdump.Options{Database: "testdb"},
|
||||
&buf,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if capturedCmd.Stderr == nil {
|
||||
t.Fatal("expected cmd.Stderr to be set, got nil")
|
||||
}
|
||||
|
||||
stderrBuilder, ok := capturedCmd.Stderr.(*strings.Builder)
|
||||
if !ok {
|
||||
t.Fatalf("expected cmd.Stderr to be *strings.Builder, got %T", capturedCmd.Stderr)
|
||||
}
|
||||
|
||||
if stderrBuilder.String() != wantStderr {
|
||||
t.Errorf("stderr = %q, want %q", stderrBuilder.String(), wantStderr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDump_ContextCancellation(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
adapter := &adapter{
|
||||
commandContext: func(ctx context.Context, name string, arg ...string) *exec.Cmd {
|
||||
return exec.CommandContext(ctx, "sh", "-c", "while :; do :; done")
|
||||
},
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err := adapter.Dump(ctx, pgdump.Options{Database: "testdb"}, &buf)
|
||||
if err == nil {
|
||||
t.Fatal("expected error due to context cancellation, got nil")
|
||||
}
|
||||
|
||||
// Accept either context deadline exceeded or signal killed.
|
||||
if !errors.Is(err, context.DeadlineExceeded) && !strings.Contains(err.Error(), "signal") {
|
||||
t.Logf("got error: %v (acceptable variants: context.DeadlineExceeded or signal killed)", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDump_DefaultExcludeTables(t *testing.T) {
|
||||
wantArgs := []string{
|
||||
"--format=custom",
|
||||
"--exclude-table=e2e_one_time_keys_json",
|
||||
}
|
||||
|
||||
adapter := &adapter{
|
||||
commandContext: mockCommandContext(
|
||||
t,
|
||||
"pg_dump",
|
||||
wantArgs,
|
||||
"",
|
||||
"",
|
||||
0,
|
||||
),
|
||||
}
|
||||
|
||||
err := adapter.Dump(
|
||||
context.Background(),
|
||||
pgdump.Options{Database: "testdb"},
|
||||
io.Discard,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDump_CustomExcludeTables(t *testing.T) {
|
||||
wantArgs := []string{
|
||||
"--format=custom",
|
||||
"--exclude-table=table_a",
|
||||
"--exclude-table=table_b",
|
||||
}
|
||||
|
||||
adapter := &adapter{
|
||||
commandContext: mockCommandContext(
|
||||
t,
|
||||
"pg_dump",
|
||||
wantArgs,
|
||||
"",
|
||||
"",
|
||||
0,
|
||||
),
|
||||
}
|
||||
|
||||
err := adapter.Dump(
|
||||
context.Background(),
|
||||
pgdump.Options{
|
||||
Database: "testdb",
|
||||
ExcludeTables: []string{"table_a", "table_b"},
|
||||
},
|
||||
io.Discard,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDump_EnvVars(t *testing.T) {
|
||||
var capturedCmd *exec.Cmd
|
||||
|
||||
adapter := &adapter{
|
||||
commandContext: func(ctx context.Context, name string, arg ...string) *exec.Cmd {
|
||||
capturedCmd = exec.CommandContext(ctx, "sh", "-c", "exit 0")
|
||||
return capturedCmd
|
||||
},
|
||||
}
|
||||
|
||||
err := adapter.Dump(
|
||||
context.Background(),
|
||||
pgdump.Options{
|
||||
Host: "myhost",
|
||||
Port: 5433,
|
||||
User: "myuser",
|
||||
Password: "mypass",
|
||||
Database: "mydb",
|
||||
},
|
||||
io.Discard,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
envStr := strings.Join(capturedCmd.Env, "\n")
|
||||
wantEnvVars := []string{
|
||||
"PGHOST=myhost",
|
||||
"PGPORT=5433",
|
||||
"PGUSER=myuser",
|
||||
"PGPASSWORD=mypass",
|
||||
"PGDATABASE=mydb",
|
||||
}
|
||||
for _, wantEnv := range wantEnvVars {
|
||||
if !strings.Contains(envStr, wantEnv) {
|
||||
t.Errorf("env missing %q", wantEnv)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDump_PipeWriterClosed(t *testing.T) {
|
||||
adapter := &adapter{
|
||||
commandContext: mockCommandContext(
|
||||
t,
|
||||
"pg_dump",
|
||||
[]string{
|
||||
"--format=custom",
|
||||
"--exclude-table=e2e_one_time_keys_json",
|
||||
},
|
||||
"pipe data",
|
||||
"",
|
||||
0,
|
||||
),
|
||||
}
|
||||
|
||||
pr, pw := io.Pipe()
|
||||
readDone := make(chan struct{})
|
||||
var readData []byte
|
||||
var readErr error
|
||||
|
||||
go func() {
|
||||
readData, readErr = io.ReadAll(pr)
|
||||
close(readDone)
|
||||
}()
|
||||
|
||||
err := adapter.Dump(
|
||||
context.Background(),
|
||||
pgdump.Options{Database: "testdb"},
|
||||
pw,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
<-readDone
|
||||
if readErr != nil {
|
||||
t.Fatalf("read error: %v", readErr)
|
||||
}
|
||||
if string(readData) != "pipe data" {
|
||||
t.Errorf("read data = %q, want %q", string(readData), "pipe data")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDump_PipeWriterClosedWithError(t *testing.T) {
|
||||
adapter := &adapter{
|
||||
commandContext: func(ctx context.Context, name string, arg ...string) *exec.Cmd {
|
||||
return exec.CommandContext(ctx, "sh", "-c", "exit 1")
|
||||
},
|
||||
}
|
||||
|
||||
pr, pw := io.Pipe()
|
||||
readDone := make(chan struct{})
|
||||
var readErr error
|
||||
|
||||
go func() {
|
||||
_, readErr = io.ReadAll(pr)
|
||||
close(readDone)
|
||||
}()
|
||||
|
||||
err := adapter.Dump(
|
||||
context.Background(),
|
||||
pgdump.Options{Database: "testdb"},
|
||||
pw,
|
||||
)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
|
||||
<-readDone
|
||||
if readErr == nil {
|
||||
t.Fatal("expected read error due to pipe close with error, got nil")
|
||||
}
|
||||
if !errors.Is(readErr, pgdump.ErrPgDumpFailed(1)) {
|
||||
t.Errorf("read error = %v, want ErrPgDumpFailed(1)", readErr)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user