2018-03-30 22:43:18 +02:00
package archiver
import (
2019-03-19 20:27:37 -05:00
"bytes"
2018-03-30 22:43:18 +02:00
"context"
2024-08-03 19:10:11 +02:00
"fmt"
2020-12-28 20:45:53 +01:00
"io"
2018-03-30 22:43:18 +02:00
"os"
"path/filepath"
"runtime"
"strings"
"sync"
2018-05-12 23:08:00 +02:00
"sync/atomic"
2018-03-30 22:43:18 +02:00
"syscall"
"testing"
"time"
2019-05-04 10:34:28 +02:00
"github.com/google/go-cmp/cmp"
2023-10-01 11:40:12 +02:00
"github.com/restic/restic/internal/backend"
2020-12-28 20:45:53 +01:00
"github.com/restic/restic/internal/backend/mem"
2018-03-30 22:43:18 +02:00
"github.com/restic/restic/internal/checker"
2025-09-23 20:01:09 +02:00
"github.com/restic/restic/internal/data"
2018-05-12 23:08:00 +02:00
"github.com/restic/restic/internal/errors"
2024-03-09 17:44:48 +01:00
"github.com/restic/restic/internal/feature"
2018-03-30 22:43:18 +02:00
"github.com/restic/restic/internal/fs"
"github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/restic"
2024-03-29 00:24:03 +01:00
rtest "github.com/restic/restic/internal/test"
2022-05-27 19:08:50 +02:00
"golang.org/x/sync/errgroup"
2018-03-30 22:43:18 +02:00
)
2022-12-11 10:41:22 +01:00
func prepareTempdirRepoSrc ( t testing . TB , src TestDir ) ( string , restic . Repository ) {
2024-03-29 00:24:03 +01:00
tempdir := rtest . TempDir ( t )
2022-12-11 10:41:22 +01:00
repo := repository . TestRepository ( t )
2018-03-30 22:43:18 +02:00
TestCreateFiles ( t , tempdir , src )
2022-12-11 10:41:22 +01:00
return tempdir , repo
2018-03-30 22:43:18 +02:00
}
2025-09-23 20:01:09 +02:00
func saveFile ( t testing . TB , repo archiverRepo , filename string , filesystem fs . FS ) ( * data . Node , ItemStats ) {
2022-05-27 19:08:50 +02:00
wg , ctx := errgroup . WithContext ( context . TODO ( ) )
2021-08-07 22:52:05 +02:00
repo . StartPackUploader ( ctx , wg )
2018-03-30 22:43:18 +02:00
arch := New ( repo , filesystem , Options { } )
2022-05-27 19:08:50 +02:00
arch . runWorkers ( ctx , wg )
2018-03-30 22:43:18 +02:00
2022-05-21 00:31:26 +02:00
arch . Error = func ( item string , err error ) error {
2018-05-20 16:11:36 +02:00
t . Errorf ( "archiver error for %v: %v" , item , err )
return err
}
2018-03-30 22:43:18 +02:00
var (
2022-10-22 12:05:49 +02:00
completeReadingCallback bool
2025-09-23 20:01:09 +02:00
completeCallbackNode * data . Node
2018-03-30 22:43:18 +02:00
completeCallbackStats ItemStats
completeCallback bool
startCallback bool
)
2022-10-22 12:05:49 +02:00
completeReading := func ( ) {
completeReadingCallback = true
if completeCallback {
t . Error ( "callbacks called in wrong order" )
}
}
2025-09-23 20:01:09 +02:00
complete := func ( node * data . Node , stats ItemStats ) {
2018-03-30 22:43:18 +02:00
completeCallback = true
completeCallbackNode = node
completeCallbackStats = stats
}
start := func ( ) {
startCallback = true
}
2024-11-02 20:27:38 +01:00
file , err := arch . FS . OpenFile ( filename , fs . O_NOFOLLOW , false )
2018-03-30 22:43:18 +02:00
if err != nil {
t . Fatal ( err )
}
2024-11-02 20:27:38 +01:00
res := arch . fileSaver . Save ( ctx , "/" , filename , file , start , completeReading , complete )
2018-05-12 21:40:31 +02:00
2022-05-29 11:57:10 +02:00
fnr := res . take ( ctx )
if fnr . err != nil {
t . Fatal ( fnr . err )
2018-03-30 22:43:18 +02:00
}
2022-05-27 19:08:50 +02:00
arch . stopWorkers ( )
err = repo . Flush ( context . Background ( ) )
2018-05-08 22:28:37 +02:00
if err != nil {
t . Fatal ( err )
}
2022-05-27 19:08:50 +02:00
if err := wg . Wait ( ) ; err != nil {
2018-03-30 22:43:18 +02:00
t . Fatal ( err )
}
if ! startCallback {
t . Errorf ( "start callback did not happen" )
}
2022-10-22 12:05:49 +02:00
if ! completeReadingCallback {
t . Errorf ( "completeReading callback did not happen" )
}
2018-03-30 22:43:18 +02:00
if ! completeCallback {
t . Errorf ( "complete callback did not happen" )
}
if completeCallbackNode == nil {
t . Errorf ( "no node returned for complete callback" )
}
2022-05-29 11:57:10 +02:00
if completeCallbackNode != nil && ! fnr . node . Equals ( * completeCallbackNode ) {
2018-03-30 22:43:18 +02:00
t . Errorf ( "different node returned for complete callback" )
}
2022-05-29 11:57:10 +02:00
if completeCallbackStats != fnr . stats {
t . Errorf ( "different stats return for complete callback, want:\n %v\ngot:\n %v" , fnr . stats , completeCallbackStats )
2018-03-30 22:43:18 +02:00
}
2022-05-29 11:57:10 +02:00
return fnr . node , fnr . stats
2018-03-30 22:43:18 +02:00
}
func TestArchiverSaveFile ( t * testing . T ) {
var tests = [ ] TestFile {
2019-04-27 21:19:02 -04:00
{ Content : "" } ,
{ Content : "foo" } ,
2024-03-29 00:24:03 +01:00
{ Content : string ( rtest . Random ( 23 , 12 * 1024 * 1024 + 1287898 ) ) } ,
2018-03-30 22:43:18 +02:00
}
for _ , testfile := range tests {
t . Run ( "" , func ( t * testing . T ) {
ctx , cancel := context . WithCancel ( context . Background ( ) )
defer cancel ( )
2022-12-11 10:41:22 +01:00
tempdir , repo := prepareTempdirRepoSrc ( t , TestDir { "file" : testfile } )
2018-09-05 08:04:55 -04:00
node , stats := saveFile ( t , repo , filepath . Join ( tempdir , "file" ) , fs . Track { FS : fs . Local { } } )
2018-03-30 22:43:18 +02:00
TestEnsureFileContent ( ctx , t , repo , "file" , node , testfile )
if stats . DataSize != uint64 ( len ( testfile . Content ) ) {
t . Errorf ( "wrong stats returned in DataSize, want %d, got %d" , len ( testfile . Content ) , stats . DataSize )
}
if stats . DataBlobs <= 0 && len ( testfile . Content ) > 0 {
t . Errorf ( "wrong stats returned in DataBlobs, want > 0, got %d" , stats . DataBlobs )
}
if stats . TreeSize != 0 {
t . Errorf ( "wrong stats returned in TreeSize, want 0, got %d" , stats . TreeSize )
}
if stats . TreeBlobs != 0 {
t . Errorf ( "wrong stats returned in DataBlobs, want 0, got %d" , stats . DataBlobs )
}
} )
}
}
func TestArchiverSaveFileReaderFS ( t * testing . T ) {
var tests = [ ] struct {
Data string
} {
{ Data : "foo" } ,
2024-03-29 00:24:03 +01:00
{ Data : string ( rtest . Random ( 23 , 12 * 1024 * 1024 + 1287898 ) ) } ,
2018-03-30 22:43:18 +02:00
}
for _ , test := range tests {
t . Run ( "" , func ( t * testing . T ) {
ctx , cancel := context . WithCancel ( context . Background ( ) )
defer cancel ( )
2022-12-11 10:41:22 +01:00
repo := repository . TestRepository ( t )
2018-03-30 22:43:18 +02:00
ts := time . Now ( )
filename := "xx"
2025-04-11 22:07:31 +02:00
readerFs , err := fs . NewReader ( filename , io . NopCloser ( strings . NewReader ( test . Data ) ) , fs . ReaderOptions {
2025-04-11 21:37:32 +02:00
ModTime : ts ,
Mode : 0123 ,
} )
2025-04-11 22:07:31 +02:00
rtest . OK ( t , err )
2018-03-30 22:43:18 +02:00
node , stats := saveFile ( t , repo , filename , readerFs )
TestEnsureFileContent ( ctx , t , repo , "file" , node , TestFile { Content : test . Data } )
if stats . DataSize != uint64 ( len ( test . Data ) ) {
2018-05-20 16:11:36 +02:00
t . Errorf ( "wrong stats returned in DataSize, want %d, got %d" , len ( test . Data ) , stats . DataSize )
}
if stats . DataBlobs <= 0 && len ( test . Data ) > 0 {
t . Errorf ( "wrong stats returned in DataBlobs, want > 0, got %d" , stats . DataBlobs )
}
if stats . TreeSize != 0 {
t . Errorf ( "wrong stats returned in TreeSize, want 0, got %d" , stats . TreeSize )
}
if stats . TreeBlobs != 0 {
t . Errorf ( "wrong stats returned in DataBlobs, want 0, got %d" , stats . DataBlobs )
}
} )
}
}
func TestArchiverSave ( t * testing . T ) {
var tests = [ ] TestFile {
2019-04-27 21:19:02 -04:00
{ Content : "" } ,
{ Content : "foo" } ,
2024-03-29 00:24:03 +01:00
{ Content : string ( rtest . Random ( 23 , 12 * 1024 * 1024 + 1287898 ) ) } ,
2018-05-20 16:11:36 +02:00
}
for _ , testfile := range tests {
t . Run ( "" , func ( t * testing . T ) {
ctx , cancel := context . WithCancel ( context . Background ( ) )
defer cancel ( )
2022-12-11 10:41:22 +01:00
tempdir , repo := prepareTempdirRepoSrc ( t , TestDir { "file" : testfile } )
2018-05-20 16:11:36 +02:00
2022-05-27 19:08:50 +02:00
wg , ctx := errgroup . WithContext ( ctx )
2021-08-07 22:52:05 +02:00
repo . StartPackUploader ( ctx , wg )
2018-05-20 16:11:36 +02:00
2018-09-05 08:04:55 -04:00
arch := New ( repo , fs . Track { FS : fs . Local { } } , Options { } )
2022-05-21 00:31:26 +02:00
arch . Error = func ( item string , err error ) error {
2018-05-20 16:11:36 +02:00
t . Errorf ( "archiver error for %v: %v" , item , err )
return err
}
2022-05-27 19:08:50 +02:00
arch . runWorkers ( ctx , wg )
2024-02-22 22:14:48 +01:00
arch . summary = & Summary { }
2018-05-20 16:11:36 +02:00
2024-02-23 20:22:14 +01:00
node , excluded , err := arch . save ( ctx , "/" , filepath . Join ( tempdir , "file" ) , nil )
2018-05-20 16:11:36 +02:00
if err != nil {
t . Fatal ( err )
}
if excluded {
t . Errorf ( "Save() excluded the node, that's unexpected" )
}
2022-05-29 11:57:10 +02:00
fnr := node . take ( ctx )
if fnr . err != nil {
t . Fatal ( fnr . err )
2018-05-20 16:11:36 +02:00
}
2022-05-29 11:57:10 +02:00
if fnr . node == nil {
2018-05-20 16:11:36 +02:00
t . Fatalf ( "returned node is nil" )
}
2022-05-29 11:57:10 +02:00
stats := fnr . stats
2018-05-20 16:11:36 +02:00
2022-05-27 19:08:50 +02:00
arch . stopWorkers ( )
2018-05-20 16:11:36 +02:00
err = repo . Flush ( ctx )
if err != nil {
t . Fatal ( err )
}
2022-05-29 11:57:10 +02:00
TestEnsureFileContent ( ctx , t , repo , "file" , fnr . node , testfile )
2018-05-20 16:11:36 +02:00
if stats . DataSize != uint64 ( len ( testfile . Content ) ) {
t . Errorf ( "wrong stats returned in DataSize, want %d, got %d" , len ( testfile . Content ) , stats . DataSize )
}
if stats . DataBlobs <= 0 && len ( testfile . Content ) > 0 {
t . Errorf ( "wrong stats returned in DataBlobs, want > 0, got %d" , stats . DataBlobs )
}
if stats . TreeSize != 0 {
t . Errorf ( "wrong stats returned in TreeSize, want 0, got %d" , stats . TreeSize )
}
if stats . TreeBlobs != 0 {
t . Errorf ( "wrong stats returned in DataBlobs, want 0, got %d" , stats . DataBlobs )
}
} )
}
}
func TestArchiverSaveReaderFS ( t * testing . T ) {
var tests = [ ] struct {
Data string
} {
{ Data : "foo" } ,
2024-03-29 00:24:03 +01:00
{ Data : string ( rtest . Random ( 23 , 12 * 1024 * 1024 + 1287898 ) ) } ,
2018-05-20 16:11:36 +02:00
}
for _ , test := range tests {
t . Run ( "" , func ( t * testing . T ) {
ctx , cancel := context . WithCancel ( context . Background ( ) )
defer cancel ( )
2022-12-11 10:41:22 +01:00
repo := repository . TestRepository ( t )
2018-05-20 16:11:36 +02:00
2022-05-27 19:08:50 +02:00
wg , ctx := errgroup . WithContext ( ctx )
2021-08-07 22:52:05 +02:00
repo . StartPackUploader ( ctx , wg )
2022-05-27 19:08:50 +02:00
2018-05-20 16:11:36 +02:00
ts := time . Now ( )
filename := "xx"
2025-04-11 22:07:31 +02:00
readerFs , err := fs . NewReader ( filename , io . NopCloser ( strings . NewReader ( test . Data ) ) , fs . ReaderOptions {
2025-04-11 21:37:32 +02:00
ModTime : ts ,
Mode : 0123 ,
} )
2025-04-11 22:07:31 +02:00
rtest . OK ( t , err )
2018-05-20 16:11:36 +02:00
arch := New ( repo , readerFs , Options { } )
2022-05-21 00:31:26 +02:00
arch . Error = func ( item string , err error ) error {
2018-05-20 16:11:36 +02:00
t . Errorf ( "archiver error for %v: %v" , item , err )
return err
}
2022-05-27 19:08:50 +02:00
arch . runWorkers ( ctx , wg )
2024-02-22 22:14:48 +01:00
arch . summary = & Summary { }
2018-05-20 16:11:36 +02:00
2024-02-23 20:22:14 +01:00
node , excluded , err := arch . save ( ctx , "/" , filename , nil )
2018-05-20 16:11:36 +02:00
t . Logf ( "Save returned %v %v" , node , err )
if err != nil {
t . Fatal ( err )
}
if excluded {
t . Errorf ( "Save() excluded the node, that's unexpected" )
}
2022-05-29 11:57:10 +02:00
fnr := node . take ( ctx )
if fnr . err != nil {
t . Fatal ( fnr . err )
2018-05-20 16:11:36 +02:00
}
2022-05-29 11:57:10 +02:00
if fnr . node == nil {
2018-05-20 16:11:36 +02:00
t . Fatalf ( "returned node is nil" )
}
2022-05-29 11:57:10 +02:00
stats := fnr . stats
2018-05-20 16:11:36 +02:00
2022-05-27 19:08:50 +02:00
arch . stopWorkers ( )
2018-05-20 16:11:36 +02:00
err = repo . Flush ( ctx )
if err != nil {
t . Fatal ( err )
}
2022-05-29 11:57:10 +02:00
TestEnsureFileContent ( ctx , t , repo , "file" , fnr . node , TestFile { Content : test . Data } )
2018-05-20 16:11:36 +02:00
if stats . DataSize != uint64 ( len ( test . Data ) ) {
2018-03-30 22:43:18 +02:00
t . Errorf ( "wrong stats returned in DataSize, want %d, got %d" , len ( test . Data ) , stats . DataSize )
}
if stats . DataBlobs <= 0 && len ( test . Data ) > 0 {
t . Errorf ( "wrong stats returned in DataBlobs, want > 0, got %d" , stats . DataBlobs )
}
if stats . TreeSize != 0 {
t . Errorf ( "wrong stats returned in TreeSize, want 0, got %d" , stats . TreeSize )
}
if stats . TreeBlobs != 0 {
t . Errorf ( "wrong stats returned in DataBlobs, want 0, got %d" , stats . DataBlobs )
}
} )
}
}
func BenchmarkArchiverSaveFileSmall ( b * testing . B ) {
const fileSize = 4 * 1024
d := TestDir { "file" : TestFile {
2024-03-29 00:24:03 +01:00
Content : string ( rtest . Random ( 23 , fileSize ) ) ,
2018-03-30 22:43:18 +02:00
} }
b . SetBytes ( fileSize )
for i := 0 ; i < b . N ; i ++ {
b . StopTimer ( )
2022-12-11 10:41:22 +01:00
tempdir , repo := prepareTempdirRepoSrc ( b , d )
2018-03-30 22:43:18 +02:00
b . StartTimer ( )
2018-09-05 08:04:55 -04:00
_ , stats := saveFile ( b , repo , filepath . Join ( tempdir , "file" ) , fs . Track { FS : fs . Local { } } )
2018-03-30 22:43:18 +02:00
b . StopTimer ( )
if stats . DataSize != fileSize {
b . Errorf ( "wrong stats returned in DataSize, want %d, got %d" , fileSize , stats . DataSize )
}
if stats . DataBlobs <= 0 {
b . Errorf ( "wrong stats returned in DataBlobs, want > 0, got %d" , stats . DataBlobs )
}
if stats . TreeSize != 0 {
b . Errorf ( "wrong stats returned in TreeSize, want 0, got %d" , stats . TreeSize )
}
if stats . TreeBlobs != 0 {
b . Errorf ( "wrong stats returned in DataBlobs, want 0, got %d" , stats . DataBlobs )
}
b . StartTimer ( )
}
}
func BenchmarkArchiverSaveFileLarge ( b * testing . B ) {
const fileSize = 40 * 1024 * 1024 + 1287898
d := TestDir { "file" : TestFile {
2024-03-29 00:24:03 +01:00
Content : string ( rtest . Random ( 23 , fileSize ) ) ,
2018-03-30 22:43:18 +02:00
} }
b . SetBytes ( fileSize )
for i := 0 ; i < b . N ; i ++ {
b . StopTimer ( )
2022-12-11 10:41:22 +01:00
tempdir , repo := prepareTempdirRepoSrc ( b , d )
2018-03-30 22:43:18 +02:00
b . StartTimer ( )
2018-09-05 08:04:55 -04:00
_ , stats := saveFile ( b , repo , filepath . Join ( tempdir , "file" ) , fs . Track { FS : fs . Local { } } )
2018-03-30 22:43:18 +02:00
b . StopTimer ( )
if stats . DataSize != fileSize {
b . Errorf ( "wrong stats returned in DataSize, want %d, got %d" , fileSize , stats . DataSize )
}
if stats . DataBlobs <= 0 {
b . Errorf ( "wrong stats returned in DataBlobs, want > 0, got %d" , stats . DataBlobs )
}
if stats . TreeSize != 0 {
b . Errorf ( "wrong stats returned in TreeSize, want 0, got %d" , stats . TreeSize )
}
if stats . TreeBlobs != 0 {
b . Errorf ( "wrong stats returned in DataBlobs, want 0, got %d" , stats . DataBlobs )
}
b . StartTimer ( )
}
}
type blobCountingRepo struct {
2024-05-19 15:11:32 +02:00
archiverRepo
2018-03-30 22:43:18 +02:00
m sync . Mutex
saved map [ restic . BlobHandle ] uint
}
2022-05-01 14:26:57 +02:00
func ( repo * blobCountingRepo ) SaveBlob ( ctx context . Context , t restic . BlobType , buf [ ] byte , id restic . ID , storeDuplicate bool ) ( restic . ID , bool , int , error ) {
2024-05-19 15:11:32 +02:00
id , exists , size , err := repo . archiverRepo . SaveBlob ( ctx , t , buf , id , storeDuplicate )
2020-06-06 22:20:44 +02:00
if exists {
2022-05-01 14:26:57 +02:00
return id , exists , size , err
2020-06-06 22:20:44 +02:00
}
2018-03-30 22:43:18 +02:00
h := restic . BlobHandle { ID : id , Type : t }
repo . m . Lock ( )
repo . saved [ h ] ++
repo . m . Unlock ( )
2022-05-01 14:26:57 +02:00
return id , exists , size , err
2018-03-30 22:43:18 +02:00
}
2025-09-23 20:01:09 +02:00
func ( repo * blobCountingRepo ) SaveTree ( ctx context . Context , t * data . Tree ) ( restic . ID , error ) {
id , err := data . SaveTree ( ctx , repo . archiverRepo , t )
2018-03-30 22:43:18 +02:00
h := restic . BlobHandle { ID : id , Type : restic . TreeBlob }
repo . m . Lock ( )
repo . saved [ h ] ++
repo . m . Unlock ( )
return id , err
}
func appendToFile ( t testing . TB , filename string , data [ ] byte ) {
f , err := os . OpenFile ( filename , os . O_CREATE | os . O_APPEND | os . O_WRONLY , 0644 )
if err != nil {
t . Fatal ( err )
}
_ , err = f . Write ( data )
if err != nil {
_ = f . Close ( )
t . Fatal ( err )
}
err = f . Close ( )
if err != nil {
t . Fatal ( err )
}
}
func TestArchiverSaveFileIncremental ( t * testing . T ) {
2024-03-29 00:24:03 +01:00
tempdir := rtest . TempDir ( t )
2018-03-30 22:43:18 +02:00
repo := & blobCountingRepo {
2024-05-19 15:11:32 +02:00
archiverRepo : repository . TestRepository ( t ) ,
saved : make ( map [ restic . BlobHandle ] uint ) ,
2018-03-30 22:43:18 +02:00
}
2024-03-29 00:24:03 +01:00
data := rtest . Random ( 23 , 512 * 1024 + 887898 )
2018-03-30 22:43:18 +02:00
testfile := filepath . Join ( tempdir , "testfile" )
for i := 0 ; i < 3 ; i ++ {
appendToFile ( t , testfile , data )
2018-09-05 08:04:55 -04:00
node , _ := saveFile ( t , repo , testfile , fs . Track { FS : fs . Local { } } )
2018-03-30 22:43:18 +02:00
t . Logf ( "node blobs: %v" , node . Content )
for h , n := range repo . saved {
if n > 1 {
t . Errorf ( "iteration %v: blob %v saved more than once (%d times)" , i , h , n )
}
}
}
}
func save ( t testing . TB , filename string , data [ ] byte ) {
f , err := os . Create ( filename )
if err != nil {
t . Fatal ( err )
}
_ , err = f . Write ( data )
if err != nil {
t . Fatal ( err )
}
err = f . Sync ( )
if err != nil {
t . Fatal ( err )
}
err = f . Close ( )
if err != nil {
t . Fatal ( err )
}
}
2020-07-08 09:59:00 +02:00
func chmodTwice ( t testing . TB , name string ) {
// POSIX says that ctime is updated "even if the file status does not
// change", but let's make sure it does change, just in case.
err := os . Chmod ( name , 0700 )
2024-03-29 00:24:03 +01:00
rtest . OK ( t , err )
2020-07-08 09:59:00 +02:00
sleep ( )
err = os . Chmod ( name , 0600 )
2024-03-29 00:24:03 +01:00
rtest . OK ( t , err )
2020-07-08 09:59:00 +02:00
}
2024-11-03 16:01:59 +01:00
func lstat ( t testing . TB , name string ) * fs . ExtendedFileInfo {
2018-03-30 22:43:18 +02:00
fi , err := os . Lstat ( name )
if err != nil {
t . Fatal ( err )
}
2024-11-03 16:01:59 +01:00
return fs . ExtendedStat ( fi )
2018-03-30 22:43:18 +02:00
}
func setTimestamp ( t testing . TB , filename string , atime , mtime time . Time ) {
var utimes = [ ... ] syscall . Timespec {
syscall . NsecToTimespec ( atime . UnixNano ( ) ) ,
syscall . NsecToTimespec ( mtime . UnixNano ( ) ) ,
}
err := syscall . UtimesNano ( filename , utimes [ : ] )
if err != nil {
t . Fatal ( err )
}
}
func remove ( t testing . TB , filename string ) {
err := os . Remove ( filename )
if err != nil {
t . Fatal ( err )
}
}
2020-07-08 09:59:00 +02:00
func rename ( t testing . TB , oldname , newname string ) {
err := os . Rename ( oldname , newname )
if err != nil {
t . Fatal ( err )
}
}
2025-09-23 20:01:09 +02:00
func nodeFromFile ( t testing . TB , localFs fs . FS , filename string ) * data . Node {
2024-11-02 20:27:38 +01:00
meta , err := localFs . OpenFile ( filename , fs . O_NOFOLLOW , true )
rtest . OK ( t , err )
2025-09-21 19:24:48 +02:00
node , err := meta . ToNode ( false , t . Logf )
2024-11-02 20:27:38 +01:00
rtest . OK ( t , err )
rtest . OK ( t , meta . Close ( ) )
2018-03-30 22:43:18 +02:00
return node
}
2020-07-08 09:59:00 +02:00
// sleep sleeps long enough to ensure a timestamp change.
func sleep ( ) {
d := 50 * time . Millisecond
2018-03-30 22:43:18 +02:00
if runtime . GOOS == "darwin" {
2020-07-08 09:59:00 +02:00
// On older Darwin instances, the file system only supports one second
// granularity.
d = 1500 * time . Millisecond
2018-03-30 22:43:18 +02:00
}
2020-07-08 09:59:00 +02:00
time . Sleep ( d )
}
2018-03-30 22:43:18 +02:00
2020-07-08 09:59:00 +02:00
func TestFileChanged ( t * testing . T ) {
var defaultContent = [ ] byte ( "foobar" )
2018-03-30 22:43:18 +02:00
var tests = [ ] struct {
2019-05-05 12:51:26 +02:00
Name string
SkipForWindows bool
Content [ ] byte
Modify func ( t testing . TB , filename string )
2020-07-08 09:59:00 +02:00
ChangeIgnore uint
2019-05-05 12:51:26 +02:00
SameFile bool
2018-03-30 22:43:18 +02:00
} {
{
Name : "same-content-new-file" ,
Modify : func ( t testing . TB , filename string ) {
remove ( t , filename )
sleep ( )
save ( t , filename , defaultContent )
} ,
} ,
{
Name : "same-content-new-timestamp" ,
Modify : func ( t testing . TB , filename string ) {
sleep ( )
save ( t , filename , defaultContent )
} ,
} ,
2019-03-19 20:27:37 -05:00
{
Name : "new-content-same-timestamp" ,
2019-05-05 12:51:26 +02:00
// on Windows, there's no "create time" field users cannot modify,
// so we're unable to detect if a file has been modified when the
// timestamps are reset, so we skip this test for Windows
SkipForWindows : true ,
2019-03-19 20:27:37 -05:00
Modify : func ( t testing . TB , filename string ) {
2019-04-23 22:39:13 -05:00
fi , err := os . Stat ( filename )
if err != nil {
t . Fatal ( err )
}
2019-03-19 20:27:37 -05:00
extFI := fs . ExtendedStat ( fi )
save ( t , filename , bytes . ToUpper ( defaultContent ) )
sleep ( )
2019-04-23 22:39:13 -05:00
setTimestamp ( t , filename , extFI . AccessTime , extFI . ModTime )
2019-03-19 20:27:37 -05:00
} ,
} ,
2018-03-30 22:43:18 +02:00
{
Name : "other-content" ,
Modify : func ( t testing . TB , filename string ) {
remove ( t , filename )
sleep ( )
save ( t , filename , [ ] byte ( "xxxxxx" ) )
} ,
} ,
{
Name : "longer-content" ,
Modify : func ( t testing . TB , filename string ) {
save ( t , filename , [ ] byte ( "xxxxxxxxxxxxxxxxxxxxxx" ) )
} ,
} ,
{
Name : "new-file" ,
Modify : func ( t testing . TB , filename string ) {
remove ( t , filename )
sleep ( )
save ( t , filename , defaultContent )
} ,
} ,
2020-07-08 09:59:00 +02:00
{
Name : "ctime-change" ,
Modify : chmodTwice ,
SameFile : false ,
SkipForWindows : true , // No ctime on Windows, so this test would fail.
} ,
{
Name : "ignore-ctime-change" ,
Modify : chmodTwice ,
ChangeIgnore : ChangeIgnoreCtime ,
SameFile : true ,
SkipForWindows : true , // No ctime on Windows, so this test is meaningless.
} ,
2019-03-10 21:22:54 +01:00
{
Name : "ignore-inode" ,
Modify : func ( t testing . TB , filename string ) {
fi := lstat ( t , filename )
2020-07-08 09:59:00 +02:00
// First create the new file, then remove the old one,
// so that the old file retains its inode number.
tempname := filename + ".old"
rename ( t , filename , tempname )
2019-03-10 21:22:54 +01:00
save ( t , filename , defaultContent )
2020-07-08 09:59:00 +02:00
remove ( t , tempname )
2024-11-03 16:01:59 +01:00
setTimestamp ( t , filename , fi . ModTime , fi . ModTime )
2019-03-10 21:22:54 +01:00
} ,
2020-07-08 09:59:00 +02:00
ChangeIgnore : ChangeIgnoreCtime | ChangeIgnoreInode ,
SameFile : true ,
2019-03-10 21:22:54 +01:00
} ,
2018-03-30 22:43:18 +02:00
}
for _ , test := range tests {
t . Run ( test . Name , func ( t * testing . T ) {
2019-05-05 12:51:26 +02:00
if runtime . GOOS == "windows" && test . SkipForWindows {
t . Skip ( "don't run test on Windows" )
}
2024-03-29 00:24:03 +01:00
tempdir := rtest . TempDir ( t )
2018-03-30 22:43:18 +02:00
filename := filepath . Join ( tempdir , "file" )
content := defaultContent
if test . Content != nil {
content = test . Content
}
save ( t , filename , content )
2024-08-27 14:35:40 +02:00
fs := & fs . Local { }
2024-11-16 16:53:34 +01:00
fiBefore , err := fs . Lstat ( filename )
rtest . OK ( t , err )
2024-11-02 20:27:38 +01:00
node := nodeFromFile ( t , fs , filename )
2018-03-30 22:43:18 +02:00
2024-11-16 16:53:34 +01:00
if fileChanged ( fiBefore , node , 0 ) {
2018-03-30 22:43:18 +02:00
t . Fatalf ( "unchanged file detected as changed" )
}
test . Modify ( t , filename )
fiAfter := lstat ( t , filename )
2019-05-05 12:50:47 +02:00
if test . SameFile {
// file should be detected as unchanged
2024-11-16 16:53:34 +01:00
if fileChanged ( fiAfter , node , test . ChangeIgnore ) {
2019-03-10 21:22:54 +01:00
t . Fatalf ( "unmodified file detected as changed" )
2019-05-05 12:50:47 +02:00
}
} else {
// file should be detected as changed
2024-11-16 16:53:34 +01:00
if ! fileChanged ( fiAfter , node , test . ChangeIgnore ) && ! test . SameFile {
2019-03-10 21:22:54 +01:00
t . Fatalf ( "modified file detected as unchanged" )
}
2018-03-30 22:43:18 +02:00
}
} )
}
}
func TestFilChangedSpecialCases ( t * testing . T ) {
2024-03-29 00:24:03 +01:00
tempdir := rtest . TempDir ( t )
2018-03-30 22:43:18 +02:00
filename := filepath . Join ( tempdir , "file" )
content := [ ] byte ( "foobar" )
save ( t , filename , content )
t . Run ( "nil-node" , func ( t * testing . T ) {
fi := lstat ( t , filename )
2024-11-16 16:53:34 +01:00
if ! fileChanged ( fi , nil , 0 ) {
2018-03-30 22:43:18 +02:00
t . Fatal ( "nil node detected as unchanged" )
}
} )
t . Run ( "type-change" , func ( t * testing . T ) {
fi := lstat ( t , filename )
2024-11-02 20:27:38 +01:00
node := nodeFromFile ( t , & fs . Local { } , filename )
2025-09-23 20:01:09 +02:00
node . Type = data . NodeTypeSymlink
2024-11-16 16:53:34 +01:00
if ! fileChanged ( fi , node , 0 ) {
2018-03-30 22:43:18 +02:00
t . Fatal ( "node with changed type detected as unchanged" )
}
} )
}
func TestArchiverSaveDir ( t * testing . T ) {
const targetNodeName = "targetdir"
var tests = [ ] struct {
src TestDir
chdir string
target string
want TestDir
} {
{
src : TestDir {
2024-03-29 00:24:03 +01:00
"targetfile" : TestFile { Content : string ( rtest . Random ( 888 , 2 * 1024 * 1024 + 5000 ) ) } ,
2018-03-30 22:43:18 +02:00
} ,
target : "." ,
want : TestDir {
"targetdir" : TestDir {
2024-03-29 00:24:03 +01:00
"targetfile" : TestFile { Content : string ( rtest . Random ( 888 , 2 * 1024 * 1024 + 5000 ) ) } ,
2018-03-30 22:43:18 +02:00
} ,
} ,
} ,
{
src : TestDir {
"targetdir" : TestDir {
"foo" : TestFile { Content : "foo" } ,
"emptyfile" : TestFile { Content : "" } ,
"bar" : TestFile { Content : "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" } ,
2024-03-29 00:24:03 +01:00
"largefile" : TestFile { Content : string ( rtest . Random ( 888 , 2 * 1024 * 1024 + 5000 ) ) } ,
"largerfile" : TestFile { Content : string ( rtest . Random ( 234 , 5 * 1024 * 1024 + 5000 ) ) } ,
2018-03-30 22:43:18 +02:00
} ,
} ,
target : "targetdir" ,
} ,
{
src : TestDir {
"foo" : TestFile { Content : "foo" } ,
"emptyfile" : TestFile { Content : "" } ,
"bar" : TestFile { Content : "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" } ,
} ,
target : "." ,
want : TestDir {
"targetdir" : TestDir {
"foo" : TestFile { Content : "foo" } ,
"emptyfile" : TestFile { Content : "" } ,
"bar" : TestFile { Content : "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" } ,
} ,
} ,
} ,
{
src : TestDir {
"foo" : TestDir {
"subdir" : TestDir {
"x" : TestFile { Content : "xxx" } ,
"y" : TestFile { Content : "yyyyyyyyyyyyyyyy" } ,
"z" : TestFile { Content : "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" } ,
} ,
"file" : TestFile { Content : "just a test" } ,
} ,
} ,
chdir : "foo/subdir" ,
target : "../../" ,
want : TestDir {
"targetdir" : TestDir {
"foo" : TestDir {
"subdir" : TestDir {
"x" : TestFile { Content : "xxx" } ,
"y" : TestFile { Content : "yyyyyyyyyyyyyyyy" } ,
"z" : TestFile { Content : "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" } ,
} ,
"file" : TestFile { Content : "just a test" } ,
} ,
} ,
} ,
} ,
{
src : TestDir {
"foo" : TestDir {
"file" : TestFile { Content : "just a test" } ,
"file2" : TestFile { Content : "again" } ,
} ,
} ,
target : "./foo" ,
want : TestDir {
"targetdir" : TestDir {
"file" : TestFile { Content : "just a test" } ,
"file2" : TestFile { Content : "again" } ,
} ,
} ,
} ,
}
for _ , test := range tests {
t . Run ( "" , func ( t * testing . T ) {
2022-12-11 10:41:22 +01:00
tempdir , repo := prepareTempdirRepoSrc ( t , test . src )
2018-03-30 22:43:18 +02:00
2022-05-27 19:08:50 +02:00
wg , ctx := errgroup . WithContext ( context . Background ( ) )
2021-08-07 22:52:05 +02:00
repo . StartPackUploader ( ctx , wg )
2022-05-27 19:08:50 +02:00
2024-11-02 20:27:38 +01:00
testFS := fs . Track { FS : fs . Local { } }
arch := New ( repo , testFS , Options { } )
2022-05-27 19:08:50 +02:00
arch . runWorkers ( ctx , wg )
2024-02-22 22:14:48 +01:00
arch . summary = & Summary { }
2018-03-30 22:43:18 +02:00
chdir := tempdir
if test . chdir != "" {
chdir = filepath . Join ( chdir , test . chdir )
}
2024-03-29 00:24:03 +01:00
back := rtest . Chdir ( t , chdir )
2018-03-30 22:43:18 +02:00
defer back ( )
2024-11-02 20:27:38 +01:00
meta , err := testFS . OpenFile ( test . target , fs . O_NOFOLLOW , true )
rtest . OK ( t , err )
ft , err := arch . saveDir ( ctx , "/" , test . target , meta , nil , nil )
rtest . OK ( t , err )
rtest . OK ( t , meta . Close ( ) )
2018-04-30 15:13:03 +02:00
2022-05-29 11:57:10 +02:00
fnr := ft . take ( ctx )
node , stats := fnr . node , fnr . stats
2018-05-08 22:28:37 +02:00
2018-03-30 22:43:18 +02:00
t . Logf ( "stats: %v" , stats )
if stats . DataSize != 0 {
t . Errorf ( "wrong stats returned in DataSize, want 0, got %d" , stats . DataSize )
}
if stats . DataBlobs != 0 {
t . Errorf ( "wrong stats returned in DataBlobs, want 0, got %d" , stats . DataBlobs )
}
2020-03-07 21:48:59 +01:00
if stats . TreeSize == 0 {
2018-03-30 22:43:18 +02:00
t . Errorf ( "wrong stats returned in TreeSize, want > 0, got %d" , stats . TreeSize )
}
if stats . TreeBlobs <= 0 {
t . Errorf ( "wrong stats returned in TreeBlobs, want > 0, got %d" , stats . TreeBlobs )
}
node . Name = targetNodeName
2025-09-23 20:01:09 +02:00
tree := & data . Tree { Nodes : [ ] * data . Node { node } }
treeID , err := data . SaveTree ( ctx , repo , tree )
2018-03-30 22:43:18 +02:00
if err != nil {
t . Fatal ( err )
}
2022-05-27 19:08:50 +02:00
arch . stopWorkers ( )
2018-03-30 22:43:18 +02:00
err = repo . Flush ( ctx )
if err != nil {
t . Fatal ( err )
}
2022-05-27 19:08:50 +02:00
err = wg . Wait ( )
if err != nil {
t . Fatal ( err )
}
2018-03-30 22:43:18 +02:00
want := test . want
if want == nil {
want = test . src
}
2022-05-27 19:08:50 +02:00
TestEnsureTree ( context . TODO ( ) , t , "/" , repo , treeID , want )
2018-03-30 22:43:18 +02:00
} )
}
}
func TestArchiverSaveDirIncremental ( t * testing . T ) {
2024-03-29 00:24:03 +01:00
tempdir := rtest . TempDir ( t )
2018-03-30 22:43:18 +02:00
repo := & blobCountingRepo {
2024-05-19 15:11:32 +02:00
archiverRepo : repository . TestRepository ( t ) ,
saved : make ( map [ restic . BlobHandle ] uint ) ,
2018-03-30 22:43:18 +02:00
}
appendToFile ( t , filepath . Join ( tempdir , "testfile" ) , [ ] byte ( "foobar" ) )
// save the empty directory several times in a row, then have a look if the
// archiver did save the same tree several times
for i := 0 ; i < 5 ; i ++ {
2022-05-27 19:08:50 +02:00
wg , ctx := errgroup . WithContext ( context . TODO ( ) )
2021-08-07 22:52:05 +02:00
repo . StartPackUploader ( ctx , wg )
2018-03-30 22:43:18 +02:00
2024-11-02 20:27:38 +01:00
testFS := fs . Track { FS : fs . Local { } }
arch := New ( repo , testFS , Options { } )
2022-05-27 19:08:50 +02:00
arch . runWorkers ( ctx , wg )
2024-02-22 22:14:48 +01:00
arch . summary = & Summary { }
2018-03-30 22:43:18 +02:00
2024-11-02 20:27:38 +01:00
meta , err := testFS . OpenFile ( tempdir , fs . O_NOFOLLOW , true )
rtest . OK ( t , err )
ft , err := arch . saveDir ( ctx , "/" , tempdir , meta , nil , nil )
rtest . OK ( t , err )
rtest . OK ( t , meta . Close ( ) )
2018-04-30 15:13:03 +02:00
2022-05-29 11:57:10 +02:00
fnr := ft . take ( ctx )
node , stats := fnr . node , fnr . stats
2018-05-08 22:28:37 +02:00
2018-03-30 22:43:18 +02:00
if err != nil {
t . Fatal ( err )
}
if i == 0 {
// operation must have added new tree data
if stats . DataSize != 0 {
t . Errorf ( "wrong stats returned in DataSize, want 0, got %d" , stats . DataSize )
}
if stats . DataBlobs != 0 {
t . Errorf ( "wrong stats returned in DataBlobs, want 0, got %d" , stats . DataBlobs )
}
2020-03-07 21:48:59 +01:00
if stats . TreeSize == 0 {
2018-03-30 22:43:18 +02:00
t . Errorf ( "wrong stats returned in TreeSize, want > 0, got %d" , stats . TreeSize )
}
if stats . TreeBlobs <= 0 {
t . Errorf ( "wrong stats returned in TreeBlobs, want > 0, got %d" , stats . TreeBlobs )
}
} else {
// operation must not have added any new data
if stats . DataSize != 0 {
t . Errorf ( "wrong stats returned in DataSize, want 0, got %d" , stats . DataSize )
}
if stats . DataBlobs != 0 {
t . Errorf ( "wrong stats returned in DataBlobs, want 0, got %d" , stats . DataBlobs )
}
if stats . TreeSize != 0 {
t . Errorf ( "wrong stats returned in TreeSize, want 0, got %d" , stats . TreeSize )
}
if stats . TreeBlobs != 0 {
t . Errorf ( "wrong stats returned in TreeBlobs, want 0, got %d" , stats . TreeBlobs )
}
}
t . Logf ( "node subtree %v" , node . Subtree )
2022-05-27 19:08:50 +02:00
arch . stopWorkers ( )
err = repo . Flush ( ctx )
if err != nil {
t . Fatal ( err )
}
err = wg . Wait ( )
2018-03-30 22:43:18 +02:00
if err != nil {
t . Fatal ( err )
}
for h , n := range repo . saved {
if n > 1 {
t . Errorf ( "iteration %v: blob %v saved more than once (%d times)" , i , h , n )
}
}
}
}
2020-04-22 22:23:02 +02:00
// bothZeroOrNeither fails the test if only one of exp, act is zero.
func bothZeroOrNeither ( tb testing . TB , exp , act uint64 ) {
2024-02-23 21:46:39 +01:00
tb . Helper ( )
2020-04-22 22:23:02 +02:00
if ( exp == 0 && act != 0 ) || ( exp != 0 && act == 0 ) {
2024-03-29 00:24:03 +01:00
rtest . Equals ( tb , exp , act )
2020-04-22 22:23:02 +02:00
}
}
2018-03-30 22:43:18 +02:00
func TestArchiverSaveTree ( t * testing . T ) {
symlink := func ( from , to string ) func ( t testing . TB ) {
return func ( t testing . TB ) {
err := os . Symlink ( from , to )
if err != nil {
t . Fatal ( err )
}
}
}
2020-04-22 22:23:02 +02:00
// The toplevel directory is not counted in the ItemStats
2018-03-30 22:43:18 +02:00
var tests = [ ] struct {
src TestDir
prepare func ( t testing . TB )
targets [ ] string
want TestDir
2024-02-23 21:46:39 +01:00
stat Summary
2018-03-30 22:43:18 +02:00
} {
{
src : TestDir {
2025-02-28 19:38:33 +00:00
"targetfile" : TestFile { Content : "foobar" } ,
2018-03-30 22:43:18 +02:00
} ,
targets : [ ] string { "targetfile" } ,
want : TestDir {
2025-02-28 19:38:33 +00:00
"targetfile" : TestFile { Content : "foobar" } ,
2018-03-30 22:43:18 +02:00
} ,
2024-02-23 21:46:39 +01:00
stat : Summary {
ItemStats : ItemStats { 1 , 6 , 32 + 6 , 0 , 0 , 0 } ,
ProcessedBytes : 6 ,
Files : ChangeStats { 1 , 0 , 0 } ,
Dirs : ChangeStats { 0 , 0 , 0 } ,
} ,
2018-03-30 22:43:18 +02:00
} ,
{
src : TestDir {
2025-02-28 19:38:33 +00:00
"targetfile" : TestFile { Content : "foobar" } ,
2018-03-30 22:43:18 +02:00
} ,
prepare : symlink ( "targetfile" , "filesymlink" ) ,
targets : [ ] string { "targetfile" , "filesymlink" } ,
want : TestDir {
2025-02-28 19:38:33 +00:00
"targetfile" : TestFile { Content : "foobar" } ,
2018-03-30 22:43:18 +02:00
"filesymlink" : TestSymlink { Target : "targetfile" } ,
} ,
2024-02-23 21:46:39 +01:00
stat : Summary {
ItemStats : ItemStats { 1 , 6 , 32 + 6 , 0 , 0 , 0 } ,
ProcessedBytes : 6 ,
Files : ChangeStats { 1 , 0 , 0 } ,
Dirs : ChangeStats { 0 , 0 , 0 } ,
} ,
2018-03-30 22:43:18 +02:00
} ,
{
src : TestDir {
"dir" : TestDir {
"subdir" : TestDir {
"subsubdir" : TestDir {
2025-02-28 19:38:33 +00:00
"targetfile" : TestFile { Content : "foobar" } ,
2018-03-30 22:43:18 +02:00
} ,
} ,
2025-02-28 19:38:33 +00:00
"otherfile" : TestFile { Content : "xxx" } ,
2018-03-30 22:43:18 +02:00
} ,
} ,
prepare : symlink ( "subdir" , filepath . FromSlash ( "dir/symlink" ) ) ,
targets : [ ] string { filepath . FromSlash ( "dir/symlink" ) } ,
want : TestDir {
"dir" : TestDir {
"symlink" : TestSymlink { Target : "subdir" } ,
} ,
} ,
2024-02-23 21:46:39 +01:00
stat : Summary {
ItemStats : ItemStats { 0 , 0 , 0 , 1 , 0x154 , 0x16a } ,
ProcessedBytes : 0 ,
Files : ChangeStats { 0 , 0 , 0 } ,
Dirs : ChangeStats { 1 , 0 , 0 } ,
} ,
2018-03-30 22:43:18 +02:00
} ,
{
src : TestDir {
"dir" : TestDir {
"subdir" : TestDir {
"subsubdir" : TestDir {
2025-02-28 19:38:33 +00:00
"targetfile" : TestFile { Content : "foobar" } ,
2018-03-30 22:43:18 +02:00
} ,
} ,
2025-02-28 19:38:33 +00:00
"otherfile" : TestFile { Content : "xxx" } ,
2018-03-30 22:43:18 +02:00
} ,
} ,
prepare : symlink ( "subdir" , filepath . FromSlash ( "dir/symlink" ) ) ,
targets : [ ] string { filepath . FromSlash ( "dir/symlink/subsubdir" ) } ,
want : TestDir {
"dir" : TestDir {
"symlink" : TestDir {
"subsubdir" : TestDir {
2025-02-28 19:38:33 +00:00
"targetfile" : TestFile { Content : "foobar" } ,
2018-03-30 22:43:18 +02:00
} ,
} ,
} ,
} ,
2024-02-23 21:46:39 +01:00
stat : Summary {
ItemStats : ItemStats { 1 , 6 , 32 + 6 , 3 , 0x47f , 0x4c1 } ,
ProcessedBytes : 6 ,
Files : ChangeStats { 1 , 0 , 0 } ,
Dirs : ChangeStats { 3 , 0 , 0 } ,
} ,
2018-03-30 22:43:18 +02:00
} ,
}
for _ , test := range tests {
t . Run ( "" , func ( t * testing . T ) {
2022-12-11 10:41:22 +01:00
tempdir , repo := prepareTempdirRepoSrc ( t , test . src )
2018-03-30 22:43:18 +02:00
2018-09-05 08:04:55 -04:00
testFS := fs . Track { FS : fs . Local { } }
2018-03-30 22:43:18 +02:00
arch := New ( repo , testFS , Options { } )
2020-04-22 22:23:02 +02:00
2022-05-27 19:08:50 +02:00
wg , ctx := errgroup . WithContext ( context . TODO ( ) )
2021-08-07 22:52:05 +02:00
repo . StartPackUploader ( ctx , wg )
2022-05-27 19:08:50 +02:00
arch . runWorkers ( ctx , wg )
2024-02-22 22:14:48 +01:00
arch . summary = & Summary { }
2018-03-30 22:43:18 +02:00
2024-03-29 00:24:03 +01:00
back := rtest . Chdir ( t , tempdir )
2018-03-30 22:43:18 +02:00
defer back ( )
if test . prepare != nil {
test . prepare ( t )
}
2024-08-27 11:26:52 +02:00
atree , err := newTree ( testFS , test . targets )
2018-03-30 22:43:18 +02:00
if err != nil {
t . Fatal ( err )
}
2024-02-23 20:22:14 +01:00
fn , _ , err := arch . saveTree ( ctx , "/" , atree , nil , nil )
2018-03-30 22:43:18 +02:00
if err != nil {
t . Fatal ( err )
}
2022-08-19 23:08:13 +02:00
fnr := fn . take ( context . TODO ( ) )
if fnr . err != nil {
t . Fatal ( fnr . err )
2018-03-30 22:43:18 +02:00
}
2022-08-19 23:08:13 +02:00
treeID := * fnr . node . Subtree
2022-05-27 19:08:50 +02:00
arch . stopWorkers ( )
err = repo . Flush ( ctx )
2018-05-08 22:28:37 +02:00
if err != nil {
t . Fatal ( err )
}
2022-05-27 19:08:50 +02:00
err = wg . Wait ( )
2018-03-30 22:43:18 +02:00
if err != nil {
t . Fatal ( err )
}
want := test . want
if want == nil {
want = test . src
}
2022-05-27 19:08:50 +02:00
TestEnsureTree ( context . TODO ( ) , t , "/" , repo , treeID , want )
2024-02-23 21:46:39 +01:00
stat := arch . summary
2020-04-22 22:23:02 +02:00
bothZeroOrNeither ( t , uint64 ( test . stat . DataBlobs ) , uint64 ( stat . DataBlobs ) )
bothZeroOrNeither ( t , uint64 ( test . stat . TreeBlobs ) , uint64 ( stat . TreeBlobs ) )
bothZeroOrNeither ( t , test . stat . DataSize , stat . DataSize )
2022-05-01 14:41:36 +02:00
bothZeroOrNeither ( t , test . stat . DataSizeInRepo , stat . DataSizeInRepo )
bothZeroOrNeither ( t , test . stat . TreeSizeInRepo , stat . TreeSizeInRepo )
2024-03-29 00:24:03 +01:00
rtest . Equals ( t , test . stat . ProcessedBytes , stat . ProcessedBytes )
rtest . Equals ( t , test . stat . Files , stat . Files )
rtest . Equals ( t , test . stat . Dirs , stat . Dirs )
2018-03-30 22:43:18 +02:00
} )
}
}
func TestArchiverSnapshot ( t * testing . T ) {
var tests = [ ] struct {
name string
src TestDir
want TestDir
chdir string
targets [ ] string
} {
{
name : "single-file" ,
src : TestDir {
"foo" : TestFile { Content : "foo" } ,
} ,
targets : [ ] string { "foo" } ,
} ,
{
name : "file-current-dir" ,
src : TestDir {
"foo" : TestFile { Content : "foo" } ,
} ,
targets : [ ] string { "./foo" } ,
} ,
{
name : "dir" ,
src : TestDir {
"target" : TestDir {
"foo" : TestFile { Content : "foo" } ,
} ,
} ,
targets : [ ] string { "target" } ,
} ,
{
name : "dir-current-dir" ,
src : TestDir {
"target" : TestDir {
"foo" : TestFile { Content : "foo" } ,
} ,
} ,
targets : [ ] string { "./target" } ,
} ,
{
name : "content-dir-current-dir" ,
src : TestDir {
"target" : TestDir {
"foo" : TestFile { Content : "foo" } ,
} ,
} ,
targets : [ ] string { "./target/." } ,
} ,
{
name : "current-dir" ,
src : TestDir {
"target" : TestDir {
"foo" : TestFile { Content : "foo" } ,
} ,
} ,
targets : [ ] string { "." } ,
} ,
{
name : "subdir" ,
src : TestDir {
"subdir" : TestDir {
"foo" : TestFile { Content : "foo" } ,
"subsubdir" : TestDir {
"foo" : TestFile { Content : "foo in subsubdir" } ,
} ,
} ,
"other" : TestFile { Content : "another file" } ,
} ,
targets : [ ] string { "subdir" } ,
want : TestDir {
"subdir" : TestDir {
"foo" : TestFile { Content : "foo" } ,
"subsubdir" : TestDir {
"foo" : TestFile { Content : "foo in subsubdir" } ,
} ,
} ,
} ,
} ,
{
name : "subsubdir" ,
src : TestDir {
"subdir" : TestDir {
"foo" : TestFile { Content : "foo" } ,
"subsubdir" : TestDir {
"foo" : TestFile { Content : "foo in subsubdir" } ,
} ,
} ,
"other" : TestFile { Content : "another file" } ,
} ,
targets : [ ] string { "subdir/subsubdir" } ,
want : TestDir {
"subdir" : TestDir {
"subsubdir" : TestDir {
"foo" : TestFile { Content : "foo in subsubdir" } ,
} ,
} ,
} ,
} ,
{
name : "parent-dir" ,
src : TestDir {
"subdir" : TestDir {
"foo" : TestFile { Content : "foo" } ,
} ,
"other" : TestFile { Content : "another file" } ,
} ,
chdir : "subdir" ,
targets : [ ] string { ".." } ,
} ,
{
name : "parent-parent-dir" ,
src : TestDir {
"subdir" : TestDir {
"foo" : TestFile { Content : "foo" } ,
"subsubdir" : TestDir {
"empty" : TestFile { Content : "" } ,
} ,
} ,
"other" : TestFile { Content : "another file" } ,
} ,
chdir : "subdir/subsubdir" ,
targets : [ ] string { "../.." } ,
} ,
{
name : "parent-parent-dir-slash" ,
src : TestDir {
"subdir" : TestDir {
"subsubdir" : TestDir {
"foo" : TestFile { Content : "foo" } ,
} ,
} ,
"other" : TestFile { Content : "another file" } ,
} ,
chdir : "subdir/subsubdir" ,
targets : [ ] string { "../../" } ,
want : TestDir {
"subdir" : TestDir {
"subsubdir" : TestDir {
"foo" : TestFile { Content : "foo" } ,
} ,
} ,
"other" : TestFile { Content : "another file" } ,
} ,
} ,
{
name : "parent-subdir" ,
src : TestDir {
"subdir" : TestDir {
"foo" : TestFile { Content : "foo" } ,
} ,
"other" : TestFile { Content : "another file" } ,
} ,
chdir : "subdir" ,
targets : [ ] string { "../subdir" } ,
want : TestDir {
"subdir" : TestDir {
"foo" : TestFile { Content : "foo" } ,
} ,
} ,
} ,
{
name : "parent-parent-dir-subdir" ,
src : TestDir {
"subdir" : TestDir {
"subsubdir" : TestDir {
"foo" : TestFile { Content : "foo" } ,
} ,
} ,
"other" : TestFile { Content : "another file" } ,
} ,
chdir : "subdir/subsubdir" ,
targets : [ ] string { "../../subdir/subsubdir" } ,
want : TestDir {
"subdir" : TestDir {
"subsubdir" : TestDir {
"foo" : TestFile { Content : "foo" } ,
} ,
} ,
} ,
} ,
{
name : "included-multiple1" ,
src : TestDir {
"subdir" : TestDir {
"subsubdir" : TestDir {
"foo" : TestFile { Content : "foo" } ,
} ,
"other" : TestFile { Content : "another file" } ,
} ,
} ,
targets : [ ] string { "subdir" , "subdir/subsubdir" } ,
} ,
{
name : "included-multiple2" ,
src : TestDir {
"subdir" : TestDir {
"subsubdir" : TestDir {
"foo" : TestFile { Content : "foo" } ,
} ,
"other" : TestFile { Content : "another file" } ,
} ,
} ,
targets : [ ] string { "subdir/subsubdir" , "subdir" } ,
} ,
{
name : "collision" ,
src : TestDir {
"subdir" : TestDir {
"foo" : TestFile { Content : "foo in subdir" } ,
"subsubdir" : TestDir {
"foo" : TestFile { Content : "foo in subsubdir" } ,
} ,
} ,
"foo" : TestFile { Content : "another file" } ,
} ,
chdir : "subdir" ,
targets : [ ] string { "." , "../foo" } ,
want : TestDir {
"foo" : TestFile { Content : "foo in subdir" } ,
"subsubdir" : TestDir {
"foo" : TestFile { Content : "foo in subsubdir" } ,
} ,
"foo-1" : TestFile { Content : "another file" } ,
} ,
} ,
}
for _ , test := range tests {
t . Run ( test . name , func ( t * testing . T ) {
ctx , cancel := context . WithCancel ( context . Background ( ) )
defer cancel ( )
2022-12-11 10:41:22 +01:00
tempdir , repo := prepareTempdirRepoSrc ( t , test . src )
2018-03-30 22:43:18 +02:00
2018-09-05 08:04:55 -04:00
arch := New ( repo , fs . Track { FS : fs . Local { } } , Options { } )
2018-03-30 22:43:18 +02:00
chdir := tempdir
if test . chdir != "" {
chdir = filepath . Join ( chdir , filepath . FromSlash ( test . chdir ) )
}
2024-03-29 00:24:03 +01:00
back := rtest . Chdir ( t , chdir )
2018-03-30 22:43:18 +02:00
defer back ( )
var targets [ ] string
for _ , target := range test . targets {
targets = append ( targets , os . ExpandEnv ( target ) )
}
t . Logf ( "targets: %v" , targets )
2024-02-22 22:14:48 +01:00
sn , snapshotID , _ , err := arch . Snapshot ( ctx , targets , SnapshotOptions { Time : time . Now ( ) } )
2018-03-30 22:43:18 +02:00
if err != nil {
t . Fatal ( err )
}
t . Logf ( "saved as %v" , snapshotID . Str ( ) )
want := test . want
if want == nil {
want = test . src
}
TestEnsureSnapshot ( t , repo , snapshotID , want )
2024-04-14 11:42:26 +02:00
checker . TestCheckRepo ( t , repo , false )
2018-03-30 22:43:18 +02:00
// check that the snapshot contains the targets with absolute paths
for i , target := range sn . Paths {
atarget , err := filepath . Abs ( test . targets [ i ] )
if err != nil {
t . Fatal ( err )
}
if target != atarget {
t . Errorf ( "wrong path in snapshot: want %v, got %v" , atarget , target )
}
}
} )
}
}
2024-08-30 11:25:51 +02:00
func TestResolveRelativeTargetsSpecial ( t * testing . T ) {
var tests = [ ] struct {
name string
targets [ ] string
expected [ ] string
win bool
} {
{
name : "basic relative path" ,
targets : [ ] string { filepath . FromSlash ( "some/path" ) } ,
expected : [ ] string { filepath . FromSlash ( "some/path" ) } ,
} ,
{
name : "partial relative path" ,
targets : [ ] string { filepath . FromSlash ( "../some/path" ) } ,
expected : [ ] string { filepath . FromSlash ( "../some/path" ) } ,
} ,
{
name : "basic absolute path" ,
targets : [ ] string { filepath . FromSlash ( "/some/path" ) } ,
expected : [ ] string { filepath . FromSlash ( "/some/path" ) } ,
} ,
{
name : "volume name" ,
targets : [ ] string { "C:" } ,
expected : [ ] string { "C:\\" } ,
win : true ,
} ,
{
name : "volume root path" ,
targets : [ ] string { "C:\\" } ,
expected : [ ] string { "C:\\" } ,
win : true ,
} ,
{
name : "UNC path" ,
targets : [ ] string { "\\\\server\\volume" } ,
expected : [ ] string { "\\\\server\\volume\\" } ,
win : true ,
} ,
{
name : "UNC path with trailing slash" ,
targets : [ ] string { "\\\\server\\volume\\" } ,
expected : [ ] string { "\\\\server\\volume\\" } ,
win : true ,
} ,
}
for _ , test := range tests {
t . Run ( test . name , func ( t * testing . T ) {
if test . win && runtime . GOOS != "windows" {
t . Skip ( "skip test on unix" )
}
targets , err := resolveRelativeTargets ( & fs . Local { } , test . targets )
rtest . OK ( t , err )
rtest . Equals ( t , test . expected , targets )
} )
}
}
2018-03-30 22:43:18 +02:00
func TestArchiverSnapshotSelect ( t * testing . T ) {
var tests = [ ] struct {
name string
src TestDir
want TestDir
selFn SelectFunc
2018-05-20 15:58:55 +02:00
err string
2018-03-30 22:43:18 +02:00
} {
{
name : "include-all" ,
src : TestDir {
"work" : TestDir {
"foo" : TestFile { Content : "foo" } ,
"foo.txt" : TestFile { Content : "foo text file" } ,
"subdir" : TestDir {
"other" : TestFile { Content : "other in subdir" } ,
"bar.txt" : TestFile { Content : "bar.txt in subdir" } ,
} ,
} ,
"other" : TestFile { Content : "another file" } ,
} ,
2024-11-03 16:01:59 +01:00
selFn : func ( item string , fi * fs . ExtendedFileInfo , _ fs . FS ) bool {
2018-03-30 22:43:18 +02:00
return true
} ,
} ,
{
name : "exclude-all" ,
src : TestDir {
"work" : TestDir {
"foo" : TestFile { Content : "foo" } ,
"foo.txt" : TestFile { Content : "foo text file" } ,
"subdir" : TestDir {
"other" : TestFile { Content : "other in subdir" } ,
"bar.txt" : TestFile { Content : "bar.txt in subdir" } ,
} ,
} ,
"other" : TestFile { Content : "another file" } ,
} ,
2024-11-03 16:01:59 +01:00
selFn : func ( item string , fi * fs . ExtendedFileInfo , _ fs . FS ) bool {
2018-03-30 22:43:18 +02:00
return false
} ,
2018-05-20 15:58:55 +02:00
err : "snapshot is empty" ,
2018-03-30 22:43:18 +02:00
} ,
{
name : "exclude-txt-files" ,
src : TestDir {
"work" : TestDir {
"foo" : TestFile { Content : "foo" } ,
"foo.txt" : TestFile { Content : "foo text file" } ,
"subdir" : TestDir {
"other" : TestFile { Content : "other in subdir" } ,
"bar.txt" : TestFile { Content : "bar.txt in subdir" } ,
} ,
} ,
"other" : TestFile { Content : "another file" } ,
} ,
want : TestDir {
"work" : TestDir {
"foo" : TestFile { Content : "foo" } ,
"subdir" : TestDir {
"other" : TestFile { Content : "other in subdir" } ,
} ,
} ,
"other" : TestFile { Content : "another file" } ,
} ,
2024-11-03 16:01:59 +01:00
selFn : func ( item string , fi * fs . ExtendedFileInfo , _ fs . FS ) bool {
2020-03-06 23:35:09 +01:00
return filepath . Ext ( item ) != ".txt"
2018-03-30 22:43:18 +02:00
} ,
} ,
{
name : "exclude-dir" ,
src : TestDir {
"work" : TestDir {
"foo" : TestFile { Content : "foo" } ,
"foo.txt" : TestFile { Content : "foo text file" } ,
"subdir" : TestDir {
"other" : TestFile { Content : "other in subdir" } ,
"bar.txt" : TestFile { Content : "bar.txt in subdir" } ,
} ,
} ,
"other" : TestFile { Content : "another file" } ,
} ,
want : TestDir {
"work" : TestDir {
"foo" : TestFile { Content : "foo" } ,
"foo.txt" : TestFile { Content : "foo text file" } ,
} ,
"other" : TestFile { Content : "another file" } ,
} ,
2024-11-03 16:01:59 +01:00
selFn : func ( item string , fi * fs . ExtendedFileInfo , fs fs . FS ) bool {
2024-08-27 12:07:26 +02:00
return fs . Base ( item ) != "subdir"
2018-03-30 22:43:18 +02:00
} ,
} ,
{
name : "select-absolute-paths" ,
src : TestDir {
"foo" : TestFile { Content : "foo" } ,
} ,
2024-11-03 16:01:59 +01:00
selFn : func ( item string , fi * fs . ExtendedFileInfo , fs fs . FS ) bool {
2024-08-27 12:07:26 +02:00
return fs . IsAbs ( item )
2018-03-30 22:43:18 +02:00
} ,
} ,
}
for _ , test := range tests {
t . Run ( test . name , func ( t * testing . T ) {
ctx , cancel := context . WithCancel ( context . Background ( ) )
defer cancel ( )
2022-12-11 10:41:22 +01:00
tempdir , repo := prepareTempdirRepoSrc ( t , test . src )
2018-03-30 22:43:18 +02:00
2018-09-05 08:04:55 -04:00
arch := New ( repo , fs . Track { FS : fs . Local { } } , Options { } )
2018-03-30 22:43:18 +02:00
arch . Select = test . selFn
2024-03-29 00:24:03 +01:00
back := rtest . Chdir ( t , tempdir )
2018-03-30 22:43:18 +02:00
defer back ( )
targets := [ ] string { "." }
2024-02-22 22:14:48 +01:00
_ , snapshotID , _ , err := arch . Snapshot ( ctx , targets , SnapshotOptions { Time : time . Now ( ) } )
2018-05-20 15:58:55 +02:00
if test . err != "" {
if err == nil {
t . Fatalf ( "expected error not found, got %v, wanted %q" , err , test . err )
}
if err . Error ( ) != test . err {
t . Fatalf ( "unexpected error, want %q, got %q" , test . err , err )
}
return
}
2018-03-30 22:43:18 +02:00
if err != nil {
t . Fatal ( err )
}
t . Logf ( "saved as %v" , snapshotID . Str ( ) )
want := test . want
if want == nil {
want = test . src
}
TestEnsureSnapshot ( t , repo , snapshotID , want )
2024-04-14 11:42:26 +02:00
checker . TestCheckRepo ( t , repo , false )
2018-03-30 22:43:18 +02:00
} )
}
}
// MockFS keeps track which files are read.
type MockFS struct {
fs . FS
m sync . Mutex
bytesRead map [ string ] int // tracks bytes read from all opened files
}
2024-11-02 20:27:38 +01:00
func ( m * MockFS ) OpenFile ( name string , flag int , metadataOnly bool ) ( fs . File , error ) {
f , err := m . FS . OpenFile ( name , flag , metadataOnly )
2018-03-30 22:43:18 +02:00
if err != nil {
return f , err
}
return MockFile { File : f , fs : m , filename : name } , nil
}
type MockFile struct {
fs . File
filename string
fs * MockFS
}
func ( f MockFile ) Read ( p [ ] byte ) ( int , error ) {
n , err := f . File . Read ( p )
if n > 0 {
f . fs . m . Lock ( )
f . fs . bytesRead [ f . filename ] += n
f . fs . m . Unlock ( )
}
return n , err
}
2025-09-23 20:01:09 +02:00
func checkSnapshotStats ( t * testing . T , sn * data . Snapshot , stat Summary ) {
2024-11-01 15:50:09 +01:00
t . Helper ( )
rtest . Equals ( t , stat . BackupStart , sn . Summary . BackupStart , "BackupStart" )
// BackupEnd is set to time.Now() and can't be compared to a fixed value
rtest . Equals ( t , stat . Files . New , sn . Summary . FilesNew , "FilesNew" )
rtest . Equals ( t , stat . Files . Changed , sn . Summary . FilesChanged , "FilesChanged" )
rtest . Equals ( t , stat . Files . Unchanged , sn . Summary . FilesUnmodified , "FilesUnmodified" )
rtest . Equals ( t , stat . Dirs . New , sn . Summary . DirsNew , "DirsNew" )
rtest . Equals ( t , stat . Dirs . Changed , sn . Summary . DirsChanged , "DirsChanged" )
rtest . Equals ( t , stat . Dirs . Unchanged , sn . Summary . DirsUnmodified , "DirsUnmodified" )
rtest . Equals ( t , stat . ProcessedBytes , sn . Summary . TotalBytesProcessed , "TotalBytesProcessed" )
rtest . Equals ( t , stat . Files . New + stat . Files . Changed + stat . Files . Unchanged , sn . Summary . TotalFilesProcessed , "TotalFilesProcessed" )
2024-02-23 22:05:15 +01:00
bothZeroOrNeither ( t , uint64 ( stat . DataBlobs ) , uint64 ( sn . Summary . DataBlobs ) )
bothZeroOrNeither ( t , uint64 ( stat . TreeBlobs ) , uint64 ( sn . Summary . TreeBlobs ) )
2025-02-28 19:38:33 +00:00
bothZeroOrNeither ( t , stat . DataSize + stat . TreeSize , sn . Summary . DataAdded )
bothZeroOrNeither ( t , stat . DataSizeInRepo + stat . TreeSizeInRepo , sn . Summary . DataAddedPacked )
2024-02-23 22:05:15 +01:00
}
2018-03-30 22:43:18 +02:00
func TestArchiverParent ( t * testing . T ) {
var tests = [ ] struct {
2024-02-23 21:46:39 +01:00
src TestDir
modify func ( path string )
2025-09-23 03:07:30 +02:00
opts SnapshotOptions
2024-02-23 21:46:39 +01:00
statInitial Summary
statSecond Summary
2018-03-30 22:43:18 +02:00
} {
{
src : TestDir {
2024-03-29 00:24:03 +01:00
"targetfile" : TestFile { Content : string ( rtest . Random ( 888 , 2 * 1024 * 1024 + 5000 ) ) } ,
2018-03-30 22:43:18 +02:00
} ,
2024-02-23 21:46:39 +01:00
statInitial : Summary {
Files : ChangeStats { 1 , 0 , 0 } ,
Dirs : ChangeStats { 0 , 0 , 0 } ,
ProcessedBytes : 2102152 ,
2024-02-23 22:05:15 +01:00
ItemStats : ItemStats { 3 , 0x201593 , 0x201632 , 1 , 0 , 0 } ,
2024-02-23 21:46:39 +01:00
} ,
statSecond : Summary {
Files : ChangeStats { 0 , 0 , 1 } ,
Dirs : ChangeStats { 0 , 0 , 0 } ,
ProcessedBytes : 2102152 ,
} ,
} ,
2025-09-23 03:07:30 +02:00
{
src : TestDir {
"targetfile" : TestFile { Content : string ( rtest . Random ( 888 , 2 * 1024 * 1024 + 5000 ) ) } ,
} ,
opts : SnapshotOptions {
SkipIfUnchanged : true ,
} ,
statInitial : Summary {
Files : ChangeStats { 1 , 0 , 0 } ,
Dirs : ChangeStats { 0 , 0 , 0 } ,
ProcessedBytes : 2102152 ,
ItemStats : ItemStats { 3 , 0x201593 , 0x201632 , 1 , 0 , 0 } ,
} ,
statSecond : Summary {
Files : ChangeStats { 0 , 0 , 1 } ,
Dirs : ChangeStats { 0 , 0 , 0 } ,
ProcessedBytes : 2102152 ,
} ,
} ,
2024-02-23 21:46:39 +01:00
{
src : TestDir {
"targetDir" : TestDir {
2024-03-29 00:24:03 +01:00
"targetfile" : TestFile { Content : string ( rtest . Random ( 888 , 1234 ) ) } ,
"targetfile2" : TestFile { Content : string ( rtest . Random ( 888 , 1235 ) ) } ,
2024-02-23 21:46:39 +01:00
} ,
} ,
statInitial : Summary {
Files : ChangeStats { 2 , 0 , 0 } ,
Dirs : ChangeStats { 1 , 0 , 0 } ,
ProcessedBytes : 2469 ,
2024-02-23 22:05:15 +01:00
ItemStats : ItemStats { 2 , 0xe1c , 0xcd9 , 2 , 0 , 0 } ,
2024-02-23 21:46:39 +01:00
} ,
statSecond : Summary {
Files : ChangeStats { 0 , 0 , 2 } ,
Dirs : ChangeStats { 0 , 0 , 1 } ,
ProcessedBytes : 2469 ,
} ,
} ,
{
src : TestDir {
"targetDir" : TestDir {
2024-03-29 00:24:03 +01:00
"targetfile" : TestFile { Content : string ( rtest . Random ( 888 , 1234 ) ) } ,
2024-02-23 21:46:39 +01:00
} ,
2024-03-29 00:24:03 +01:00
"targetfile2" : TestFile { Content : string ( rtest . Random ( 888 , 1235 ) ) } ,
2024-02-23 21:46:39 +01:00
} ,
modify : func ( path string ) {
remove ( t , filepath . Join ( path , "targetDir" , "targetfile" ) )
save ( t , filepath . Join ( path , "targetfile2" ) , [ ] byte ( "foobar" ) )
} ,
statInitial : Summary {
Files : ChangeStats { 2 , 0 , 0 } ,
Dirs : ChangeStats { 1 , 0 , 0 } ,
ProcessedBytes : 2469 ,
2024-02-23 22:05:15 +01:00
ItemStats : ItemStats { 2 , 0xe13 , 0xcf8 , 2 , 0 , 0 } ,
2024-02-23 21:46:39 +01:00
} ,
statSecond : Summary {
Files : ChangeStats { 0 , 1 , 0 } ,
Dirs : ChangeStats { 0 , 1 , 0 } ,
ProcessedBytes : 6 ,
2024-02-23 22:05:15 +01:00
ItemStats : ItemStats { 1 , 0x305 , 0x233 , 2 , 0 , 0 } ,
2018-03-30 22:43:18 +02:00
} ,
} ,
}
for _ , test := range tests {
t . Run ( "" , func ( t * testing . T ) {
ctx , cancel := context . WithCancel ( context . Background ( ) )
defer cancel ( )
2022-12-11 10:41:22 +01:00
tempdir , repo := prepareTempdirRepoSrc ( t , test . src )
2018-03-30 22:43:18 +02:00
testFS := & MockFS {
2018-09-05 08:04:55 -04:00
FS : fs . Track { FS : fs . Local { } } ,
2018-03-30 22:43:18 +02:00
bytesRead : make ( map [ string ] int ) ,
}
arch := New ( repo , testFS , Options { } )
2024-03-29 00:24:03 +01:00
back := rtest . Chdir ( t , tempdir )
2018-03-30 22:43:18 +02:00
defer back ( )
2025-09-23 03:07:30 +02:00
opts := test . opts
opts . Time = time . Now ( )
firstSnapshot , firstSnapshotID , summary , err := arch . Snapshot ( ctx , [ ] string { "." } , opts )
2018-03-30 22:43:18 +02:00
if err != nil {
t . Fatal ( err )
}
t . Logf ( "first backup saved as %v" , firstSnapshotID . Str ( ) )
t . Logf ( "testfs: %v" , testFS )
// check that all files have been read exactly once
TestWalkFiles ( t , "." , test . src , func ( filename string , item interface { } ) error {
file , ok := item . ( TestFile )
if ! ok {
return nil
}
n , ok := testFS . bytesRead [ filename ]
if ! ok {
t . Fatalf ( "file %v was not read at all" , filename )
}
if n != len ( file . Content ) {
t . Fatalf ( "file %v: read %v bytes, wanted %v bytes" , filename , n , len ( file . Content ) )
}
return nil
} )
2024-03-29 00:24:03 +01:00
rtest . Equals ( t , test . statInitial . Files , summary . Files )
rtest . Equals ( t , test . statInitial . Dirs , summary . Dirs )
rtest . Equals ( t , test . statInitial . ProcessedBytes , summary . ProcessedBytes )
2025-09-23 03:07:30 +02:00
rtest . Assert ( t , summary . BackupStart . Before ( summary . BackupEnd ) , "BackupStart %v is not before BackupEnd %v" , summary . BackupStart , summary . BackupEnd )
2024-02-23 22:05:15 +01:00
checkSnapshotStats ( t , firstSnapshot , test . statInitial )
2024-02-23 21:46:39 +01:00
if test . modify != nil {
test . modify ( tempdir )
}
2018-03-30 22:43:18 +02:00
2025-09-23 03:07:30 +02:00
opts = test . opts
opts . Time = time . Now ( )
opts . ParentSnapshot = firstSnapshot
2024-02-23 21:46:39 +01:00
testFS . bytesRead = map [ string ] int { }
2024-02-23 22:05:15 +01:00
secondSnapshot , secondSnapshotID , summary , err := arch . Snapshot ( ctx , [ ] string { "." } , opts )
2018-03-30 22:43:18 +02:00
if err != nil {
t . Fatal ( err )
}
2024-02-23 21:46:39 +01:00
if test . modify == nil {
// check that no files were read this time
2024-03-29 00:24:03 +01:00
rtest . Equals ( t , map [ string ] int { } , testFS . bytesRead )
2024-02-23 21:46:39 +01:00
}
2024-03-29 00:24:03 +01:00
rtest . Equals ( t , test . statSecond . Files , summary . Files )
rtest . Equals ( t , test . statSecond . Dirs , summary . Dirs )
rtest . Equals ( t , test . statSecond . ProcessedBytes , summary . ProcessedBytes )
2025-09-23 03:07:30 +02:00
rtest . Assert ( t , summary . BackupStart . Before ( summary . BackupEnd ) , "BackupStart %v is not before BackupEnd %v" , summary . BackupStart , summary . BackupEnd )
2018-03-30 22:43:18 +02:00
2025-09-23 03:07:30 +02:00
if secondSnapshot != nil {
checkSnapshotStats ( t , secondSnapshot , test . statSecond )
t . Logf ( "second backup saved as %v" , secondSnapshotID . Str ( ) )
t . Logf ( "testfs: %v" , testFS )
}
2018-03-30 22:43:18 +02:00
2024-04-14 11:42:26 +02:00
checker . TestCheckRepo ( t , repo , false )
2018-03-30 22:43:18 +02:00
} )
}
}
func TestArchiverErrorReporting ( t * testing . T ) {
ignoreErrorForBasename := func ( basename string ) ErrorFunc {
2022-05-21 00:31:26 +02:00
return func ( item string , err error ) error {
2025-06-12 22:43:45 +02:00
if filepath . Base ( item ) == basename {
t . Logf ( "ignoring error for %v: %v" , basename , err )
2018-03-30 22:43:18 +02:00
return nil
}
t . Errorf ( "error handler called for unexpected file %v: %v" , item , err )
return err
}
}
chmodUnreadable := func ( filename string ) func ( testing . TB ) {
return func ( t testing . TB ) {
if runtime . GOOS == "windows" {
t . Skip ( "Skipping this test for windows" )
}
err := os . Chmod ( filepath . FromSlash ( filename ) , 0004 )
if err != nil {
t . Fatal ( err )
}
}
}
var tests = [ ] struct {
2025-06-12 22:37:57 +02:00
name string
targets [ ] string
src TestDir
want TestDir
prepare func ( t testing . TB )
errFn ErrorFunc
2025-09-05 19:35:25 +02:00
errStr [ ] string
2018-03-30 22:43:18 +02:00
} {
{
name : "no-error" ,
src : TestDir {
"targetfile" : TestFile { Content : "foobar" } ,
} ,
} ,
{
name : "file-unreadable" ,
src : TestDir {
"targetfile" : TestFile { Content : "foobar" } ,
} ,
2025-06-12 22:37:57 +02:00
prepare : chmodUnreadable ( "targetfile" ) ,
2025-09-05 19:35:25 +02:00
errStr : [ ] string { "open targetfile: permission denied" } ,
2018-03-30 22:43:18 +02:00
} ,
{
name : "file-unreadable-ignore-error" ,
src : TestDir {
"targetfile" : TestFile { Content : "foobar" } ,
"other" : TestFile { Content : "xxx" } ,
} ,
want : TestDir {
"other" : TestFile { Content : "xxx" } ,
} ,
prepare : chmodUnreadable ( "targetfile" ) ,
errFn : ignoreErrorForBasename ( "targetfile" ) ,
} ,
{
name : "file-subdir-unreadable" ,
src : TestDir {
"subdir" : TestDir {
"targetfile" : TestFile { Content : "foobar" } ,
} ,
} ,
2025-06-12 22:37:57 +02:00
prepare : chmodUnreadable ( "subdir/targetfile" ) ,
2025-09-05 19:35:25 +02:00
errStr : [ ] string { "open subdir/targetfile: permission denied" } ,
2018-03-30 22:43:18 +02:00
} ,
{
name : "file-subdir-unreadable-ignore-error" ,
src : TestDir {
"subdir" : TestDir {
"targetfile" : TestFile { Content : "foobar" } ,
"other" : TestFile { Content : "xxx" } ,
} ,
} ,
want : TestDir {
"subdir" : TestDir {
"other" : TestFile { Content : "xxx" } ,
} ,
} ,
prepare : chmodUnreadable ( "subdir/targetfile" ) ,
errFn : ignoreErrorForBasename ( "targetfile" ) ,
} ,
2025-06-12 22:37:57 +02:00
{
name : "parent-dir-missing" ,
targets : [ ] string { "subdir/missing" } ,
src : TestDir { } ,
2025-09-21 22:59:59 +02:00
errStr : [ ] string { "stat subdir: no such file or directory" , "CreateFile subdir: The system cannot find the file specified" , "GetFileAttributesEx subdir: The system cannot find the file specified" } ,
2025-06-12 22:37:57 +02:00
} ,
2025-06-12 22:43:45 +02:00
{
name : "parent-dir-missing-filtered" ,
targets : [ ] string { "targetfile" , "subdir/missing" } ,
src : TestDir {
"targetfile" : TestFile { Content : "foobar" } ,
} ,
errFn : ignoreErrorForBasename ( "subdir" ) ,
} ,
2018-03-30 22:43:18 +02:00
}
for _ , test := range tests {
t . Run ( test . name , func ( t * testing . T ) {
ctx , cancel := context . WithCancel ( context . Background ( ) )
defer cancel ( )
2022-12-11 10:41:22 +01:00
tempdir , repo := prepareTempdirRepoSrc ( t , test . src )
2018-03-30 22:43:18 +02:00
2024-03-29 00:24:03 +01:00
back := rtest . Chdir ( t , tempdir )
2018-03-30 22:43:18 +02:00
defer back ( )
if test . prepare != nil {
test . prepare ( t )
}
2018-09-05 08:04:55 -04:00
arch := New ( repo , fs . Track { FS : fs . Local { } } , Options { } )
2018-03-30 22:43:18 +02:00
arch . Error = test . errFn
2025-06-12 22:37:57 +02:00
target := test . targets
if len ( target ) == 0 {
target = [ ] string { "." }
}
_ , snapshotID , _ , err := arch . Snapshot ( ctx , target , SnapshotOptions { Time : time . Now ( ) } )
2025-09-05 19:35:25 +02:00
if test . errStr != nil {
// check if any of the expected errors are contained in the error message
for _ , errStr := range test . errStr {
if strings . Contains ( err . Error ( ) , errStr ) {
t . Logf ( "found expected error (%v)" , err )
return
}
2018-03-30 22:43:18 +02:00
}
2025-06-12 22:37:57 +02:00
t . Fatalf ( "expected error (%v) not returned by archiver, got (%v)" , test . errStr , err )
2018-03-30 22:43:18 +02:00
return
}
if err != nil {
t . Fatalf ( "unexpected error of type %T found: %v" , err , err )
}
t . Logf ( "saved as %v" , snapshotID . Str ( ) )
want := test . want
if want == nil {
want = test . src
}
TestEnsureSnapshot ( t , repo , snapshotID , want )
2024-04-14 11:42:26 +02:00
checker . TestCheckRepo ( t , repo , false )
2018-03-30 22:43:18 +02:00
} )
}
}
2018-05-12 23:08:00 +02:00
2020-12-28 20:45:53 +01:00
type noCancelBackend struct {
2023-10-01 11:40:12 +02:00
backend . Backend
2020-12-28 20:45:53 +01:00
}
2023-10-01 11:40:12 +02:00
func ( c * noCancelBackend ) Remove ( _ context . Context , h backend . Handle ) error {
2020-12-28 20:45:53 +01:00
return c . Backend . Remove ( context . Background ( ) , h )
}
2023-10-01 11:40:12 +02:00
func ( c * noCancelBackend ) Save ( _ context . Context , h backend . Handle , rd backend . RewindReader ) error {
2020-12-28 20:45:53 +01:00
return c . Backend . Save ( context . Background ( ) , h , rd )
}
2023-10-01 11:40:12 +02:00
func ( c * noCancelBackend ) Load ( _ context . Context , h backend . Handle , length int , offset int64 , fn func ( rd io . Reader ) error ) error {
2020-12-28 20:45:53 +01:00
return c . Backend . Load ( context . Background ( ) , h , length , offset , fn )
}
2023-10-01 11:40:12 +02:00
func ( c * noCancelBackend ) Stat ( _ context . Context , h backend . Handle ) ( backend . FileInfo , error ) {
2020-12-28 20:45:53 +01:00
return c . Backend . Stat ( context . Background ( ) , h )
}
2023-10-01 11:40:12 +02:00
func ( c * noCancelBackend ) List ( _ context . Context , t backend . FileType , fn func ( backend . FileInfo ) error ) error {
2020-12-28 20:45:53 +01:00
return c . Backend . List ( context . Background ( ) , t , fn )
}
2023-05-18 19:18:09 +02:00
func ( c * noCancelBackend ) Delete ( _ context . Context ) error {
2020-12-28 20:45:53 +01:00
return c . Backend . Delete ( context . Background ( ) )
}
func TestArchiverContextCanceled ( t * testing . T ) {
ctx , cancel := context . WithCancel ( context . Background ( ) )
cancel ( )
2024-03-29 00:24:03 +01:00
tempdir := rtest . TempDir ( t )
2020-12-28 20:45:53 +01:00
TestCreateFiles ( t , tempdir , TestDir {
"targetfile" : TestFile { Content : "foobar" } ,
} )
// Ensure that the archiver itself reports the canceled context and not just the backend
2024-05-10 16:59:09 +02:00
repo , _ := repository . TestRepositoryWithBackend ( t , & noCancelBackend { mem . New ( ) } , 0 , repository . Options { } )
2020-12-28 20:45:53 +01:00
2024-03-29 00:24:03 +01:00
back := rtest . Chdir ( t , tempdir )
2020-12-28 20:45:53 +01:00
defer back ( )
arch := New ( repo , fs . Track { FS : fs . Local { } } , Options { } )
2024-02-22 22:14:48 +01:00
_ , snapshotID , _ , err := arch . Snapshot ( ctx , [ ] string { "." } , SnapshotOptions { Time : time . Now ( ) } )
2020-12-28 20:45:53 +01:00
if err != nil {
t . Logf ( "found expected error (%v)" , err )
return
}
if snapshotID . IsNull ( ) {
t . Fatalf ( "no error returned but found null id" )
}
t . Fatalf ( "expected error not returned by archiver" )
}
2018-05-12 23:08:00 +02:00
// TrackFS keeps track which files are opened. For some files, an error is injected.
type TrackFS struct {
fs . FS
errorOn map [ string ] error
opened map [ string ] uint
m sync . Mutex
}
2024-11-02 20:27:38 +01:00
func ( m * TrackFS ) OpenFile ( name string , flag int , metadataOnly bool ) ( fs . File , error ) {
2018-05-12 23:08:00 +02:00
m . m . Lock ( )
m . opened [ name ] ++
m . m . Unlock ( )
2024-11-02 20:27:38 +01:00
return m . FS . OpenFile ( name , flag , metadataOnly )
2018-05-12 23:08:00 +02:00
}
type failSaveRepo struct {
2024-05-19 15:11:32 +02:00
archiverRepo
2018-05-12 23:08:00 +02:00
failAfter int32
cnt int32
err error
}
2022-05-01 14:26:57 +02:00
func ( f * failSaveRepo ) SaveBlob ( ctx context . Context , t restic . BlobType , buf [ ] byte , id restic . ID , storeDuplicate bool ) ( restic . ID , bool , int , error ) {
2018-05-12 23:08:00 +02:00
val := atomic . AddInt32 ( & f . cnt , 1 )
if val >= f . failAfter {
2022-11-05 13:42:17 +01:00
return restic . Hash ( buf ) , false , 0 , f . err
2018-05-12 23:08:00 +02:00
}
2024-05-19 15:11:32 +02:00
return f . archiverRepo . SaveBlob ( ctx , t , buf , id , storeDuplicate )
2018-05-12 23:08:00 +02:00
}
func TestArchiverAbortEarlyOnError ( t * testing . T ) {
var testErr = errors . New ( "test error" )
var tests = [ ] struct {
src TestDir
wantOpen map [ string ] uint
2020-02-14 23:16:13 +01:00
failAfter uint // error after so many blobs have been saved to the repo
2018-05-12 23:08:00 +02:00
err error
} {
{
src : TestDir {
"dir" : TestDir {
"bar" : TestFile { Content : "foobar" } ,
"baz" : TestFile { Content : "foobar" } ,
"foo" : TestFile { Content : "foobar" } ,
} ,
} ,
wantOpen : map [ string ] uint {
filepath . FromSlash ( "dir/bar" ) : 1 ,
filepath . FromSlash ( "dir/baz" ) : 1 ,
filepath . FromSlash ( "dir/foo" ) : 1 ,
} ,
} ,
{
src : TestDir {
"dir" : TestDir {
2024-03-29 00:24:03 +01:00
"file0" : TestFile { Content : string ( rtest . Random ( 0 , 1024 ) ) } ,
"file1" : TestFile { Content : string ( rtest . Random ( 1 , 1024 ) ) } ,
"file2" : TestFile { Content : string ( rtest . Random ( 2 , 1024 ) ) } ,
"file3" : TestFile { Content : string ( rtest . Random ( 3 , 1024 ) ) } ,
"file4" : TestFile { Content : string ( rtest . Random ( 4 , 1024 ) ) } ,
"file5" : TestFile { Content : string ( rtest . Random ( 5 , 1024 ) ) } ,
"file6" : TestFile { Content : string ( rtest . Random ( 6 , 1024 ) ) } ,
"file7" : TestFile { Content : string ( rtest . Random ( 7 , 1024 ) ) } ,
"file8" : TestFile { Content : string ( rtest . Random ( 8 , 1024 ) ) } ,
"file9" : TestFile { Content : string ( rtest . Random ( 9 , 1024 ) ) } ,
2018-05-12 23:08:00 +02:00
} ,
} ,
wantOpen : map [ string ] uint {
2022-12-02 20:07:34 +01:00
filepath . FromSlash ( "dir/file0" ) : 1 ,
2018-05-12 23:08:00 +02:00
filepath . FromSlash ( "dir/file1" ) : 1 ,
filepath . FromSlash ( "dir/file2" ) : 1 ,
filepath . FromSlash ( "dir/file3" ) : 1 ,
filepath . FromSlash ( "dir/file8" ) : 0 ,
filepath . FromSlash ( "dir/file9" ) : 0 ,
} ,
2022-12-02 20:07:34 +01:00
// fails after four to seven files were opened, as the ReadConcurrency allows for
// two queued files and SaveBlobConcurrency for one blob queued for saving.
2020-02-14 23:16:13 +01:00
failAfter : 4 ,
2018-05-12 23:08:00 +02:00
err : testErr ,
} ,
}
for _ , test := range tests {
t . Run ( "" , func ( t * testing . T ) {
ctx , cancel := context . WithCancel ( context . Background ( ) )
defer cancel ( )
2022-12-11 10:41:22 +01:00
tempdir , repo := prepareTempdirRepoSrc ( t , test . src )
2018-05-12 23:08:00 +02:00
2024-03-29 00:24:03 +01:00
back := rtest . Chdir ( t , tempdir )
2018-05-12 23:08:00 +02:00
defer back ( )
testFS := & TrackFS {
2018-09-05 08:04:55 -04:00
FS : fs . Track { FS : fs . Local { } } ,
2018-05-12 23:08:00 +02:00
opened : make ( map [ string ] uint ) ,
}
if testFS . errorOn == nil {
testFS . errorOn = make ( map [ string ] error )
}
testRepo := & failSaveRepo {
2024-05-19 15:11:32 +02:00
archiverRepo : repo ,
failAfter : int32 ( test . failAfter ) ,
err : test . err ,
2018-05-12 23:08:00 +02:00
}
2020-02-14 23:16:13 +01:00
// at most two files may be queued
arch := New ( testRepo , testFS , Options {
2022-12-02 20:07:34 +01:00
ReadConcurrency : 2 ,
SaveBlobConcurrency : 1 ,
2020-02-14 23:16:13 +01:00
} )
2018-05-12 23:08:00 +02:00
2024-02-22 22:14:48 +01:00
_ , _ , _ , err := arch . Snapshot ( ctx , [ ] string { "." } , SnapshotOptions { Time : time . Now ( ) } )
2022-06-13 20:35:37 +02:00
if ! errors . Is ( err , test . err ) {
t . Errorf ( "expected error (%v) not found, got %v" , test . err , err )
2018-05-12 23:08:00 +02:00
}
t . Logf ( "Snapshot return error: %v" , err )
t . Logf ( "track fs: %v" , testFS . opened )
for k , v := range test . wantOpen {
if testFS . opened [ k ] != v {
t . Errorf ( "opened %v %d times, want %d" , k , testFS . opened [ k ] , v )
}
}
} )
}
}
2019-04-23 22:23:35 +02:00
2025-09-23 20:01:09 +02:00
func snapshot ( t testing . TB , repo archiverRepo , fs fs . FS , parent * data . Snapshot , filename string ) ( * data . Snapshot , * data . Node ) {
2019-04-23 22:23:35 +02:00
ctx , cancel := context . WithCancel ( context . Background ( ) )
defer cancel ( )
arch := New ( repo , fs , Options { } )
sopts := SnapshotOptions {
Time : time . Now ( ) ,
ParentSnapshot : parent ,
}
2024-02-22 22:14:48 +01:00
snapshot , _ , _ , err := arch . Snapshot ( ctx , [ ] string { filename } , sopts )
2019-04-23 22:23:35 +02:00
if err != nil {
t . Fatal ( err )
}
2025-09-23 20:01:09 +02:00
tree , err := data . LoadTree ( ctx , repo , * snapshot . Tree )
2019-04-23 22:23:35 +02:00
if err != nil {
t . Fatal ( err )
}
node := tree . Find ( filename )
if node == nil {
t . Fatalf ( "unable to find node for testfile in snapshot" )
}
2022-10-03 14:48:14 +02:00
return snapshot , node
2019-04-23 22:23:35 +02:00
}
2024-11-02 20:27:38 +01:00
type overrideFS struct {
2019-04-23 22:23:35 +02:00
fs . FS
2024-11-03 16:01:59 +01:00
overrideFI * fs . ExtendedFileInfo
2024-11-15 21:21:04 +01:00
resetFIOnRead bool
2025-09-23 20:01:09 +02:00
overrideNode * data . Node
2024-11-15 21:21:04 +01:00
overrideErr error
2019-04-23 22:23:35 +02:00
}
2024-11-02 20:27:38 +01:00
func ( m * overrideFS ) OpenFile ( name string , flag int , metadataOnly bool ) ( fs . File , error ) {
f , err := m . FS . OpenFile ( name , flag , metadataOnly )
if err != nil {
return f , err
2019-04-23 22:23:35 +02:00
}
2024-11-15 21:21:04 +01:00
if filepath . Base ( name ) == "testfile" || filepath . Base ( name ) == "testdir" {
2024-11-02 20:27:38 +01:00
return & overrideFile { f , m } , nil
2019-04-23 22:23:35 +02:00
}
2024-11-02 20:27:38 +01:00
return f , nil
2019-04-23 22:23:35 +02:00
}
2024-11-02 20:27:38 +01:00
type overrideFile struct {
2019-04-23 22:23:35 +02:00
fs . File
2024-11-02 20:27:38 +01:00
ofs * overrideFS
}
2024-11-03 16:01:59 +01:00
func ( f overrideFile ) Stat ( ) ( * fs . ExtendedFileInfo , error ) {
2024-11-15 21:21:04 +01:00
if f . ofs . overrideFI == nil {
return f . File . Stat ( )
}
2024-11-02 20:27:38 +01:00
return f . ofs . overrideFI , nil
2024-11-15 21:21:04 +01:00
}
func ( f overrideFile ) MakeReadable ( ) error {
if f . ofs . resetFIOnRead {
f . ofs . overrideFI = nil
}
return f . File . MakeReadable ( )
2019-04-23 22:23:35 +02:00
}
2025-09-23 20:01:09 +02:00
func ( f overrideFile ) ToNode ( ignoreXattrListError bool , warnf func ( format string , args ... any ) ) ( * data . Node , error ) {
2024-11-02 20:27:38 +01:00
if f . ofs . overrideNode == nil {
2025-09-21 19:24:48 +02:00
return f . File . ToNode ( ignoreXattrListError , warnf )
2024-11-02 20:27:38 +01:00
}
return f . ofs . overrideNode , f . ofs . overrideErr
2019-04-23 22:23:35 +02:00
}
2019-05-05 14:57:38 +02:00
// used by wrapFileInfo, use untyped const in order to avoid having a version
// of wrapFileInfo for each OS
const (
mockFileInfoMode = 0400
mockFileInfoUID = 51234
mockFileInfoGID = 51235
)
2019-05-04 10:34:28 +02:00
func TestMetadataChanged ( t * testing . T ) {
2024-03-09 17:44:48 +01:00
defer feature . TestSetFlag ( t , feature . Flag , feature . DeviceIDForHardlinks , true ) ( )
2019-05-04 10:34:28 +02:00
files := TestDir {
"testfile" : TestFile {
Content : "foo bar test file" ,
} ,
}
2019-04-23 22:23:35 +02:00
2022-12-11 10:41:22 +01:00
tempdir , repo := prepareTempdirRepoSrc ( t , files )
2019-05-04 10:34:28 +02:00
2024-03-29 00:24:03 +01:00
back := rtest . Chdir ( t , tempdir )
2019-05-04 10:34:28 +02:00
defer back ( )
// get metadata
fi := lstat ( t , "testfile" )
2024-08-28 10:58:07 +02:00
localFS := & fs . Local { }
2024-11-02 20:27:38 +01:00
meta , err := localFS . OpenFile ( "testfile" , fs . O_NOFOLLOW , true )
rtest . OK ( t , err )
2025-09-21 19:24:48 +02:00
want , err := meta . ToNode ( false , t . Logf )
2024-11-02 20:27:38 +01:00
rtest . OK ( t , err )
rtest . OK ( t , meta . Close ( ) )
2019-05-04 10:34:28 +02:00
2024-11-02 20:27:38 +01:00
fs := & overrideFS {
FS : localFS ,
overrideFI : fi ,
2025-09-23 20:01:09 +02:00
overrideNode : & data . Node { } ,
2019-05-04 10:34:28 +02:00
}
2024-11-02 20:27:38 +01:00
* fs . overrideNode = * want
2019-05-04 10:34:28 +02:00
2022-10-03 14:48:14 +02:00
sn , node2 := snapshot ( t , repo , fs , nil , "testfile" )
2019-05-04 10:34:28 +02:00
// set some values so we can then compare the nodes
2024-03-09 17:38:41 +01:00
want . DeviceID = 0
2019-05-04 10:34:28 +02:00
want . Content = node2 . Content
want . Path = ""
2020-02-27 11:21:01 +01:00
if len ( want . ExtendedAttributes ) == 0 {
want . ExtendedAttributes = nil
}
2019-05-04 10:34:28 +02:00
2019-11-06 16:38:46 +08:00
want . AccessTime = want . ModTime
2019-05-04 10:34:28 +02:00
// make sure that metadata was recorded successfully
if ! cmp . Equal ( want , node2 ) {
t . Fatalf ( "metadata does not match:\n%v" , cmp . Diff ( want , node2 ) )
}
2024-11-30 16:58:04 +01:00
// modify the mode and UID/GID
modFI := * fi
modFI . Mode = mockFileInfoMode
if runtime . GOOS != "windows" {
modFI . UID = mockFileInfoUID
modFI . GID = mockFileInfoGID
}
fs . overrideFI = & modFI
2024-11-16 16:53:34 +01:00
rtest . Assert ( t , ! fileChanged ( fs . overrideFI , node2 , 0 ) , "testfile must not be considered as changed" )
2019-05-04 10:34:28 +02:00
// set the override values in the 'want' node which
2024-11-30 16:58:04 +01:00
want . Mode = mockFileInfoMode
2019-05-04 10:34:28 +02:00
// ignore UID and GID on Windows
if runtime . GOOS != "windows" {
2024-11-30 16:58:04 +01:00
want . UID = mockFileInfoUID
want . GID = mockFileInfoGID
2019-05-04 10:34:28 +02:00
}
2024-11-02 20:27:38 +01:00
// update mock node accordingly
2024-11-30 16:58:04 +01:00
fs . overrideNode . Mode = want . Mode
2024-11-02 20:27:38 +01:00
fs . overrideNode . UID = want . UID
fs . overrideNode . GID = want . GID
2019-05-04 10:34:28 +02:00
// make another snapshot
2022-10-03 14:48:14 +02:00
_ , node3 := snapshot ( t , repo , fs , sn , "testfile" )
2019-05-04 10:34:28 +02:00
// make sure that metadata was recorded successfully
if ! cmp . Equal ( want , node3 ) {
t . Fatalf ( "metadata does not match:\n%v" , cmp . Diff ( want , node3 ) )
}
// make sure the content matches
TestEnsureFileContent ( context . Background ( ) , t , repo , "testfile" , node3 , files [ "testfile" ] . ( TestFile ) )
2019-04-23 22:23:35 +02:00
2024-04-14 11:42:26 +02:00
checker . TestCheckRepo ( t , repo , false )
2019-04-23 22:23:35 +02:00
}
2020-02-07 22:14:50 +01:00
2024-11-15 21:21:04 +01:00
func TestRacyFileTypeSwap ( t * testing . T ) {
2020-02-07 22:14:50 +01:00
files := TestDir {
2024-11-02 20:27:38 +01:00
"testfile" : TestFile {
2020-02-07 22:14:50 +01:00
Content : "foo bar test file" ,
} ,
2024-11-15 21:21:04 +01:00
"testdir" : TestDir { } ,
2020-02-07 22:14:50 +01:00
}
2024-11-15 21:21:04 +01:00
for _ , dirError := range [ ] bool { false , true } {
desc := "file changed type"
if dirError {
desc = "dir changed type"
}
t . Run ( desc , func ( t * testing . T ) {
tempdir , repo := prepareTempdirRepoSrc ( t , files )
2020-02-07 22:14:50 +01:00
2024-11-15 21:21:04 +01:00
back := rtest . Chdir ( t , tempdir )
defer back ( )
2020-02-07 22:14:50 +01:00
2024-11-15 21:21:04 +01:00
// get metadata of current folder
var fakeName , realName string
if dirError {
// lstat claims this is a directory, but it's actually a file
fakeName = "testdir"
realName = "testfile"
} else {
fakeName = "testfile"
realName = "testdir"
}
fakeFI := lstat ( t , fakeName )
tempfile := filepath . Join ( tempdir , realName )
2020-02-07 22:14:50 +01:00
2024-11-15 21:21:04 +01:00
statfs := & overrideFS {
FS : fs . Local { } ,
overrideFI : fakeFI ,
resetFIOnRead : true ,
}
2020-02-07 22:14:50 +01:00
2024-11-15 21:21:04 +01:00
ctx , cancel := context . WithCancel ( context . Background ( ) )
defer cancel ( )
2020-02-07 22:14:50 +01:00
2024-11-15 21:21:04 +01:00
wg , ctx := errgroup . WithContext ( ctx )
repo . StartPackUploader ( ctx , wg )
2020-02-07 22:14:50 +01:00
2024-11-15 21:21:04 +01:00
arch := New ( repo , fs . Track { FS : statfs } , Options { } )
arch . Error = func ( item string , err error ) error {
t . Logf ( "archiver error as expected for %v: %v" , item , err )
return err
}
arch . runWorkers ( ctx , wg )
2020-02-07 22:14:50 +01:00
2024-11-15 21:21:04 +01:00
// fs.Track will panic if the file was not closed
_ , excluded , err := arch . save ( ctx , "/" , tempfile , nil )
rtest . Assert ( t , err != nil && strings . Contains ( err . Error ( ) , "changed type, refusing to archive" ) , "save() returned wrong error: %v" , err )
tpe := "file"
if dirError {
tpe = "directory"
}
rtest . Assert ( t , strings . Contains ( err . Error ( ) , tpe + " " ) , "unexpected item type in error: %v" , err )
rtest . Assert ( t , ! excluded , "Save() excluded the node, that's unexpected" )
} )
2020-02-07 22:14:50 +01:00
}
}
2024-08-03 19:10:11 +02:00
2024-11-02 20:27:38 +01:00
type mockToNoder struct {
2025-09-23 20:01:09 +02:00
node * data . Node
2024-11-02 20:27:38 +01:00
err error
}
2025-09-23 20:01:09 +02:00
func ( m * mockToNoder ) ToNode ( _ bool , _ func ( format string , args ... any ) ) ( * data . Node , error ) {
2024-11-02 20:27:38 +01:00
return m . node , m . err
}
2024-08-03 19:10:11 +02:00
func TestMetadataBackupErrorFiltering ( t * testing . T ) {
tempdir := t . TempDir ( )
filename := filepath . Join ( tempdir , "file" )
2024-11-02 20:27:38 +01:00
repo := repository . TestRepository ( t )
2024-08-03 19:10:11 +02:00
arch := New ( repo , fs . Local { } , Options { } )
var filteredErr error
replacementErr := fmt . Errorf ( "replacement" )
arch . Error = func ( item string , err error ) error {
filteredErr = err
return replacementErr
}
2024-11-02 20:27:38 +01:00
nonExistNoder := & mockToNoder {
2025-09-23 20:01:09 +02:00
node : & data . Node { Type : data . NodeTypeFile } ,
2024-11-02 20:27:38 +01:00
err : fmt . Errorf ( "not found" ) ,
}
2024-08-03 19:10:11 +02:00
// check that errors from reading extended metadata are properly filtered
2024-11-02 20:27:38 +01:00
node , err := arch . nodeFromFileInfo ( "file" , filename + "invalid" , nonExistNoder , false )
2024-08-03 19:10:11 +02:00
rtest . Assert ( t , node != nil , "node is missing" )
rtest . Assert ( t , err == replacementErr , "expected %v got %v" , replacementErr , err )
rtest . Assert ( t , filteredErr != nil , "missing inner error" )
2024-09-14 18:59:59 +02:00
// check that errors from reading irregular file are not filtered
filteredErr = nil
2024-11-02 20:27:38 +01:00
nonExistNoder = & mockToNoder {
2025-09-23 20:01:09 +02:00
node : & data . Node { Type : data . NodeTypeIrregular } ,
2024-11-02 20:27:38 +01:00
err : fmt . Errorf ( ` unsupported file type "irregular" ` ) ,
}
node , err = arch . nodeFromFileInfo ( "file" , filename , nonExistNoder , false )
2024-09-14 18:59:59 +02:00
rtest . Assert ( t , node != nil , "node is missing" )
rtest . Assert ( t , filteredErr == nil , "error for irregular node should not have been filtered" )
rtest . Assert ( t , strings . Contains ( err . Error ( ) , "irregular" ) , "unexpected error %q does not warn about irregular file mode" , err )
}
func TestIrregularFile ( t * testing . T ) {
files := TestDir {
"testfile" : TestFile {
Content : "foo bar test file" ,
} ,
}
tempdir , repo := prepareTempdirRepoSrc ( t , files )
back := rtest . Chdir ( t , tempdir )
defer back ( )
tempfile := filepath . Join ( tempdir , "testfile" )
fi := lstat ( t , "testfile" )
2024-11-30 16:58:04 +01:00
// patch mode to irregular
fi . Mode = ( fi . Mode &^ os . ModeType ) | os . ModeIrregular
2024-09-14 18:59:59 +02:00
2024-11-02 20:27:38 +01:00
override := & overrideFS {
FS : fs . Local { } ,
2024-11-30 16:58:04 +01:00
overrideFI : fi ,
2025-09-23 20:01:09 +02:00
overrideNode : & data . Node {
Type : data . NodeTypeIrregular ,
2024-09-14 18:59:59 +02:00
} ,
2024-11-02 20:27:38 +01:00
overrideErr : fmt . Errorf ( ` unsupported file type "irregular" ` ) ,
2024-09-14 18:59:59 +02:00
}
ctx , cancel := context . WithCancel ( context . Background ( ) )
defer cancel ( )
2024-11-02 20:27:38 +01:00
arch := New ( repo , fs . Track { FS : override } , Options { } )
2024-09-14 18:59:59 +02:00
_ , excluded , err := arch . save ( ctx , "/" , tempfile , nil )
if err == nil {
t . Fatalf ( "Save() should have failed" )
}
rtest . Assert ( t , strings . Contains ( err . Error ( ) , "irregular" ) , "unexpected error %q does not warn about irregular file mode" , err )
if excluded {
t . Errorf ( "Save() excluded the node, that's unexpected" )
}
2024-08-03 19:10:11 +02:00
}
2024-11-02 22:55:16 +01:00
type missingFS struct {
fs . FS
errorOnOpen bool
}
2025-02-28 19:52:43 +00:00
func ( fs * missingFS ) OpenFile ( _ string , _ int , _ bool ) ( fs . File , error ) {
2024-11-02 22:55:16 +01:00
if fs . errorOnOpen {
return nil , os . ErrNotExist
}
return & missingFile { } , nil
}
type missingFile struct {
fs . File
}
2024-11-03 16:01:59 +01:00
func ( f * missingFile ) Stat ( ) ( * fs . ExtendedFileInfo , error ) {
2024-11-02 22:55:16 +01:00
return nil , os . ErrNotExist
}
func ( f * missingFile ) Close ( ) error {
// prevent segfault in test
return nil
}
func TestDisappearedFile ( t * testing . T ) {
tempdir , repo := prepareTempdirRepoSrc ( t , TestDir { } )
back := rtest . Chdir ( t , tempdir )
defer back ( )
ctx , cancel := context . WithCancel ( context . Background ( ) )
defer cancel ( )
// depending on the underlying FS implementation a missing file may be detected by OpenFile or
// the subsequent file.Stat() call. Thus test both cases.
for _ , errorOnOpen := range [ ] bool { false , true } {
arch := New ( repo , fs . Track { FS : & missingFS { FS : & fs . Local { } , errorOnOpen : errorOnOpen } } , Options { } )
_ , excluded , err := arch . save ( ctx , "/" , filepath . Join ( tempdir , "testdir" ) , nil )
rtest . OK ( t , err )
rtest . Assert ( t , excluded , "testfile should have been excluded" )
}
}