Рыба проекта. Минимальная функциональность

This commit is contained in:
2026-08-03 22:22:24 +03:00
commit 8c8631ac9c
80 changed files with 10618 additions and 0 deletions
+249
View File
@@ -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())
}