250 lines
4.8 KiB
Go
250 lines
4.8 KiB
Go
package pipeline
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"runtime"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.tswf.io/infra/go-synapse-backupper/pkg/domain"
|
|
"git.tswf.io/infra/go-synapse-backupper/pkg/domain/crypto"
|
|
"git.tswf.io/infra/go-synapse-backupper/pkg/domain/pgdump"
|
|
)
|
|
|
|
var (
|
|
errPgDumpFailed = errors.New("pg_dump failed")
|
|
errEncryptFailed = errors.New("encryption failed")
|
|
)
|
|
|
|
type fakeSink struct {
|
|
transaction *fakeSinkTx
|
|
}
|
|
|
|
func (sink *fakeSink) Begin(key string) (domain.SinkTx, error) {
|
|
sink.transaction = &fakeSinkTx{}
|
|
return sink.transaction, nil
|
|
}
|
|
|
|
func (sink *fakeSink) List(prefix string) ([]string, error) {
|
|
return make([]string, 0), nil
|
|
}
|
|
|
|
func (sink *fakeSink) Remove(key string) error {
|
|
return nil
|
|
}
|
|
|
|
type fakeSinkTx struct {
|
|
committed bool
|
|
aborted bool
|
|
data []byte
|
|
}
|
|
|
|
func (transaction *fakeSinkTx) Write(p []byte) (int, error) {
|
|
transaction.data = append(transaction.data, p...)
|
|
return len(p), nil
|
|
}
|
|
|
|
func (transaction *fakeSinkTx) Commit() error {
|
|
transaction.committed = true
|
|
return nil
|
|
}
|
|
|
|
func (transaction *fakeSinkTx) Abort() error {
|
|
transaction.aborted = true
|
|
return nil
|
|
}
|
|
|
|
type fakeDumper struct {
|
|
writeBytes int
|
|
returnErr error
|
|
closePipe bool
|
|
}
|
|
|
|
func (dumper *fakeDumper) Dump(
|
|
ctx context.Context,
|
|
opts pgdump.Options,
|
|
writer io.Writer,
|
|
) error {
|
|
if dumper.writeBytes > 0 {
|
|
data := make([]byte, dumper.writeBytes)
|
|
if _, err := writer.Write(data); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if dumper.closePipe {
|
|
if closer, ok := writer.(io.Closer); ok {
|
|
_ = closer.Close()
|
|
}
|
|
}
|
|
return dumper.returnErr
|
|
}
|
|
|
|
type fakeEncryptor struct {
|
|
readBytes int
|
|
returnErr error
|
|
}
|
|
|
|
func (encryptor *fakeEncryptor) Encrypt(
|
|
plaintext io.Reader,
|
|
recipients []crypto.RecipientPub,
|
|
sink io.Writer,
|
|
rand io.Reader,
|
|
) error {
|
|
if encryptor.readBytes > 0 {
|
|
buf := make([]byte, encryptor.readBytes)
|
|
if _, err := io.ReadFull(plaintext, buf); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return encryptor.returnErr
|
|
}
|
|
|
|
func countGoroutines() int {
|
|
return runtime.NumGoroutine()
|
|
}
|
|
|
|
func waitForGoroutinesStable(baseline int) bool {
|
|
deadline := time.Now().Add(time.Second)
|
|
for time.Now().Before(deadline) {
|
|
time.Sleep(10 * time.Millisecond)
|
|
if runtime.NumGoroutine() <= baseline {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func TestPipeline_DumpReturnsWithoutClosing(t *testing.T) {
|
|
baseline := countGoroutines()
|
|
|
|
sink := &fakeSink{}
|
|
dumper := &fakeDumper{
|
|
writeBytes: 4 * 1024,
|
|
returnErr: errPgDumpFailed,
|
|
closePipe: false,
|
|
}
|
|
encryptor := &fakeEncryptor{
|
|
readBytes: 4 * 1024,
|
|
}
|
|
|
|
runner := NewRunner(
|
|
WithDumper(dumper),
|
|
WithEncryptor(encryptor),
|
|
)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
defer cancel()
|
|
|
|
err := runner.Run(
|
|
ctx,
|
|
pgdump.Options{Key: "backup.sql"},
|
|
make([]crypto.RecipientPub, 0),
|
|
sink,
|
|
nil,
|
|
)
|
|
|
|
if !errors.Is(err, errPgDumpFailed) {
|
|
t.Fatalf("expected errPgDumpFailed, got %v", err)
|
|
}
|
|
|
|
if !sink.transaction.aborted {
|
|
t.Fatalf("expected transaction to be aborted on error")
|
|
}
|
|
|
|
if waitForGoroutinesStable(baseline) {
|
|
return
|
|
}
|
|
|
|
t.Fatalf("goroutine leak detected: baseline %d, current %d", baseline, countGoroutines())
|
|
}
|
|
|
|
func TestPipeline_EncryptFailsFirst(t *testing.T) {
|
|
baseline := countGoroutines()
|
|
|
|
sink := &fakeSink{}
|
|
dumper := &fakeDumper{
|
|
writeBytes: 64 * 1024,
|
|
returnErr: nil,
|
|
closePipe: false,
|
|
}
|
|
encryptor := &fakeEncryptor{
|
|
readBytes: 1024,
|
|
returnErr: errEncryptFailed,
|
|
}
|
|
|
|
runner := NewRunner(
|
|
WithDumper(dumper),
|
|
WithEncryptor(encryptor),
|
|
)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
defer cancel()
|
|
|
|
err := runner.Run(
|
|
ctx,
|
|
pgdump.Options{Key: "backup.sql"},
|
|
make([]crypto.RecipientPub, 0),
|
|
sink,
|
|
nil,
|
|
)
|
|
|
|
if !errors.Is(err, errEncryptFailed) {
|
|
t.Fatalf("expected errEncryptFailed, got %v", err)
|
|
}
|
|
|
|
if !sink.transaction.aborted {
|
|
t.Fatalf("expected transaction to be aborted on error")
|
|
}
|
|
|
|
if waitForGoroutinesStable(baseline) {
|
|
return
|
|
}
|
|
|
|
t.Fatalf("goroutine leak detected: baseline %d, current %d", baseline, countGoroutines())
|
|
}
|
|
|
|
func TestPipeline_SuccessfulRunCommits(t *testing.T) {
|
|
baseline := countGoroutines()
|
|
|
|
sink := &fakeSink{}
|
|
dumper := &fakeDumper{
|
|
writeBytes: 4 * 1024,
|
|
returnErr: nil,
|
|
closePipe: true,
|
|
}
|
|
encryptor := &fakeEncryptor{
|
|
readBytes: 4 * 1024,
|
|
}
|
|
|
|
runner := NewRunner(
|
|
WithDumper(dumper),
|
|
WithEncryptor(encryptor),
|
|
)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
defer cancel()
|
|
|
|
err := runner.Run(
|
|
ctx,
|
|
pgdump.Options{Key: "backup.sql"},
|
|
make([]crypto.RecipientPub, 0),
|
|
sink,
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if !sink.transaction.committed {
|
|
t.Fatalf("expected transaction to be committed on success")
|
|
}
|
|
|
|
if waitForGoroutinesStable(baseline) {
|
|
return
|
|
}
|
|
|
|
t.Fatalf("goroutine leak detected: baseline %d, current %d", baseline, countGoroutines())
|
|
}
|