Рыба проекта. Минимальная функциональность
This commit is contained in:
@@ -0,0 +1,249 @@
|
||||
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())
|
||||
}
|
||||
Reference in New Issue
Block a user