2
2
mirror of https://github.com/octoleo/restic.git synced 2024-11-26 06:46:34 +00:00

Merge pull request #4298 from restic/backend-parseconfig-cleanup

Unified and slightly type-safer backend config parsing
This commit is contained in:
Michael Eischer 2023-06-08 12:02:27 +02:00 committed by GitHub
commit e14ccb1142
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 511 additions and 637 deletions

View File

@ -535,147 +535,21 @@ func OpenRepository(ctx context.Context, opts GlobalOptions) (*repository.Reposi
}
func parseConfig(loc location.Location, opts options.Options) (interface{}, error) {
// only apply options for a particular backend here
opts = opts.Extract(loc.Scheme)
switch loc.Scheme {
case "local":
cfg := loc.Config.(local.Config)
if err := opts.Apply(loc.Scheme, &cfg); err != nil {
cfg := loc.Config
if cfg, ok := cfg.(restic.ApplyEnvironmenter); ok {
if err := cfg.ApplyEnvironment(""); err != nil {
return nil, err
}
debug.Log("opening local repository at %#v", cfg)
return cfg, nil
case "sftp":
cfg := loc.Config.(sftp.Config)
if err := opts.Apply(loc.Scheme, &cfg); err != nil {
return nil, err
}
debug.Log("opening sftp repository at %#v", cfg)
return cfg, nil
case "s3":
cfg := loc.Config.(s3.Config)
if cfg.KeyID == "" {
cfg.KeyID = os.Getenv("AWS_ACCESS_KEY_ID")
}
if cfg.Secret.String() == "" {
cfg.Secret = options.NewSecretString(os.Getenv("AWS_SECRET_ACCESS_KEY"))
}
if cfg.KeyID == "" && cfg.Secret.String() != "" {
return nil, errors.Fatalf("unable to open S3 backend: Key ID ($AWS_ACCESS_KEY_ID) is empty")
} else if cfg.KeyID != "" && cfg.Secret.String() == "" {
return nil, errors.Fatalf("unable to open S3 backend: Secret ($AWS_SECRET_ACCESS_KEY) is empty")
}
if cfg.Region == "" {
cfg.Region = os.Getenv("AWS_DEFAULT_REGION")
}
if err := opts.Apply(loc.Scheme, &cfg); err != nil {
return nil, err
}
debug.Log("opening s3 repository at %#v", cfg)
return cfg, nil
case "gs":
cfg := loc.Config.(gs.Config)
if cfg.ProjectID == "" {
cfg.ProjectID = os.Getenv("GOOGLE_PROJECT_ID")
}
if err := opts.Apply(loc.Scheme, &cfg); err != nil {
return nil, err
}
debug.Log("opening gs repository at %#v", cfg)
return cfg, nil
case "azure":
cfg := loc.Config.(azure.Config)
if cfg.AccountName == "" {
cfg.AccountName = os.Getenv("AZURE_ACCOUNT_NAME")
}
if cfg.AccountKey.String() == "" {
cfg.AccountKey = options.NewSecretString(os.Getenv("AZURE_ACCOUNT_KEY"))
}
if cfg.AccountSAS.String() == "" {
cfg.AccountSAS = options.NewSecretString(os.Getenv("AZURE_ACCOUNT_SAS"))
}
if err := opts.Apply(loc.Scheme, &cfg); err != nil {
return nil, err
}
debug.Log("opening gs repository at %#v", cfg)
return cfg, nil
case "swift":
cfg := loc.Config.(swift.Config)
if err := swift.ApplyEnvironment("", &cfg); err != nil {
return nil, err
}
if err := opts.Apply(loc.Scheme, &cfg); err != nil {
return nil, err
}
debug.Log("opening swift repository at %#v", cfg)
return cfg, nil
case "b2":
cfg := loc.Config.(b2.Config)
if cfg.AccountID == "" {
cfg.AccountID = os.Getenv("B2_ACCOUNT_ID")
}
if cfg.AccountID == "" {
return nil, errors.Fatalf("unable to open B2 backend: Account ID ($B2_ACCOUNT_ID) is empty")
}
if cfg.Key.String() == "" {
cfg.Key = options.NewSecretString(os.Getenv("B2_ACCOUNT_KEY"))
}
if cfg.Key.String() == "" {
return nil, errors.Fatalf("unable to open B2 backend: Key ($B2_ACCOUNT_KEY) is empty")
}
if err := opts.Apply(loc.Scheme, &cfg); err != nil {
return nil, err
}
debug.Log("opening b2 repository at %#v", cfg)
return cfg, nil
case "rest":
cfg := loc.Config.(rest.Config)
if err := opts.Apply(loc.Scheme, &cfg); err != nil {
return nil, err
}
debug.Log("opening rest repository at %#v", cfg)
return cfg, nil
case "rclone":
cfg := loc.Config.(rclone.Config)
if err := opts.Apply(loc.Scheme, &cfg); err != nil {
return nil, err
}
debug.Log("opening rest repository at %#v", cfg)
return cfg, nil
}
return nil, errors.Fatalf("invalid backend: %q", loc.Scheme)
// only apply options for a particular backend here
opts = opts.Extract(loc.Scheme)
if err := opts.Apply(loc.Scheme, cfg); err != nil {
return nil, err
}
debug.Log("opening %v repository at %#v", loc.Scheme, cfg)
return cfg, nil
}
// Open the backend specified by a location config.
@ -704,23 +578,23 @@ func open(ctx context.Context, s string, gopts GlobalOptions, opts options.Optio
switch loc.Scheme {
case "local":
be, err = local.Open(ctx, cfg.(local.Config))
be, err = local.Open(ctx, *cfg.(*local.Config))
case "sftp":
be, err = sftp.Open(ctx, cfg.(sftp.Config))
be, err = sftp.Open(ctx, *cfg.(*sftp.Config))
case "s3":
be, err = s3.Open(ctx, cfg.(s3.Config), rt)
be, err = s3.Open(ctx, *cfg.(*s3.Config), rt)
case "gs":
be, err = gs.Open(cfg.(gs.Config), rt)
be, err = gs.Open(*cfg.(*gs.Config), rt)
case "azure":
be, err = azure.Open(ctx, cfg.(azure.Config), rt)
be, err = azure.Open(ctx, *cfg.(*azure.Config), rt)
case "swift":
be, err = swift.Open(ctx, cfg.(swift.Config), rt)
be, err = swift.Open(ctx, *cfg.(*swift.Config), rt)
case "b2":
be, err = b2.Open(ctx, cfg.(b2.Config), rt)
be, err = b2.Open(ctx, *cfg.(*b2.Config), rt)
case "rest":
be, err = rest.Open(cfg.(rest.Config), rt)
be, err = rest.Open(*cfg.(*rest.Config), rt)
case "rclone":
be, err = rclone.Open(cfg.(rclone.Config), lim)
be, err = rclone.Open(*cfg.(*rclone.Config), lim)
default:
return nil, errors.Fatalf("invalid backend: %q", loc.Scheme)
@ -780,23 +654,23 @@ func create(ctx context.Context, s string, opts options.Options) (restic.Backend
var be restic.Backend
switch loc.Scheme {
case "local":
be, err = local.Create(ctx, cfg.(local.Config))
be, err = local.Create(ctx, *cfg.(*local.Config))
case "sftp":
be, err = sftp.Create(ctx, cfg.(sftp.Config))
be, err = sftp.Create(ctx, *cfg.(*sftp.Config))
case "s3":
be, err = s3.Create(ctx, cfg.(s3.Config), rt)
be, err = s3.Create(ctx, *cfg.(*s3.Config), rt)
case "gs":
be, err = gs.Create(ctx, cfg.(gs.Config), rt)
be, err = gs.Create(ctx, *cfg.(*gs.Config), rt)
case "azure":
be, err = azure.Create(ctx, cfg.(azure.Config), rt)
be, err = azure.Create(ctx, *cfg.(*azure.Config), rt)
case "swift":
be, err = swift.Open(ctx, cfg.(swift.Config), rt)
be, err = swift.Open(ctx, *cfg.(*swift.Config), rt)
case "b2":
be, err = b2.Create(ctx, cfg.(b2.Config), rt)
be, err = b2.Create(ctx, *cfg.(*b2.Config), rt)
case "rest":
be, err = rest.Create(ctx, cfg.(rest.Config), rt)
be, err = rest.Create(ctx, *cfg.(*rest.Config), rt)
case "rclone":
be, err = rclone.Create(ctx, cfg.(rclone.Config))
be, err = rclone.Create(ctx, *cfg.(*rclone.Config))
default:
debug.Log("invalid repository scheme: %v", s)
return nil, errors.Fatalf("invalid scheme %q", loc.Scheme)

View File

@ -18,34 +18,34 @@ import (
rtest "github.com/restic/restic/internal/test"
)
func newAzureTestSuite(t testing.TB) *test.Suite {
func newAzureTestSuite(t testing.TB) *test.Suite[azure.Config] {
tr, err := backend.Transport(backend.TransportOptions{})
if err != nil {
t.Fatalf("cannot create transport for tests: %v", err)
}
return &test.Suite{
return &test.Suite[azure.Config]{
// do not use excessive data
MinimalData: true,
// NewConfig returns a config for a new temporary backend that will be used in tests.
NewConfig: func() (interface{}, error) {
azcfg, err := azure.ParseConfig(os.Getenv("RESTIC_TEST_AZURE_REPOSITORY"))
NewConfig: func() (*azure.Config, error) {
cfg, err := azure.ParseConfig(os.Getenv("RESTIC_TEST_AZURE_REPOSITORY"))
if err != nil {
return nil, err
}
err = cfg.ApplyEnvironment("RESTIC_TEST_")
if err != nil {
return nil, err
}
cfg := azcfg.(azure.Config)
cfg.AccountName = os.Getenv("RESTIC_TEST_AZURE_ACCOUNT_NAME")
cfg.AccountKey = options.NewSecretString(os.Getenv("RESTIC_TEST_AZURE_ACCOUNT_KEY"))
cfg.Prefix = fmt.Sprintf("test-%d", time.Now().UnixNano())
return cfg, nil
},
// CreateFn is a function that creates a temporary repository for the tests.
Create: func(config interface{}) (restic.Backend, error) {
cfg := config.(azure.Config)
Create: func(cfg azure.Config) (restic.Backend, error) {
ctx := context.TODO()
be, err := azure.Create(ctx, cfg, tr)
if err != nil {
@ -65,15 +65,13 @@ func newAzureTestSuite(t testing.TB) *test.Suite {
},
// OpenFn is a function that opens a previously created temporary repository.
Open: func(config interface{}) (restic.Backend, error) {
cfg := config.(azure.Config)
Open: func(cfg azure.Config) (restic.Backend, error) {
ctx := context.TODO()
return azure.Open(ctx, cfg, tr)
},
// CleanupFn removes data created during the tests.
Cleanup: func(config interface{}) error {
cfg := config.(azure.Config)
Cleanup: func(cfg azure.Config) error {
ctx := context.TODO()
be, err := azure.Open(ctx, cfg, tr)
if err != nil {
@ -141,12 +139,11 @@ func TestUploadLargeFile(t *testing.T) {
return
}
azcfg, err := azure.ParseConfig(os.Getenv("RESTIC_TEST_AZURE_REPOSITORY"))
cfg, err := azure.ParseConfig(os.Getenv("RESTIC_TEST_AZURE_REPOSITORY"))
if err != nil {
t.Fatal(err)
}
cfg := azcfg.(azure.Config)
cfg.AccountName = os.Getenv("RESTIC_TEST_AZURE_ACCOUNT_NAME")
cfg.AccountKey = options.NewSecretString(os.Getenv("RESTIC_TEST_AZURE_ACCOUNT_KEY"))
cfg.Prefix = fmt.Sprintf("test-upload-large-%d", time.Now().UnixNano())
@ -156,7 +153,7 @@ func TestUploadLargeFile(t *testing.T) {
t.Fatal(err)
}
be, err := azure.Create(ctx, cfg, tr)
be, err := azure.Create(ctx, *cfg, tr)
if err != nil {
t.Fatal(err)
}

View File

@ -1,11 +1,13 @@
package azure
import (
"os"
"path"
"strings"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/options"
"github.com/restic/restic/internal/restic"
)
// Config contains all configuration necessary to connect to an azure compatible
@ -33,7 +35,7 @@ func init() {
// ParseConfig parses the string s and extracts the azure config. The
// configuration format is azure:containerName:/[prefix].
func ParseConfig(s string) (interface{}, error) {
func ParseConfig(s string) (*Config, error) {
if !strings.HasPrefix(s, "azure:") {
return nil, errors.New("azure: invalid format")
}
@ -51,5 +53,23 @@ func ParseConfig(s string) (interface{}, error) {
cfg := NewConfig()
cfg.Container = container
cfg.Prefix = prefix
return cfg, nil
return &cfg, nil
}
var _ restic.ApplyEnvironmenter = &Config{}
// ApplyEnvironment saves values from the environment to the config.
func (cfg *Config) ApplyEnvironment(prefix string) error {
if cfg.AccountName == "" {
cfg.AccountName = os.Getenv(prefix + "AZURE_ACCOUNT_NAME")
}
if cfg.AccountKey.String() == "" {
cfg.AccountKey = options.NewSecretString(os.Getenv(prefix + "AZURE_ACCOUNT_KEY"))
}
if cfg.AccountSAS.String() == "" {
cfg.AccountSAS = options.NewSecretString(os.Getenv(prefix + "AZURE_ACCOUNT_SAS"))
}
return nil
}

View File

@ -1,22 +1,23 @@
package azure
import "testing"
import (
"testing"
var configTests = []struct {
s string
cfg Config
}{
{"azure:container-name:/", Config{
"github.com/restic/restic/internal/backend/test"
)
var configTests = []test.ConfigTestData[Config]{
{S: "azure:container-name:/", Cfg: Config{
Container: "container-name",
Prefix: "",
Connections: 5,
}},
{"azure:container-name:/prefix/directory", Config{
{S: "azure:container-name:/prefix/directory", Cfg: Config{
Container: "container-name",
Prefix: "prefix/directory",
Connections: 5,
}},
{"azure:container-name:/prefix/directory/", Config{
{S: "azure:container-name:/prefix/directory/", Cfg: Config{
Container: "container-name",
Prefix: "prefix/directory",
Connections: 5,
@ -24,17 +25,5 @@ var configTests = []struct {
}
func TestParseConfig(t *testing.T) {
for i, test := range configTests {
cfg, err := ParseConfig(test.s)
if err != nil {
t.Errorf("test %d:%s failed: %v", i, test.s, err)
continue
}
if cfg != test.cfg {
t.Errorf("test %d:\ninput:\n %s\n wrong config, want:\n %v\ngot:\n %v",
i, test.s, test.cfg, cfg)
continue
}
}
test.ParseConfigTester(t, ParseConfig, configTests)
}

View File

@ -10,19 +10,18 @@ import (
"github.com/restic/restic/internal/backend"
"github.com/restic/restic/internal/backend/b2"
"github.com/restic/restic/internal/backend/test"
"github.com/restic/restic/internal/options"
"github.com/restic/restic/internal/restic"
rtest "github.com/restic/restic/internal/test"
)
func newB2TestSuite(t testing.TB) *test.Suite {
func newB2TestSuite(t testing.TB) *test.Suite[b2.Config] {
tr, err := backend.Transport(backend.TransportOptions{})
if err != nil {
t.Fatalf("cannot create transport for tests: %v", err)
}
return &test.Suite{
return &test.Suite[b2.Config]{
// do not use excessive data
MinimalData: true,
@ -30,34 +29,33 @@ func newB2TestSuite(t testing.TB) *test.Suite {
WaitForDelayedRemoval: 10 * time.Second,
// NewConfig returns a config for a new temporary backend that will be used in tests.
NewConfig: func() (interface{}, error) {
b2cfg, err := b2.ParseConfig(os.Getenv("RESTIC_TEST_B2_REPOSITORY"))
NewConfig: func() (*b2.Config, error) {
cfg, err := b2.ParseConfig(os.Getenv("RESTIC_TEST_B2_REPOSITORY"))
if err != nil {
return nil, err
}
err = cfg.ApplyEnvironment("RESTIC_TEST_")
if err != nil {
return nil, err
}
cfg := b2cfg.(b2.Config)
cfg.AccountID = os.Getenv("RESTIC_TEST_B2_ACCOUNT_ID")
cfg.Key = options.NewSecretString(os.Getenv("RESTIC_TEST_B2_ACCOUNT_KEY"))
cfg.Prefix = fmt.Sprintf("test-%d", time.Now().UnixNano())
return cfg, nil
},
// CreateFn is a function that creates a temporary repository for the tests.
Create: func(config interface{}) (restic.Backend, error) {
cfg := config.(b2.Config)
Create: func(cfg b2.Config) (restic.Backend, error) {
return b2.Create(context.Background(), cfg, tr)
},
// OpenFn is a function that opens a previously created temporary repository.
Open: func(config interface{}) (restic.Backend, error) {
cfg := config.(b2.Config)
Open: func(cfg b2.Config) (restic.Backend, error) {
return b2.Open(context.Background(), cfg, tr)
},
// CleanupFn removes data created during the tests.
Cleanup: func(config interface{}) error {
cfg := config.(b2.Config)
Cleanup: func(cfg b2.Config) error {
be, err := b2.Open(context.Background(), cfg, tr)
if err != nil {
return err

View File

@ -1,12 +1,14 @@
package b2
import (
"os"
"path"
"regexp"
"strings"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/options"
"github.com/restic/restic/internal/restic"
)
// Config contains all configuration necessary to connect to an b2 compatible
@ -58,7 +60,7 @@ func checkBucketName(name string) error {
// ParseConfig parses the string s and extracts the b2 config. The supported
// configuration format is b2:bucketname/prefix. If no prefix is given the
// prefix "restic" will be used.
func ParseConfig(s string) (interface{}, error) {
func ParseConfig(s string) (*Config, error) {
if !strings.HasPrefix(s, "b2:") {
return nil, errors.New("invalid format, want: b2:bucket-name[:path]")
}
@ -77,5 +79,27 @@ func ParseConfig(s string) (interface{}, error) {
cfg.Bucket = bucket
cfg.Prefix = prefix
return cfg, nil
return &cfg, nil
}
var _ restic.ApplyEnvironmenter = &Config{}
// ApplyEnvironment saves values from the environment to the config.
func (cfg *Config) ApplyEnvironment(prefix string) error {
if cfg.AccountID == "" {
cfg.AccountID = os.Getenv(prefix + "B2_ACCOUNT_ID")
}
if cfg.AccountID == "" {
return errors.Fatalf("unable to open B2 backend: Account ID ($B2_ACCOUNT_ID) is empty")
}
if cfg.Key.String() == "" {
cfg.Key = options.NewSecretString(os.Getenv(prefix + "B2_ACCOUNT_KEY"))
}
if cfg.Key.String() == "" {
return errors.Fatalf("unable to open B2 backend: Key ($B2_ACCOUNT_KEY) is empty")
}
return nil
}

View File

@ -1,37 +1,38 @@
package b2
import "testing"
import (
"testing"
var configTests = []struct {
s string
cfg Config
}{
{"b2:bucketname", Config{
"github.com/restic/restic/internal/backend/test"
)
var configTests = []test.ConfigTestData[Config]{
{S: "b2:bucketname", Cfg: Config{
Bucket: "bucketname",
Prefix: "",
Connections: 5,
}},
{"b2:bucketname:", Config{
{S: "b2:bucketname:", Cfg: Config{
Bucket: "bucketname",
Prefix: "",
Connections: 5,
}},
{"b2:bucketname:/prefix/directory", Config{
{S: "b2:bucketname:/prefix/directory", Cfg: Config{
Bucket: "bucketname",
Prefix: "prefix/directory",
Connections: 5,
}},
{"b2:foobar", Config{
{S: "b2:foobar", Cfg: Config{
Bucket: "foobar",
Prefix: "",
Connections: 5,
}},
{"b2:foobar:", Config{
{S: "b2:foobar:", Cfg: Config{
Bucket: "foobar",
Prefix: "",
Connections: 5,
}},
{"b2:foobar:/", Config{
{S: "b2:foobar:/", Cfg: Config{
Bucket: "foobar",
Prefix: "",
Connections: 5,
@ -39,19 +40,7 @@ var configTests = []struct {
}
func TestParseConfig(t *testing.T) {
for _, test := range configTests {
t.Run("", func(t *testing.T) {
cfg, err := ParseConfig(test.s)
if err != nil {
t.Fatalf("%s failed: %v", test.s, err)
}
if cfg != test.cfg {
t.Fatalf("input: %s\n wrong config, want:\n %#v\ngot:\n %#v",
test.s, test.cfg, cfg)
}
})
}
test.ParseConfigTester(t, ParseConfig, configTests)
}
var invalidConfigTests = []struct {

View File

@ -1,11 +1,13 @@
package gs
import (
"os"
"path"
"strings"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/options"
"github.com/restic/restic/internal/restic"
)
// Config contains all configuration necessary to connect to a Google Cloud Storage
@ -34,7 +36,7 @@ func init() {
// ParseConfig parses the string s and extracts the gcs config. The
// supported configuration format is gs:bucketName:/[prefix].
func ParseConfig(s string) (interface{}, error) {
func ParseConfig(s string) (*Config, error) {
if !strings.HasPrefix(s, "gs:") {
return nil, errors.New("gs: invalid format")
}
@ -54,5 +56,15 @@ func ParseConfig(s string) (interface{}, error) {
cfg := NewConfig()
cfg.Bucket = bucket
cfg.Prefix = prefix
return cfg, nil
return &cfg, nil
}
var _ restic.ApplyEnvironmenter = &Config{}
// ApplyEnvironment saves values from the environment to the config.
func (cfg *Config) ApplyEnvironment(prefix string) error {
if cfg.ProjectID == "" {
cfg.ProjectID = os.Getenv(prefix + "GOOGLE_PROJECT_ID")
}
return nil
}

View File

@ -1,24 +1,25 @@
package gs
import "testing"
import (
"testing"
var configTests = []struct {
s string
cfg Config
}{
{"gs:bucketname:/", Config{
"github.com/restic/restic/internal/backend/test"
)
var configTests = []test.ConfigTestData[Config]{
{S: "gs:bucketname:/", Cfg: Config{
Bucket: "bucketname",
Prefix: "",
Connections: 5,
Region: "us",
}},
{"gs:bucketname:/prefix/directory", Config{
{S: "gs:bucketname:/prefix/directory", Cfg: Config{
Bucket: "bucketname",
Prefix: "prefix/directory",
Connections: 5,
Region: "us",
}},
{"gs:bucketname:/prefix/directory/", Config{
{S: "gs:bucketname:/prefix/directory/", Cfg: Config{
Bucket: "bucketname",
Prefix: "prefix/directory",
Connections: 5,
@ -27,17 +28,5 @@ var configTests = []struct {
}
func TestParseConfig(t *testing.T) {
for i, test := range configTests {
cfg, err := ParseConfig(test.s)
if err != nil {
t.Errorf("test %d:%s failed: %v", i, test.s, err)
continue
}
if cfg != test.cfg {
t.Errorf("test %d:\ninput:\n %s\n wrong config, want:\n %v\ngot:\n %v",
i, test.s, test.cfg, cfg)
continue
}
}
test.ParseConfigTester(t, ParseConfig, configTests)
}

View File

@ -15,33 +15,30 @@ import (
rtest "github.com/restic/restic/internal/test"
)
func newGSTestSuite(t testing.TB) *test.Suite {
func newGSTestSuite(t testing.TB) *test.Suite[gs.Config] {
tr, err := backend.Transport(backend.TransportOptions{})
if err != nil {
t.Fatalf("cannot create transport for tests: %v", err)
}
return &test.Suite{
return &test.Suite[gs.Config]{
// do not use excessive data
MinimalData: true,
// NewConfig returns a config for a new temporary backend that will be used in tests.
NewConfig: func() (interface{}, error) {
gscfg, err := gs.ParseConfig(os.Getenv("RESTIC_TEST_GS_REPOSITORY"))
NewConfig: func() (*gs.Config, error) {
cfg, err := gs.ParseConfig(os.Getenv("RESTIC_TEST_GS_REPOSITORY"))
if err != nil {
return nil, err
}
cfg := gscfg.(gs.Config)
cfg.ProjectID = os.Getenv("RESTIC_TEST_GS_PROJECT_ID")
cfg.Prefix = fmt.Sprintf("test-%d", time.Now().UnixNano())
return cfg, nil
},
// CreateFn is a function that creates a temporary repository for the tests.
Create: func(config interface{}) (restic.Backend, error) {
cfg := config.(gs.Config)
Create: func(cfg gs.Config) (restic.Backend, error) {
be, err := gs.Create(context.Background(), cfg, tr)
if err != nil {
return nil, err
@ -60,15 +57,12 @@ func newGSTestSuite(t testing.TB) *test.Suite {
},
// OpenFn is a function that opens a previously created temporary repository.
Open: func(config interface{}) (restic.Backend, error) {
cfg := config.(gs.Config)
Open: func(cfg gs.Config) (restic.Backend, error) {
return gs.Open(cfg, tr)
},
// CleanupFn removes data created during the tests.
Cleanup: func(config interface{}) error {
cfg := config.(gs.Config)
Cleanup: func(cfg gs.Config) error {
be, err := gs.Open(cfg, tr)
if err != nil {
return err

View File

@ -27,12 +27,12 @@ func init() {
}
// ParseConfig parses a local backend config.
func ParseConfig(s string) (interface{}, error) {
func ParseConfig(s string) (*Config, error) {
if !strings.HasPrefix(s, "local:") {
return nil, errors.New(`invalid format, prefix "local" not found`)
}
cfg := NewConfig()
cfg.Path = s[6:]
return cfg, nil
return &cfg, nil
}

View File

@ -0,0 +1,18 @@
package local
import (
"testing"
"github.com/restic/restic/internal/backend/test"
)
var configTests = []test.ConfigTestData[Config]{
{S: "local:/some/path", Cfg: Config{
Path: "/some/path",
Connections: 2,
}},
}
func TestParseConfig(t *testing.T) {
test.ParseConfigTester(t, ParseConfig, configTests)
}

View File

@ -12,10 +12,10 @@ import (
rtest "github.com/restic/restic/internal/test"
)
func newTestSuite(t testing.TB) *test.Suite {
return &test.Suite{
func newTestSuite(t testing.TB) *test.Suite[local.Config] {
return &test.Suite[local.Config]{
// NewConfig returns a config for a new temporary backend that will be used in tests.
NewConfig: func() (interface{}, error) {
NewConfig: func() (*local.Config, error) {
dir, err := os.MkdirTemp(rtest.TestTempDir, "restic-test-local-")
if err != nil {
t.Fatal(err)
@ -23,7 +23,7 @@ func newTestSuite(t testing.TB) *test.Suite {
t.Logf("create new backend at %v", dir)
cfg := local.Config{
cfg := &local.Config{
Path: dir,
Connections: 2,
}
@ -31,20 +31,17 @@ func newTestSuite(t testing.TB) *test.Suite {
},
// CreateFn is a function that creates a temporary repository for the tests.
Create: func(config interface{}) (restic.Backend, error) {
cfg := config.(local.Config)
Create: func(cfg local.Config) (restic.Backend, error) {
return local.Create(context.TODO(), cfg)
},
// OpenFn is a function that opens a previously created temporary repository.
Open: func(config interface{}) (restic.Backend, error) {
cfg := config.(local.Config)
Open: func(cfg local.Config) (restic.Backend, error) {
return local.Open(context.TODO(), cfg)
},
// CleanupFn removes data created during the tests.
Cleanup: func(config interface{}) error {
cfg := config.(local.Config)
Cleanup: func(cfg local.Config) error {
if !rtest.TestCleanupTempDirs {
t.Logf("leaving test backend dir at %v", cfg.Path)
}

View File

@ -29,18 +29,24 @@ type parser struct {
stripPassword func(string) string
}
func configToAny[C any](parser func(string) (*C, error)) func(string) (interface{}, error) {
return func(s string) (interface{}, error) {
return parser(s)
}
}
// parsers is a list of valid config parsers for the backends. The first parser
// is the fallback and should always be set to the local backend.
var parsers = []parser{
{"b2", b2.ParseConfig, noPassword},
{"local", local.ParseConfig, noPassword},
{"sftp", sftp.ParseConfig, noPassword},
{"s3", s3.ParseConfig, noPassword},
{"gs", gs.ParseConfig, noPassword},
{"azure", azure.ParseConfig, noPassword},
{"swift", swift.ParseConfig, noPassword},
{"rest", rest.ParseConfig, rest.StripPassword},
{"rclone", rclone.ParseConfig, noPassword},
{"b2", configToAny(b2.ParseConfig), noPassword},
{"local", configToAny(local.ParseConfig), noPassword},
{"sftp", configToAny(sftp.ParseConfig), noPassword},
{"s3", configToAny(s3.ParseConfig), noPassword},
{"gs", configToAny(gs.ParseConfig), noPassword},
{"azure", configToAny(azure.ParseConfig), noPassword},
{"swift", configToAny(swift.ParseConfig), noPassword},
{"rest", configToAny(rest.ParseConfig), rest.StripPassword},
{"rclone", configToAny(rclone.ParseConfig), noPassword},
}
// noPassword returns the repository location unchanged (there's no sensitive information there)

View File

@ -29,7 +29,7 @@ var parseTests = []struct {
{
"local:/srv/repo",
Location{Scheme: "local",
Config: local.Config{
Config: &local.Config{
Path: "/srv/repo",
Connections: 2,
},
@ -38,7 +38,7 @@ var parseTests = []struct {
{
"local:dir1/dir2",
Location{Scheme: "local",
Config: local.Config{
Config: &local.Config{
Path: "dir1/dir2",
Connections: 2,
},
@ -47,7 +47,7 @@ var parseTests = []struct {
{
"local:dir1/dir2",
Location{Scheme: "local",
Config: local.Config{
Config: &local.Config{
Path: "dir1/dir2",
Connections: 2,
},
@ -56,7 +56,7 @@ var parseTests = []struct {
{
"dir1/dir2",
Location{Scheme: "local",
Config: local.Config{
Config: &local.Config{
Path: "dir1/dir2",
Connections: 2,
},
@ -65,7 +65,7 @@ var parseTests = []struct {
{
"/dir1/dir2",
Location{Scheme: "local",
Config: local.Config{
Config: &local.Config{
Path: "/dir1/dir2",
Connections: 2,
},
@ -74,7 +74,7 @@ var parseTests = []struct {
{
"local:../dir1/dir2",
Location{Scheme: "local",
Config: local.Config{
Config: &local.Config{
Path: "../dir1/dir2",
Connections: 2,
},
@ -83,7 +83,7 @@ var parseTests = []struct {
{
"/dir1/dir2",
Location{Scheme: "local",
Config: local.Config{
Config: &local.Config{
Path: "/dir1/dir2",
Connections: 2,
},
@ -92,7 +92,7 @@ var parseTests = []struct {
{
"/dir1:foobar/dir2",
Location{Scheme: "local",
Config: local.Config{
Config: &local.Config{
Path: "/dir1:foobar/dir2",
Connections: 2,
},
@ -101,7 +101,7 @@ var parseTests = []struct {
{
`\dir1\foobar\dir2`,
Location{Scheme: "local",
Config: local.Config{
Config: &local.Config{
Path: `\dir1\foobar\dir2`,
Connections: 2,
},
@ -110,7 +110,7 @@ var parseTests = []struct {
{
`c:\dir1\foobar\dir2`,
Location{Scheme: "local",
Config: local.Config{
Config: &local.Config{
Path: `c:\dir1\foobar\dir2`,
Connections: 2,
},
@ -119,7 +119,7 @@ var parseTests = []struct {
{
`C:\Users\appveyor\AppData\Local\Temp\1\restic-test-879453535\repo`,
Location{Scheme: "local",
Config: local.Config{
Config: &local.Config{
Path: `C:\Users\appveyor\AppData\Local\Temp\1\restic-test-879453535\repo`,
Connections: 2,
},
@ -128,7 +128,7 @@ var parseTests = []struct {
{
`c:/dir1/foobar/dir2`,
Location{Scheme: "local",
Config: local.Config{
Config: &local.Config{
Path: `c:/dir1/foobar/dir2`,
Connections: 2,
},
@ -137,7 +137,7 @@ var parseTests = []struct {
{
"sftp:user@host:/srv/repo",
Location{Scheme: "sftp",
Config: sftp.Config{
Config: &sftp.Config{
User: "user",
Host: "host",
Path: "/srv/repo",
@ -148,7 +148,7 @@ var parseTests = []struct {
{
"sftp:host:/srv/repo",
Location{Scheme: "sftp",
Config: sftp.Config{
Config: &sftp.Config{
User: "",
Host: "host",
Path: "/srv/repo",
@ -159,7 +159,7 @@ var parseTests = []struct {
{
"sftp://user@host/srv/repo",
Location{Scheme: "sftp",
Config: sftp.Config{
Config: &sftp.Config{
User: "user",
Host: "host",
Path: "srv/repo",
@ -170,7 +170,7 @@ var parseTests = []struct {
{
"sftp://user@host//srv/repo",
Location{Scheme: "sftp",
Config: sftp.Config{
Config: &sftp.Config{
User: "user",
Host: "host",
Path: "/srv/repo",
@ -182,7 +182,7 @@ var parseTests = []struct {
{
"s3://eu-central-1/bucketname",
Location{Scheme: "s3",
Config: s3.Config{
Config: &s3.Config{
Endpoint: "eu-central-1",
Bucket: "bucketname",
Prefix: "",
@ -193,7 +193,7 @@ var parseTests = []struct {
{
"s3://hostname.foo/bucketname",
Location{Scheme: "s3",
Config: s3.Config{
Config: &s3.Config{
Endpoint: "hostname.foo",
Bucket: "bucketname",
Prefix: "",
@ -204,7 +204,7 @@ var parseTests = []struct {
{
"s3://hostname.foo/bucketname/prefix/directory",
Location{Scheme: "s3",
Config: s3.Config{
Config: &s3.Config{
Endpoint: "hostname.foo",
Bucket: "bucketname",
Prefix: "prefix/directory",
@ -215,7 +215,7 @@ var parseTests = []struct {
{
"s3:eu-central-1/repo",
Location{Scheme: "s3",
Config: s3.Config{
Config: &s3.Config{
Endpoint: "eu-central-1",
Bucket: "repo",
Prefix: "",
@ -226,7 +226,7 @@ var parseTests = []struct {
{
"s3:eu-central-1/repo/prefix/directory",
Location{Scheme: "s3",
Config: s3.Config{
Config: &s3.Config{
Endpoint: "eu-central-1",
Bucket: "repo",
Prefix: "prefix/directory",
@ -237,7 +237,7 @@ var parseTests = []struct {
{
"s3:https://hostname.foo/repo",
Location{Scheme: "s3",
Config: s3.Config{
Config: &s3.Config{
Endpoint: "hostname.foo",
Bucket: "repo",
Prefix: "",
@ -248,7 +248,7 @@ var parseTests = []struct {
{
"s3:https://hostname.foo/repo/prefix/directory",
Location{Scheme: "s3",
Config: s3.Config{
Config: &s3.Config{
Endpoint: "hostname.foo",
Bucket: "repo",
Prefix: "prefix/directory",
@ -259,7 +259,7 @@ var parseTests = []struct {
{
"s3:http://hostname.foo/repo",
Location{Scheme: "s3",
Config: s3.Config{
Config: &s3.Config{
Endpoint: "hostname.foo",
Bucket: "repo",
Prefix: "",
@ -271,7 +271,7 @@ var parseTests = []struct {
{
"swift:container17:/",
Location{Scheme: "swift",
Config: swift.Config{
Config: &swift.Config{
Container: "container17",
Prefix: "",
Connections: 5,
@ -281,7 +281,7 @@ var parseTests = []struct {
{
"swift:container17:/prefix97",
Location{Scheme: "swift",
Config: swift.Config{
Config: &swift.Config{
Container: "container17",
Prefix: "prefix97",
Connections: 5,
@ -291,7 +291,7 @@ var parseTests = []struct {
{
"rest:http://hostname.foo:1234/",
Location{Scheme: "rest",
Config: rest.Config{
Config: &rest.Config{
URL: parseURL("http://hostname.foo:1234/"),
Connections: 5,
},
@ -299,7 +299,7 @@ var parseTests = []struct {
},
{
"b2:bucketname:/prefix", Location{Scheme: "b2",
Config: b2.Config{
Config: &b2.Config{
Bucket: "bucketname",
Prefix: "prefix",
Connections: 5,
@ -308,7 +308,7 @@ var parseTests = []struct {
},
{
"b2:bucketname", Location{Scheme: "b2",
Config: b2.Config{
Config: &b2.Config{
Bucket: "bucketname",
Prefix: "",
Connections: 5,

View File

@ -15,19 +15,19 @@ type memConfig struct {
be restic.Backend
}
func newTestSuite() *test.Suite {
return &test.Suite{
func newTestSuite() *test.Suite[*memConfig] {
return &test.Suite[*memConfig]{
// NewConfig returns a config for a new temporary backend that will be used in tests.
NewConfig: func() (interface{}, error) {
return &memConfig{}, nil
NewConfig: func() (**memConfig, error) {
cfg := &memConfig{}
return &cfg, nil
},
// CreateFn is a function that creates a temporary repository for the tests.
Create: func(cfg interface{}) (restic.Backend, error) {
c := cfg.(*memConfig)
if c.be != nil {
_, err := c.be.Stat(context.TODO(), restic.Handle{Type: restic.ConfigFile})
if err != nil && !c.be.IsNotExist(err) {
Create: func(cfg *memConfig) (restic.Backend, error) {
if cfg.be != nil {
_, err := cfg.be.Stat(context.TODO(), restic.Handle{Type: restic.ConfigFile})
if err != nil && !cfg.be.IsNotExist(err) {
return nil, err
}
@ -36,21 +36,20 @@ func newTestSuite() *test.Suite {
}
}
c.be = mem.New()
return c.be, nil
cfg.be = mem.New()
return cfg.be, nil
},
// OpenFn is a function that opens a previously created temporary repository.
Open: func(cfg interface{}) (restic.Backend, error) {
c := cfg.(*memConfig)
if c.be == nil {
c.be = mem.New()
Open: func(cfg *memConfig) (restic.Backend, error) {
if cfg.be == nil {
cfg.be = mem.New()
}
return c.be, nil
return cfg.be, nil
},
// CleanupFn removes data created during the tests.
Cleanup: func(cfg interface{}) error {
Cleanup: func(cfg *memConfig) error {
// no cleanup needed
return nil
},

View File

@ -12,22 +12,21 @@ import (
rtest "github.com/restic/restic/internal/test"
)
func newTestSuite(t testing.TB) *test.Suite {
func newTestSuite(t testing.TB) *test.Suite[rclone.Config] {
dir := rtest.TempDir(t)
return &test.Suite{
return &test.Suite[rclone.Config]{
// NewConfig returns a config for a new temporary backend that will be used in tests.
NewConfig: func() (interface{}, error) {
NewConfig: func() (*rclone.Config, error) {
t.Logf("use backend at %v", dir)
cfg := rclone.NewConfig()
cfg.Remote = dir
return cfg, nil
return &cfg, nil
},
// CreateFn is a function that creates a temporary repository for the tests.
Create: func(config interface{}) (restic.Backend, error) {
Create: func(cfg rclone.Config) (restic.Backend, error) {
t.Logf("Create()")
cfg := config.(rclone.Config)
be, err := rclone.Create(context.TODO(), cfg)
var e *exec.Error
if errors.As(err, &e) && e.Err == exec.ErrNotFound {
@ -38,9 +37,8 @@ func newTestSuite(t testing.TB) *test.Suite {
},
// OpenFn is a function that opens a previously created temporary repository.
Open: func(config interface{}) (restic.Backend, error) {
Open: func(cfg rclone.Config) (restic.Backend, error) {
t.Logf("Open()")
cfg := config.(rclone.Config)
return rclone.Open(cfg, nil)
},
}

View File

@ -34,7 +34,7 @@ func NewConfig() Config {
}
// ParseConfig parses the string s and extracts the remote server URL.
func ParseConfig(s string) (interface{}, error) {
func ParseConfig(s string) (*Config, error) {
if !strings.HasPrefix(s, "rclone:") {
return nil, errors.New("invalid rclone backend specification")
}
@ -42,5 +42,5 @@ func ParseConfig(s string) (interface{}, error) {
s = s[7:]
cfg := NewConfig()
cfg.Remote = s
return cfg, nil
return &cfg, nil
}

View File

@ -1,37 +1,24 @@
package rclone
import (
"reflect"
"testing"
"github.com/restic/restic/internal/backend/test"
)
func TestParseConfig(t *testing.T) {
var tests = []struct {
s string
cfg Config
}{
{
"rclone:local:foo:/bar",
Config{
Remote: "local:foo:/bar",
Program: defaultConfig.Program,
Args: defaultConfig.Args,
Connections: defaultConfig.Connections,
Timeout: defaultConfig.Timeout,
},
var configTests = []test.ConfigTestData[Config]{
{
S: "rclone:local:foo:/bar",
Cfg: Config{
Remote: "local:foo:/bar",
Program: defaultConfig.Program,
Args: defaultConfig.Args,
Connections: defaultConfig.Connections,
Timeout: defaultConfig.Timeout,
},
}
for _, test := range tests {
t.Run("", func(t *testing.T) {
cfg, err := ParseConfig(test.s)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(cfg, test.cfg) {
t.Fatalf("wrong config, want:\n %v\ngot:\n %v", test.cfg, cfg)
}
})
}
},
}
func TestParseConfig(t *testing.T) {
test.ParseConfigTester(t, ParseConfig, configTests)
}

View File

@ -26,7 +26,7 @@ func NewConfig() Config {
}
// ParseConfig parses the string s and extracts the REST server URL.
func ParseConfig(s string) (interface{}, error) {
func ParseConfig(s string) (*Config, error) {
if !strings.HasPrefix(s, "rest:") {
return nil, errors.New("invalid REST backend specification")
}
@ -40,7 +40,7 @@ func ParseConfig(s string) (interface{}, error) {
cfg := NewConfig()
cfg.URL = u
return cfg, nil
return &cfg, nil
}
// StripPassword removes the password from the URL

View File

@ -2,8 +2,9 @@ package rest
import (
"net/url"
"reflect"
"testing"
"github.com/restic/restic/internal/backend/test"
)
func parseURL(s string) *url.URL {
@ -15,20 +16,17 @@ func parseURL(s string) *url.URL {
return u
}
var configTests = []struct {
s string
cfg Config
}{
var configTests = []test.ConfigTestData[Config]{
{
s: "rest:http://localhost:1234",
cfg: Config{
S: "rest:http://localhost:1234",
Cfg: Config{
URL: parseURL("http://localhost:1234/"),
Connections: 5,
},
},
{
s: "rest:http://localhost:1234/",
cfg: Config{
S: "rest:http://localhost:1234/",
Cfg: Config{
URL: parseURL("http://localhost:1234/"),
Connections: 5,
},
@ -36,17 +34,5 @@ var configTests = []struct {
}
func TestParseConfig(t *testing.T) {
for _, test := range configTests {
t.Run("", func(t *testing.T) {
cfg, err := ParseConfig(test.s)
if err != nil {
t.Fatalf("%s failed: %v", test.s, err)
}
if !reflect.DeepEqual(cfg, test.cfg) {
t.Fatalf("\ninput: %s\n wrong config, want:\n %v\ngot:\n %v",
test.s, test.cfg, cfg)
}
})
}
test.ParseConfigTester(t, ParseConfig, configTests)
}

View File

@ -67,36 +67,34 @@ func runRESTServer(ctx context.Context, t testing.TB, dir string) (*url.URL, fun
return url, cleanup
}
func newTestSuite(_ context.Context, t testing.TB, url *url.URL, minimalData bool) *test.Suite {
func newTestSuite(_ context.Context, t testing.TB, url *url.URL, minimalData bool) *test.Suite[rest.Config] {
tr, err := backend.Transport(backend.TransportOptions{})
if err != nil {
t.Fatalf("cannot create transport for tests: %v", err)
}
return &test.Suite{
return &test.Suite[rest.Config]{
MinimalData: minimalData,
// NewConfig returns a config for a new temporary backend that will be used in tests.
NewConfig: func() (interface{}, error) {
NewConfig: func() (*rest.Config, error) {
cfg := rest.NewConfig()
cfg.URL = url
return cfg, nil
return &cfg, nil
},
// CreateFn is a function that creates a temporary repository for the tests.
Create: func(config interface{}) (restic.Backend, error) {
cfg := config.(rest.Config)
Create: func(cfg rest.Config) (restic.Backend, error) {
return rest.Create(context.TODO(), cfg, tr)
},
// OpenFn is a function that opens a previously created temporary repository.
Open: func(config interface{}) (restic.Backend, error) {
cfg := config.(rest.Config)
Open: func(cfg rest.Config) (restic.Backend, error) {
return rest.Open(cfg, tr)
},
// CleanupFn removes data created during the tests.
Cleanup: func(config interface{}) error {
Cleanup: func(cfg rest.Config) error {
return nil
},
}
@ -130,12 +128,10 @@ func TestBackendRESTExternalServer(t *testing.T) {
t.Fatal(err)
}
c := cfg.(rest.Config)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
newTestSuite(ctx, t, c.URL, true).RunTests(t)
newTestSuite(ctx, t, cfg.URL, true).RunTests(t)
}
func BenchmarkBackendREST(t *testing.B) {

View File

@ -2,11 +2,13 @@ package s3
import (
"net/url"
"os"
"path"
"strings"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/options"
"github.com/restic/restic/internal/restic"
)
// Config contains all configuration necessary to connect to an s3 compatible
@ -44,7 +46,7 @@ func init() {
// supported configuration formats are s3://host/bucketname/prefix and
// s3:host/bucketname/prefix. The host can also be a valid s3 region
// name. If no prefix is given the prefix "restic" will be used.
func ParseConfig(s string) (interface{}, error) {
func ParseConfig(s string) (*Config, error) {
switch {
case strings.HasPrefix(s, "s3:http"):
// assume that a URL has been specified, parse it and
@ -75,7 +77,7 @@ func ParseConfig(s string) (interface{}, error) {
return createConfig(endpoint, bucket, prefix, false)
}
func createConfig(endpoint, bucket, prefix string, useHTTP bool) (interface{}, error) {
func createConfig(endpoint, bucket, prefix string, useHTTP bool) (*Config, error) {
if endpoint == "" {
return nil, errors.New("s3: invalid format, host/region or bucket name not found")
}
@ -89,5 +91,30 @@ func createConfig(endpoint, bucket, prefix string, useHTTP bool) (interface{}, e
cfg.UseHTTP = useHTTP
cfg.Bucket = bucket
cfg.Prefix = prefix
return cfg, nil
return &cfg, nil
}
var _ restic.ApplyEnvironmenter = &Config{}
// ApplyEnvironment saves values from the environment to the config.
func (cfg *Config) ApplyEnvironment(prefix string) error {
if cfg.KeyID == "" {
cfg.KeyID = os.Getenv(prefix + "AWS_ACCESS_KEY_ID")
}
if cfg.Secret.String() == "" {
cfg.Secret = options.NewSecretString(os.Getenv(prefix + "AWS_SECRET_ACCESS_KEY"))
}
if cfg.KeyID == "" && cfg.Secret.String() != "" {
return errors.Fatalf("unable to open S3 backend: Key ID ($AWS_ACCESS_KEY_ID) is empty")
} else if cfg.KeyID != "" && cfg.Secret.String() == "" {
return errors.Fatalf("unable to open S3 backend: Secret ($AWS_SECRET_ACCESS_KEY) is empty")
}
if cfg.Region == "" {
cfg.Region = os.Getenv(prefix + "AWS_DEFAULT_REGION")
}
return nil
}

View File

@ -3,94 +3,93 @@ package s3
import (
"strings"
"testing"
"github.com/restic/restic/internal/backend/test"
)
var configTests = []struct {
s string
cfg Config
}{
{"s3://eu-central-1/bucketname", Config{
var configTests = []test.ConfigTestData[Config]{
{S: "s3://eu-central-1/bucketname", Cfg: Config{
Endpoint: "eu-central-1",
Bucket: "bucketname",
Prefix: "",
Connections: 5,
}},
{"s3://eu-central-1/bucketname/", Config{
{S: "s3://eu-central-1/bucketname/", Cfg: Config{
Endpoint: "eu-central-1",
Bucket: "bucketname",
Prefix: "",
Connections: 5,
}},
{"s3://eu-central-1/bucketname/prefix/directory", Config{
{S: "s3://eu-central-1/bucketname/prefix/directory", Cfg: Config{
Endpoint: "eu-central-1",
Bucket: "bucketname",
Prefix: "prefix/directory",
Connections: 5,
}},
{"s3://eu-central-1/bucketname/prefix/directory/", Config{
{S: "s3://eu-central-1/bucketname/prefix/directory/", Cfg: Config{
Endpoint: "eu-central-1",
Bucket: "bucketname",
Prefix: "prefix/directory",
Connections: 5,
}},
{"s3:eu-central-1/foobar", Config{
{S: "s3:eu-central-1/foobar", Cfg: Config{
Endpoint: "eu-central-1",
Bucket: "foobar",
Prefix: "",
Connections: 5,
}},
{"s3:eu-central-1/foobar/", Config{
{S: "s3:eu-central-1/foobar/", Cfg: Config{
Endpoint: "eu-central-1",
Bucket: "foobar",
Prefix: "",
Connections: 5,
}},
{"s3:eu-central-1/foobar/prefix/directory", Config{
{S: "s3:eu-central-1/foobar/prefix/directory", Cfg: Config{
Endpoint: "eu-central-1",
Bucket: "foobar",
Prefix: "prefix/directory",
Connections: 5,
}},
{"s3:eu-central-1/foobar/prefix/directory/", Config{
{S: "s3:eu-central-1/foobar/prefix/directory/", Cfg: Config{
Endpoint: "eu-central-1",
Bucket: "foobar",
Prefix: "prefix/directory",
Connections: 5,
}},
{"s3:https://hostname:9999/foobar", Config{
{S: "s3:https://hostname:9999/foobar", Cfg: Config{
Endpoint: "hostname:9999",
Bucket: "foobar",
Prefix: "",
Connections: 5,
}},
{"s3:https://hostname:9999/foobar/", Config{
{S: "s3:https://hostname:9999/foobar/", Cfg: Config{
Endpoint: "hostname:9999",
Bucket: "foobar",
Prefix: "",
Connections: 5,
}},
{"s3:http://hostname:9999/foobar", Config{
{S: "s3:http://hostname:9999/foobar", Cfg: Config{
Endpoint: "hostname:9999",
Bucket: "foobar",
Prefix: "",
UseHTTP: true,
Connections: 5,
}},
{"s3:http://hostname:9999/foobar/", Config{
{S: "s3:http://hostname:9999/foobar/", Cfg: Config{
Endpoint: "hostname:9999",
Bucket: "foobar",
Prefix: "",
UseHTTP: true,
Connections: 5,
}},
{"s3:http://hostname:9999/bucket/prefix/directory", Config{
{S: "s3:http://hostname:9999/bucket/prefix/directory", Cfg: Config{
Endpoint: "hostname:9999",
Bucket: "bucket",
Prefix: "prefix/directory",
UseHTTP: true,
Connections: 5,
}},
{"s3:http://hostname:9999/bucket/prefix/directory/", Config{
{S: "s3:http://hostname:9999/bucket/prefix/directory/", Cfg: Config{
Endpoint: "hostname:9999",
Bucket: "bucket",
Prefix: "prefix/directory",
@ -100,19 +99,7 @@ var configTests = []struct {
}
func TestParseConfig(t *testing.T) {
for i, test := range configTests {
cfg, err := ParseConfig(test.s)
if err != nil {
t.Errorf("test %d:%s failed: %v", i, test.s, err)
continue
}
if cfg != test.cfg {
t.Errorf("test %d:\ninput:\n %s\n wrong config, want:\n %v\ngot:\n %v",
i, test.s, test.cfg, cfg)
continue
}
}
test.ParseConfigTester(t, ParseConfig, configTests)
}
func TestParseError(t *testing.T) {

View File

@ -120,15 +120,15 @@ func createS3(t testing.TB, cfg MinioTestConfig, tr http.RoundTripper) (be resti
return be, err
}
func newMinioTestSuite(ctx context.Context, t testing.TB) *test.Suite {
func newMinioTestSuite(ctx context.Context, t testing.TB) *test.Suite[MinioTestConfig] {
tr, err := backend.Transport(backend.TransportOptions{})
if err != nil {
t.Fatalf("cannot create transport for tests: %v", err)
}
return &test.Suite{
return &test.Suite[MinioTestConfig]{
// NewConfig returns a config for a new temporary backend that will be used in tests.
NewConfig: func() (interface{}, error) {
NewConfig: func() (*MinioTestConfig, error) {
cfg := MinioTestConfig{}
cfg.tempdir = rtest.TempDir(t)
@ -142,13 +142,11 @@ func newMinioTestSuite(ctx context.Context, t testing.TB) *test.Suite {
cfg.Config.UseHTTP = true
cfg.Config.KeyID = key
cfg.Config.Secret = options.NewSecretString(secret)
return cfg, nil
return &cfg, nil
},
// CreateFn is a function that creates a temporary repository for the tests.
Create: func(config interface{}) (restic.Backend, error) {
cfg := config.(MinioTestConfig)
Create: func(cfg MinioTestConfig) (restic.Backend, error) {
be, err := createS3(t, cfg, tr)
if err != nil {
return nil, err
@ -167,14 +165,12 @@ func newMinioTestSuite(ctx context.Context, t testing.TB) *test.Suite {
},
// OpenFn is a function that opens a previously created temporary repository.
Open: func(config interface{}) (restic.Backend, error) {
cfg := config.(MinioTestConfig)
Open: func(cfg MinioTestConfig) (restic.Backend, error) {
return s3.Open(ctx, cfg.Config, tr)
},
// CleanupFn removes data created during the tests.
Cleanup: func(config interface{}) error {
cfg := config.(MinioTestConfig)
Cleanup: func(cfg MinioTestConfig) error {
if cfg.stopServer != nil {
cfg.stopServer()
}
@ -217,24 +213,23 @@ func BenchmarkBackendMinio(t *testing.B) {
newMinioTestSuite(ctx, t).RunBenchmarks(t)
}
func newS3TestSuite(t testing.TB) *test.Suite {
func newS3TestSuite(t testing.TB) *test.Suite[s3.Config] {
tr, err := backend.Transport(backend.TransportOptions{})
if err != nil {
t.Fatalf("cannot create transport for tests: %v", err)
}
return &test.Suite{
return &test.Suite[s3.Config]{
// do not use excessive data
MinimalData: true,
// NewConfig returns a config for a new temporary backend that will be used in tests.
NewConfig: func() (interface{}, error) {
s3cfg, err := s3.ParseConfig(os.Getenv("RESTIC_TEST_S3_REPOSITORY"))
NewConfig: func() (*s3.Config, error) {
cfg, err := s3.ParseConfig(os.Getenv("RESTIC_TEST_S3_REPOSITORY"))
if err != nil {
return nil, err
}
cfg := s3cfg.(s3.Config)
cfg.KeyID = os.Getenv("RESTIC_TEST_S3_KEY")
cfg.Secret = options.NewSecretString(os.Getenv("RESTIC_TEST_S3_SECRET"))
cfg.Prefix = fmt.Sprintf("test-%d", time.Now().UnixNano())
@ -242,9 +237,7 @@ func newS3TestSuite(t testing.TB) *test.Suite {
},
// CreateFn is a function that creates a temporary repository for the tests.
Create: func(config interface{}) (restic.Backend, error) {
cfg := config.(s3.Config)
Create: func(cfg s3.Config) (restic.Backend, error) {
be, err := s3.Create(context.TODO(), cfg, tr)
if err != nil {
return nil, err
@ -263,15 +256,12 @@ func newS3TestSuite(t testing.TB) *test.Suite {
},
// OpenFn is a function that opens a previously created temporary repository.
Open: func(config interface{}) (restic.Backend, error) {
cfg := config.(s3.Config)
Open: func(cfg s3.Config) (restic.Backend, error) {
return s3.Open(context.TODO(), cfg, tr)
},
// CleanupFn removes data created during the tests.
Cleanup: func(config interface{}) error {
cfg := config.(s3.Config)
Cleanup: func(cfg s3.Config) error {
be, err := s3.Open(context.TODO(), cfg, tr)
if err != nil {
return err

View File

@ -35,7 +35,7 @@ func init() {
// and sftp:user@host:directory. The directory will be path Cleaned and can
// be an absolute path if it starts with a '/' (e.g.
// sftp://user@host//absolute and sftp:user@host:/absolute).
func ParseConfig(s string) (interface{}, error) {
func ParseConfig(s string) (*Config, error) {
var user, host, port, dir string
switch {
case strings.HasPrefix(s, "sftp://"):
@ -89,5 +89,5 @@ func ParseConfig(s string) (interface{}, error) {
cfg.Port = port
cfg.Path = p
return cfg, nil
return &cfg, nil
}

View File

@ -2,94 +2,81 @@ package sftp
import (
"testing"
"github.com/restic/restic/internal/backend/test"
)
var configTests = []struct {
in string
cfg Config
}{
var configTests = []test.ConfigTestData[Config]{
// first form, user specified sftp://user@host/dir
{
"sftp://user@host/dir/subdir",
Config{User: "user", Host: "host", Path: "dir/subdir", Connections: 5},
S: "sftp://user@host/dir/subdir",
Cfg: Config{User: "user", Host: "host", Path: "dir/subdir", Connections: 5},
},
{
"sftp://host/dir/subdir",
Config{Host: "host", Path: "dir/subdir", Connections: 5},
S: "sftp://host/dir/subdir",
Cfg: Config{Host: "host", Path: "dir/subdir", Connections: 5},
},
{
"sftp://host//dir/subdir",
Config{Host: "host", Path: "/dir/subdir", Connections: 5},
S: "sftp://host//dir/subdir",
Cfg: Config{Host: "host", Path: "/dir/subdir", Connections: 5},
},
{
"sftp://host:10022//dir/subdir",
Config{Host: "host", Port: "10022", Path: "/dir/subdir", Connections: 5},
S: "sftp://host:10022//dir/subdir",
Cfg: Config{Host: "host", Port: "10022", Path: "/dir/subdir", Connections: 5},
},
{
"sftp://user@host:10022//dir/subdir",
Config{User: "user", Host: "host", Port: "10022", Path: "/dir/subdir", Connections: 5},
S: "sftp://user@host:10022//dir/subdir",
Cfg: Config{User: "user", Host: "host", Port: "10022", Path: "/dir/subdir", Connections: 5},
},
{
"sftp://user@host/dir/subdir/../other",
Config{User: "user", Host: "host", Path: "dir/other", Connections: 5},
S: "sftp://user@host/dir/subdir/../other",
Cfg: Config{User: "user", Host: "host", Path: "dir/other", Connections: 5},
},
{
"sftp://user@host/dir///subdir",
Config{User: "user", Host: "host", Path: "dir/subdir", Connections: 5},
S: "sftp://user@host/dir///subdir",
Cfg: Config{User: "user", Host: "host", Path: "dir/subdir", Connections: 5},
},
// IPv6 address.
{
"sftp://user@[::1]/dir",
Config{User: "user", Host: "::1", Path: "dir", Connections: 5},
S: "sftp://user@[::1]/dir",
Cfg: Config{User: "user", Host: "::1", Path: "dir", Connections: 5},
},
// IPv6 address with port.
{
"sftp://user@[::1]:22/dir",
Config{User: "user", Host: "::1", Port: "22", Path: "dir", Connections: 5},
S: "sftp://user@[::1]:22/dir",
Cfg: Config{User: "user", Host: "::1", Port: "22", Path: "dir", Connections: 5},
},
// second form, user specified sftp:user@host:/dir
{
"sftp:user@host:/dir/subdir",
Config{User: "user", Host: "host", Path: "/dir/subdir", Connections: 5},
S: "sftp:user@host:/dir/subdir",
Cfg: Config{User: "user", Host: "host", Path: "/dir/subdir", Connections: 5},
},
{
"sftp:user@domain@host:/dir/subdir",
Config{User: "user@domain", Host: "host", Path: "/dir/subdir", Connections: 5},
S: "sftp:user@domain@host:/dir/subdir",
Cfg: Config{User: "user@domain", Host: "host", Path: "/dir/subdir", Connections: 5},
},
{
"sftp:host:../dir/subdir",
Config{Host: "host", Path: "../dir/subdir", Connections: 5},
S: "sftp:host:../dir/subdir",
Cfg: Config{Host: "host", Path: "../dir/subdir", Connections: 5},
},
{
"sftp:user@host:dir/subdir:suffix",
Config{User: "user", Host: "host", Path: "dir/subdir:suffix", Connections: 5},
S: "sftp:user@host:dir/subdir:suffix",
Cfg: Config{User: "user", Host: "host", Path: "dir/subdir:suffix", Connections: 5},
},
{
"sftp:user@host:dir/subdir/../other",
Config{User: "user", Host: "host", Path: "dir/other", Connections: 5},
S: "sftp:user@host:dir/subdir/../other",
Cfg: Config{User: "user", Host: "host", Path: "dir/other", Connections: 5},
},
{
"sftp:user@host:dir///subdir",
Config{User: "user", Host: "host", Path: "dir/subdir", Connections: 5},
S: "sftp:user@host:dir///subdir",
Cfg: Config{User: "user", Host: "host", Path: "dir/subdir", Connections: 5},
},
}
func TestParseConfig(t *testing.T) {
for i, test := range configTests {
cfg, err := ParseConfig(test.in)
if err != nil {
t.Errorf("test %d:%s failed: %v", i, test.in, err)
continue
}
if cfg != test.cfg {
t.Errorf("test %d:\ninput:\n %s\n wrong config, want:\n %v\ngot:\n %v",
i, test.in, test.cfg, cfg)
continue
}
}
test.ParseConfigTester(t, ParseConfig, configTests)
}
var configTestsInvalid = []string{

View File

@ -29,10 +29,10 @@ func findSFTPServerBinary() string {
var sftpServer = findSFTPServerBinary()
func newTestSuite(t testing.TB) *test.Suite {
return &test.Suite{
func newTestSuite(t testing.TB) *test.Suite[sftp.Config] {
return &test.Suite[sftp.Config]{
// NewConfig returns a config for a new temporary backend that will be used in tests.
NewConfig: func() (interface{}, error) {
NewConfig: func() (*sftp.Config, error) {
dir, err := os.MkdirTemp(rtest.TestTempDir, "restic-test-sftp-")
if err != nil {
t.Fatal(err)
@ -40,7 +40,7 @@ func newTestSuite(t testing.TB) *test.Suite {
t.Logf("create new backend at %v", dir)
cfg := sftp.Config{
cfg := &sftp.Config{
Path: dir,
Command: fmt.Sprintf("%q -e", sftpServer),
Connections: 5,
@ -49,20 +49,17 @@ func newTestSuite(t testing.TB) *test.Suite {
},
// CreateFn is a function that creates a temporary repository for the tests.
Create: func(config interface{}) (restic.Backend, error) {
cfg := config.(sftp.Config)
Create: func(cfg sftp.Config) (restic.Backend, error) {
return sftp.Create(context.TODO(), cfg)
},
// OpenFn is a function that opens a previously created temporary repository.
Open: func(config interface{}) (restic.Backend, error) {
cfg := config.(sftp.Config)
Open: func(cfg sftp.Config) (restic.Backend, error) {
return sftp.Open(context.TODO(), cfg)
},
// CleanupFn removes data created during the tests.
Cleanup: func(config interface{}) error {
cfg := config.(sftp.Config)
Cleanup: func(cfg sftp.Config) error {
if !rtest.TestCleanupTempDirs {
t.Logf("leaving test backend dir at %v", cfg.Path)
}

View File

@ -6,6 +6,7 @@ import (
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/options"
"github.com/restic/restic/internal/restic"
)
// Config contains basic configuration needed to specify swift location for a swift server
@ -50,7 +51,7 @@ func NewConfig() Config {
}
// ParseConfig parses the string s and extract swift's container name and prefix.
func ParseConfig(s string) (interface{}, error) {
func ParseConfig(s string) (*Config, error) {
if !strings.HasPrefix(s, "swift:") {
return nil, errors.New("invalid URL, expected: swift:container-name:/[prefix]")
}
@ -70,48 +71,49 @@ func ParseConfig(s string) (interface{}, error) {
cfg.Container = container
cfg.Prefix = prefix
return cfg, nil
return &cfg, nil
}
var _ restic.ApplyEnvironmenter = &Config{}
// ApplyEnvironment saves values from the environment to the config.
func ApplyEnvironment(prefix string, cfg interface{}) error {
c := cfg.(*Config)
func (cfg *Config) ApplyEnvironment(prefix string) error {
for _, val := range []struct {
s *string
env string
}{
// v2/v3 specific
{&c.UserName, prefix + "OS_USERNAME"},
{&c.APIKey, prefix + "OS_PASSWORD"},
{&c.Region, prefix + "OS_REGION_NAME"},
{&c.AuthURL, prefix + "OS_AUTH_URL"},
{&cfg.UserName, prefix + "OS_USERNAME"},
{&cfg.APIKey, prefix + "OS_PASSWORD"},
{&cfg.Region, prefix + "OS_REGION_NAME"},
{&cfg.AuthURL, prefix + "OS_AUTH_URL"},
// v3 specific
{&c.UserID, prefix + "OS_USER_ID"},
{&c.Domain, prefix + "OS_USER_DOMAIN_NAME"},
{&c.DomainID, prefix + "OS_USER_DOMAIN_ID"},
{&c.Tenant, prefix + "OS_PROJECT_NAME"},
{&c.TenantDomain, prefix + "OS_PROJECT_DOMAIN_NAME"},
{&c.TenantDomainID, prefix + "OS_PROJECT_DOMAIN_ID"},
{&c.TrustID, prefix + "OS_TRUST_ID"},
{&cfg.UserID, prefix + "OS_USER_ID"},
{&cfg.Domain, prefix + "OS_USER_DOMAIN_NAME"},
{&cfg.DomainID, prefix + "OS_USER_DOMAIN_ID"},
{&cfg.Tenant, prefix + "OS_PROJECT_NAME"},
{&cfg.TenantDomain, prefix + "OS_PROJECT_DOMAIN_NAME"},
{&cfg.TenantDomainID, prefix + "OS_PROJECT_DOMAIN_ID"},
{&cfg.TrustID, prefix + "OS_TRUST_ID"},
// v2 specific
{&c.TenantID, prefix + "OS_TENANT_ID"},
{&c.Tenant, prefix + "OS_TENANT_NAME"},
{&cfg.TenantID, prefix + "OS_TENANT_ID"},
{&cfg.Tenant, prefix + "OS_TENANT_NAME"},
// v1 specific
{&c.AuthURL, prefix + "ST_AUTH"},
{&c.UserName, prefix + "ST_USER"},
{&c.APIKey, prefix + "ST_KEY"},
{&cfg.AuthURL, prefix + "ST_AUTH"},
{&cfg.UserName, prefix + "ST_USER"},
{&cfg.APIKey, prefix + "ST_KEY"},
// Application Credential auth
{&c.ApplicationCredentialID, prefix + "OS_APPLICATION_CREDENTIAL_ID"},
{&c.ApplicationCredentialName, prefix + "OS_APPLICATION_CREDENTIAL_NAME"},
{&cfg.ApplicationCredentialID, prefix + "OS_APPLICATION_CREDENTIAL_ID"},
{&cfg.ApplicationCredentialName, prefix + "OS_APPLICATION_CREDENTIAL_NAME"},
// Manual authentication
{&c.StorageURL, prefix + "OS_STORAGE_URL"},
{&cfg.StorageURL, prefix + "OS_STORAGE_URL"},
{&c.DefaultContainerPolicy, prefix + "SWIFT_DEFAULT_CONTAINER_POLICY"},
{&cfg.DefaultContainerPolicy, prefix + "SWIFT_DEFAULT_CONTAINER_POLICY"},
} {
if *val.s == "" {
*val.s = os.Getenv(val.env)
@ -121,8 +123,8 @@ func ApplyEnvironment(prefix string, cfg interface{}) error {
s *options.SecretString
env string
}{
{&c.ApplicationCredentialSecret, prefix + "OS_APPLICATION_CREDENTIAL_SECRET"},
{&c.AuthToken, prefix + "OS_AUTH_TOKEN"},
{&cfg.ApplicationCredentialSecret, prefix + "OS_APPLICATION_CREDENTIAL_SECRET"},
{&cfg.AuthToken, prefix + "OS_AUTH_TOKEN"},
} {
if val.s.String() == "" {
*val.s = options.NewSecretString(os.Getenv(val.env))

View File

@ -1,29 +1,30 @@
package swift
import "testing"
import (
"testing"
var configTests = []struct {
s string
cfg Config
}{
"github.com/restic/restic/internal/backend/test"
)
var configTests = []test.ConfigTestData[Config]{
{
"swift:cnt1:/",
Config{
S: "swift:cnt1:/",
Cfg: Config{
Container: "cnt1",
Prefix: "",
Connections: 5,
},
},
{
"swift:cnt2:/prefix",
Config{Container: "cnt2",
S: "swift:cnt2:/prefix",
Cfg: Config{Container: "cnt2",
Prefix: "prefix",
Connections: 5,
},
},
{
"swift:cnt3:/prefix/longer",
Config{Container: "cnt3",
S: "swift:cnt3:/prefix/longer",
Cfg: Config{Container: "cnt3",
Prefix: "prefix/longer",
Connections: 5,
},
@ -31,24 +32,7 @@ var configTests = []struct {
}
func TestParseConfig(t *testing.T) {
for _, test := range configTests {
t.Run("", func(t *testing.T) {
v, err := ParseConfig(test.s)
if err != nil {
t.Fatalf("parsing %q failed: %v", test.s, err)
}
cfg, ok := v.(Config)
if !ok {
t.Fatalf("wrong type returned, want Config, got %T", cfg)
}
if cfg != test.cfg {
t.Fatalf("wrong output for %q, want:\n %#v\ngot:\n %#v",
test.s, test.cfg, cfg)
}
})
}
test.ParseConfigTester(t, ParseConfig, configTests)
}
var configTestsInvalid = []string{

View File

@ -15,13 +15,13 @@ import (
rtest "github.com/restic/restic/internal/test"
)
func newSwiftTestSuite(t testing.TB) *test.Suite {
func newSwiftTestSuite(t testing.TB) *test.Suite[swift.Config] {
tr, err := backend.Transport(backend.TransportOptions{})
if err != nil {
t.Fatalf("cannot create transport for tests: %v", err)
}
return &test.Suite{
return &test.Suite[swift.Config]{
// do not use excessive data
MinimalData: true,
@ -42,14 +42,13 @@ func newSwiftTestSuite(t testing.TB) *test.Suite {
},
// NewConfig returns a config for a new temporary backend that will be used in tests.
NewConfig: func() (interface{}, error) {
swiftcfg, err := swift.ParseConfig(os.Getenv("RESTIC_TEST_SWIFT"))
NewConfig: func() (*swift.Config, error) {
cfg, err := swift.ParseConfig(os.Getenv("RESTIC_TEST_SWIFT"))
if err != nil {
return nil, err
}
cfg := swiftcfg.(swift.Config)
if err = swift.ApplyEnvironment("RESTIC_TEST_", &cfg); err != nil {
if err = cfg.ApplyEnvironment("RESTIC_TEST_"); err != nil {
return nil, err
}
cfg.Prefix += fmt.Sprintf("/test-%d", time.Now().UnixNano())
@ -58,9 +57,7 @@ func newSwiftTestSuite(t testing.TB) *test.Suite {
},
// CreateFn is a function that creates a temporary repository for the tests.
Create: func(config interface{}) (restic.Backend, error) {
cfg := config.(swift.Config)
Create: func(cfg swift.Config) (restic.Backend, error) {
be, err := swift.Open(context.TODO(), cfg, tr)
if err != nil {
return nil, err
@ -79,15 +76,12 @@ func newSwiftTestSuite(t testing.TB) *test.Suite {
},
// OpenFn is a function that opens a previously created temporary repository.
Open: func(config interface{}) (restic.Backend, error) {
cfg := config.(swift.Config)
Open: func(cfg swift.Config) (restic.Backend, error) {
return swift.Open(context.TODO(), cfg, tr)
},
// CleanupFn removes data created during the tests.
Cleanup: func(config interface{}) error {
cfg := config.(swift.Config)
Cleanup: func(cfg swift.Config) error {
be, err := swift.Open(context.TODO(), cfg, tr)
if err != nil {
return err

View File

@ -29,7 +29,7 @@ func remove(t testing.TB, be restic.Backend, h restic.Handle) {
// BenchmarkLoadFile benchmarks the Load() method of a backend by
// loading a complete file.
func (s *Suite) BenchmarkLoadFile(t *testing.B) {
func (s *Suite[C]) BenchmarkLoadFile(t *testing.B) {
be := s.open(t)
defer s.close(t, be)
@ -64,7 +64,7 @@ func (s *Suite) BenchmarkLoadFile(t *testing.B) {
// BenchmarkLoadPartialFile benchmarks the Load() method of a backend by
// loading the remainder of a file starting at a given offset.
func (s *Suite) BenchmarkLoadPartialFile(t *testing.B) {
func (s *Suite[C]) BenchmarkLoadPartialFile(t *testing.B) {
be := s.open(t)
defer s.close(t, be)
@ -101,7 +101,7 @@ func (s *Suite) BenchmarkLoadPartialFile(t *testing.B) {
// BenchmarkLoadPartialFileOffset benchmarks the Load() method of a
// backend by loading a number of bytes of a file starting at a given offset.
func (s *Suite) BenchmarkLoadPartialFileOffset(t *testing.B) {
func (s *Suite[C]) BenchmarkLoadPartialFileOffset(t *testing.B) {
be := s.open(t)
defer s.close(t, be)
@ -139,7 +139,7 @@ func (s *Suite) BenchmarkLoadPartialFileOffset(t *testing.B) {
}
// BenchmarkSave benchmarks the Save() method of a backend.
func (s *Suite) BenchmarkSave(t *testing.B) {
func (s *Suite[C]) BenchmarkSave(t *testing.B) {
be := s.open(t)
defer s.close(t, be)

View File

@ -0,0 +1,28 @@
package test
import (
"fmt"
"reflect"
"testing"
)
type ConfigTestData[C comparable] struct {
S string
Cfg C
}
func ParseConfigTester[C comparable](t *testing.T, parser func(s string) (*C, error), tests []ConfigTestData[C]) {
for i, test := range tests {
t.Run(fmt.Sprint(i), func(t *testing.T) {
cfg, err := parser(test.S)
if err != nil {
t.Fatalf("%s failed: %v", test.S, err)
}
if !reflect.DeepEqual(*cfg, test.Cfg) {
t.Fatalf("input: %s\n wrong config, want:\n %#v\ngot:\n %#v",
test.S, test.Cfg, *cfg)
}
})
}
}

View File

@ -11,21 +11,21 @@ import (
)
// Suite implements a test suite for restic backends.
type Suite struct {
type Suite[C any] struct {
// Config should be used to configure the backend.
Config interface{}
Config *C
// NewConfig returns a config for a new temporary backend that will be used in tests.
NewConfig func() (interface{}, error)
NewConfig func() (*C, error)
// CreateFn is a function that creates a temporary repository for the tests.
Create func(cfg interface{}) (restic.Backend, error)
Create func(cfg C) (restic.Backend, error)
// OpenFn is a function that opens a previously created temporary repository.
Open func(cfg interface{}) (restic.Backend, error)
Open func(cfg C) (restic.Backend, error)
// CleanupFn removes data created during the tests.
Cleanup func(cfg interface{}) error
Cleanup func(cfg C) error
// MinimalData instructs the tests to not use excessive data.
MinimalData bool
@ -40,7 +40,7 @@ type Suite struct {
}
// RunTests executes all defined tests as subtests of t.
func (s *Suite) RunTests(t *testing.T) {
func (s *Suite[C]) RunTests(t *testing.T) {
var err error
s.Config, err = s.NewConfig()
if err != nil {
@ -61,7 +61,7 @@ func (s *Suite) RunTests(t *testing.T) {
}
if s.Cleanup != nil {
if err = s.Cleanup(s.Config); err != nil {
if err = s.Cleanup(*s.Config); err != nil {
t.Fatal(err)
}
}
@ -72,7 +72,7 @@ type testFunction struct {
Fn func(*testing.T)
}
func (s *Suite) testFuncs(t testing.TB) (funcs []testFunction) {
func (s *Suite[C]) testFuncs(t testing.TB) (funcs []testFunction) {
tpe := reflect.TypeOf(s)
v := reflect.ValueOf(s)
@ -107,7 +107,7 @@ type benchmarkFunction struct {
Fn func(*testing.B)
}
func (s *Suite) benchmarkFuncs(t testing.TB) (funcs []benchmarkFunction) {
func (s *Suite[C]) benchmarkFuncs(t testing.TB) (funcs []benchmarkFunction) {
tpe := reflect.TypeOf(s)
v := reflect.ValueOf(s)
@ -138,7 +138,7 @@ func (s *Suite) benchmarkFuncs(t testing.TB) (funcs []benchmarkFunction) {
}
// RunBenchmarks executes all defined benchmarks as subtests of b.
func (s *Suite) RunBenchmarks(b *testing.B) {
func (s *Suite[C]) RunBenchmarks(b *testing.B) {
var err error
s.Config, err = s.NewConfig()
if err != nil {
@ -158,28 +158,28 @@ func (s *Suite) RunBenchmarks(b *testing.B) {
return
}
if err = s.Cleanup(s.Config); err != nil {
if err = s.Cleanup(*s.Config); err != nil {
b.Fatal(err)
}
}
func (s *Suite) create(t testing.TB) restic.Backend {
be, err := s.Create(s.Config)
func (s *Suite[C]) create(t testing.TB) restic.Backend {
be, err := s.Create(*s.Config)
if err != nil {
t.Fatal(err)
}
return be
}
func (s *Suite) open(t testing.TB) restic.Backend {
be, err := s.Open(s.Config)
func (s *Suite[C]) open(t testing.TB) restic.Backend {
be, err := s.Open(*s.Config)
if err != nil {
t.Fatal(err)
}
return be
}
func (s *Suite) close(t testing.TB, be restic.Backend) {
func (s *Suite[C]) close(t testing.TB, be restic.Backend) {
err := be.Close()
if err != nil {
t.Fatal(err)

View File

@ -38,7 +38,7 @@ func beTest(ctx context.Context, be restic.Backend, h restic.Handle) (bool, erro
// TestCreateWithConfig tests that creating a backend in a location which already
// has a config file fails.
func (s *Suite) TestCreateWithConfig(t *testing.T) {
func (s *Suite[C]) TestCreateWithConfig(t *testing.T) {
b := s.open(t)
defer s.close(t, b)
@ -57,7 +57,7 @@ func (s *Suite) TestCreateWithConfig(t *testing.T) {
store(t, b, restic.ConfigFile, []byte("test config"))
// now create the backend again, this must fail
_, err = s.Create(s.Config)
_, err = s.Create(*s.Config)
if err == nil {
t.Fatalf("expected error not found for creating a backend with an existing config file")
}
@ -70,7 +70,7 @@ func (s *Suite) TestCreateWithConfig(t *testing.T) {
}
// TestLocation tests that a location string is returned.
func (s *Suite) TestLocation(t *testing.T) {
func (s *Suite[C]) TestLocation(t *testing.T) {
b := s.open(t)
defer s.close(t, b)
@ -81,7 +81,7 @@ func (s *Suite) TestLocation(t *testing.T) {
}
// TestConfig saves and loads a config from the backend.
func (s *Suite) TestConfig(t *testing.T) {
func (s *Suite[C]) TestConfig(t *testing.T) {
b := s.open(t)
defer s.close(t, b)
@ -118,7 +118,7 @@ func (s *Suite) TestConfig(t *testing.T) {
}
// TestLoad tests the backend's Load function.
func (s *Suite) TestLoad(t *testing.T) {
func (s *Suite[C]) TestLoad(t *testing.T) {
seedRand(t)
b := s.open(t)
@ -222,8 +222,12 @@ func (s *Suite) TestLoad(t *testing.T) {
test.OK(t, b.Remove(context.TODO(), handle))
}
type setter interface {
SetListMaxItems(int)
}
// TestList makes sure that the backend implements List() pagination correctly.
func (s *Suite) TestList(t *testing.T) {
func (s *Suite[C]) TestList(t *testing.T) {
seedRand(t)
numTestFiles := rand.Intn(20) + 20
@ -269,10 +273,6 @@ func (s *Suite) TestList(t *testing.T) {
t.Run(fmt.Sprintf("max-%v", test.maxItems), func(t *testing.T) {
list2 := make(map[restic.ID]int64)
type setter interface {
SetListMaxItems(int)
}
if s, ok := b.(setter); ok {
t.Logf("setting max list items to %d", test.maxItems)
s.SetListMaxItems(test.maxItems)
@ -326,7 +326,7 @@ func (s *Suite) TestList(t *testing.T) {
}
// TestListCancel tests that the context is respected and the error is returned by List.
func (s *Suite) TestListCancel(t *testing.T) {
func (s *Suite[C]) TestListCancel(t *testing.T) {
seedRand(t)
numTestFiles := 5
@ -466,7 +466,7 @@ func (ec errorCloser) Rewind() error {
}
// TestSave tests saving data in the backend.
func (s *Suite) TestSave(t *testing.T) {
func (s *Suite[C]) TestSave(t *testing.T) {
seedRand(t)
b := s.open(t)
@ -582,7 +582,7 @@ func (r *incompleteByteReader) Length() int64 {
}
// TestSaveError tests saving data in the backend.
func (s *Suite) TestSaveError(t *testing.T) {
func (s *Suite[C]) TestSaveError(t *testing.T) {
seedRand(t)
b := s.open(t)
@ -621,7 +621,7 @@ func (b *wrongByteReader) Hash() []byte {
}
// TestSaveWrongHash tests that uploads with a wrong hash fail
func (s *Suite) TestSaveWrongHash(t *testing.T) {
func (s *Suite[C]) TestSaveWrongHash(t *testing.T) {
seedRand(t)
b := s.open(t)
@ -679,7 +679,7 @@ func testLoad(b restic.Backend, h restic.Handle) error {
})
}
func (s *Suite) delayedRemove(t testing.TB, be restic.Backend, handles ...restic.Handle) error {
func (s *Suite[C]) delayedRemove(t testing.TB, be restic.Backend, handles ...restic.Handle) error {
// Some backend (swift, I'm looking at you) may implement delayed
// removal of data. Let's wait a bit if this happens.
@ -746,7 +746,7 @@ func delayedList(t testing.TB, b restic.Backend, tpe restic.FileType, max int, m
}
// TestBackend tests all functions of the backend.
func (s *Suite) TestBackend(t *testing.T) {
func (s *Suite[C]) TestBackend(t *testing.T) {
b := s.open(t)
defer s.close(t, b)
@ -867,7 +867,7 @@ func (s *Suite) TestBackend(t *testing.T) {
}
// TestZZZDelete tests the Delete function. The name ensures that this test is executed last.
func (s *Suite) TestZZZDelete(t *testing.T) {
func (s *Suite[C]) TestZZZDelete(t *testing.T) {
if !test.TestCleanupTempDirs {
t.Skipf("not removing backend, TestCleanupTempDirs is false")
}

View File

@ -80,3 +80,8 @@ type FileInfo struct {
Size int64
Name string
}
// ApplyEnvironmenter fills in a backend configuration from the environment
type ApplyEnvironmenter interface {
ApplyEnvironment(prefix string) error
}