mirror of
https://github.com/kopia/kopia.git
synced 2026-05-11 00:04:46 -04:00
Upgrades go to 1.22 and switches to new-style for loops --------- Co-authored-by: Julio López <1953782+julio-lopez@users.noreply.github.com>
285 lines
7.7 KiB
Go
285 lines
7.7 KiB
Go
// Package testdirtree provides utilities for creating test directory trees for testing.
|
|
package testdirtree
|
|
|
|
import (
|
|
cryptorand "crypto/rand"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"math/rand"
|
|
"os"
|
|
"path/filepath"
|
|
"sync/atomic"
|
|
"testing"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
|
|
"github.com/pkg/errors"
|
|
"golang.org/x/text/unicode/norm"
|
|
|
|
"github.com/kopia/kopia/internal/clock"
|
|
"github.com/kopia/kopia/internal/iocopy"
|
|
"github.com/kopia/kopia/internal/testutil"
|
|
)
|
|
|
|
var globalRandomNameCounter = new(int32)
|
|
|
|
func intOrDefault(a, b int) int {
|
|
if a > 0 {
|
|
return a
|
|
}
|
|
|
|
return b
|
|
}
|
|
|
|
func generateHexString(l int) string {
|
|
// original hex filename generator
|
|
b := make([]byte, (l+1)/2)
|
|
|
|
cryptorand.Read(b)
|
|
|
|
return hex.EncodeToString(b)[:l]
|
|
}
|
|
|
|
func generateUnicodeString(rangeMin, rangeMax, l int) string {
|
|
// generate a random unicode string within a defined range
|
|
s := ""
|
|
|
|
for i := 0; i < l; {
|
|
c := rand.Intn(rangeMax-rangeMin+1) + rangeMin
|
|
r := rune(c)
|
|
// IsLetter & IsDigit function as a sanity check to prevent writing punctuation/control characters
|
|
// ValidRune is a sanity check for macOS since APFS can't handle invalid utf-8 and will error out
|
|
if (unicode.IsLetter(r) || unicode.IsDigit(r)) && utf8.ValidRune(r) {
|
|
s += string(r)
|
|
i++
|
|
}
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
func randomUnicodeName(l int) string {
|
|
// random language selection for unicode
|
|
s := ""
|
|
n := rand.Intn(4)
|
|
|
|
switch n {
|
|
case 1:
|
|
// cyrillic runs 0x0400 (1024) to 0x052F (1327)
|
|
s += generateUnicodeString(1024, 1327, l)
|
|
case 2:
|
|
// arabic characters run 0x0600 (1536) to 0x06FF (1791)
|
|
s += generateUnicodeString(1536, 1791, l)
|
|
case 3:
|
|
// cjk characters run 0x4E00 (19968) to 0x9FFF (40959)
|
|
// however the end of the range has compatibility issues due to infrequent usage, so we trim the range a bit (36864)
|
|
s += generateUnicodeString(19968, 36864, l)
|
|
default:
|
|
// latin characters (including extensions A & B) run 0x0020 (32) to 0x024F (591)
|
|
s += generateUnicodeString(32, 591, l)
|
|
}
|
|
|
|
return norm.NFKD.String(s)
|
|
}
|
|
|
|
func randomName(opt DirectoryTreeOptions) string {
|
|
maxNameLength := intOrDefault(opt.MaxNameLength, 15)
|
|
minNameLength := intOrDefault(opt.MinNameLength, 3)
|
|
|
|
s := ""
|
|
l := rand.Intn(maxNameLength-minNameLength+1) + minNameLength
|
|
|
|
// check if we should skip unicode filename testing
|
|
// skipped during race detection, on ARM, and by default to keep logs cleaner
|
|
if testutil.ShouldSkipUnicodeFilenames() {
|
|
s += generateHexString(l)
|
|
} else {
|
|
s += randomUnicodeName(l)
|
|
}
|
|
|
|
return fmt.Sprintf("%v.%v", s, atomic.AddInt32(globalRandomNameCounter, 1))
|
|
}
|
|
|
|
// DirectoryTreeOptions lists options for CreateDirectoryTree.
|
|
type DirectoryTreeOptions struct {
|
|
Depth int
|
|
MaxSubdirsPerDirectory int
|
|
MaxFilesPerDirectory int
|
|
MaxSymlinksPerDirectory int
|
|
MaxFileSize int
|
|
MinFileSize int
|
|
MinNameLength int
|
|
MaxNameLength int
|
|
NonExistingSymlinkTargetPercentage int // 0..100
|
|
}
|
|
|
|
// MaybeSimplifyFilesystem applies caps to the provided DirectoryTreeOptions to reduce
|
|
// test time on ARM.
|
|
func MaybeSimplifyFilesystem(o DirectoryTreeOptions) DirectoryTreeOptions {
|
|
if !testutil.ShouldReduceTestComplexity() {
|
|
return o
|
|
}
|
|
|
|
if o.Depth > 2 {
|
|
o.Depth = 2
|
|
}
|
|
|
|
if o.MaxFilesPerDirectory > 5 {
|
|
o.MaxFilesPerDirectory = 5
|
|
}
|
|
|
|
if o.MaxSubdirsPerDirectory > 3 {
|
|
o.MaxFilesPerDirectory = 3
|
|
}
|
|
|
|
if o.MaxSymlinksPerDirectory > 3 {
|
|
o.MaxSymlinksPerDirectory = 3
|
|
}
|
|
|
|
if o.MaxFileSize > 100000 {
|
|
o.MaxFileSize = 100000
|
|
}
|
|
|
|
return o
|
|
}
|
|
|
|
// DirectoryTreeCounters stores stats about files and directories created by CreateDirectoryTree.
|
|
type DirectoryTreeCounters struct {
|
|
Files int
|
|
Directories int
|
|
Symlinks int
|
|
TotalFileSize int64
|
|
MaxFileSize int64
|
|
}
|
|
|
|
// MustCreateDirectoryTree creates a directory tree of a given depth with random files.
|
|
func MustCreateDirectoryTree(t *testing.T, dirname string, options DirectoryTreeOptions) {
|
|
t.Helper()
|
|
|
|
var counters DirectoryTreeCounters
|
|
if err := createDirectoryTreeInternal(dirname, options, &counters); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
t.Logf("created directory tree %#v", counters)
|
|
}
|
|
|
|
// CreateDirectoryTree creates a directory tree of a given depth with random files.
|
|
func CreateDirectoryTree(dirname string, options DirectoryTreeOptions, counters *DirectoryTreeCounters) error {
|
|
if counters == nil {
|
|
counters = &DirectoryTreeCounters{}
|
|
}
|
|
|
|
return createDirectoryTreeInternal(dirname, options, counters)
|
|
}
|
|
|
|
// MustCreateRandomFile creates a new file at the provided path with randomized contents.
|
|
// It will fail with a test error if the creation does not succeed.
|
|
func MustCreateRandomFile(t *testing.T, filePath string, options DirectoryTreeOptions, counters *DirectoryTreeCounters) {
|
|
t.Helper()
|
|
|
|
if err := CreateRandomFile(filePath, options, counters); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// CreateRandomFile creates a new file at the provided path with randomized contents.
|
|
func CreateRandomFile(filePath string, options DirectoryTreeOptions, counters *DirectoryTreeCounters) error {
|
|
if counters == nil {
|
|
counters = &DirectoryTreeCounters{}
|
|
}
|
|
|
|
return createRandomFile(filePath, options, counters)
|
|
}
|
|
|
|
// createDirectoryTreeInternal creates a directory tree of a given depth with random files.
|
|
func createDirectoryTreeInternal(dirname string, options DirectoryTreeOptions, counters *DirectoryTreeCounters) error {
|
|
if err := os.MkdirAll(dirname, 0o700); err != nil {
|
|
return errors.Wrapf(err, "unable to create directory %v", dirname)
|
|
}
|
|
|
|
counters.Directories++
|
|
|
|
if options.Depth > 0 && options.MaxSubdirsPerDirectory > 0 {
|
|
childOptions := options
|
|
childOptions.Depth--
|
|
|
|
numSubDirs := rand.Intn(options.MaxSubdirsPerDirectory) + 1
|
|
for range numSubDirs {
|
|
subdirName := randomName(options)
|
|
|
|
if err := createDirectoryTreeInternal(filepath.Join(dirname, subdirName), childOptions, counters); err != nil {
|
|
return errors.Wrap(err, "unable to create subdirectory")
|
|
}
|
|
}
|
|
}
|
|
|
|
var fileNames []string
|
|
|
|
if options.MaxFilesPerDirectory > 0 {
|
|
numFiles := rand.Intn(options.MaxFilesPerDirectory) + 1
|
|
for range numFiles {
|
|
fileName := randomName(options)
|
|
|
|
if err := createRandomFile(filepath.Join(dirname, fileName), options, counters); err != nil {
|
|
return errors.Wrap(err, "unable to create random file")
|
|
}
|
|
|
|
fileNames = append(fileNames, fileName)
|
|
}
|
|
}
|
|
|
|
if options.MaxSymlinksPerDirectory > 0 {
|
|
numSymlinks := rand.Intn(options.MaxSymlinksPerDirectory) + 1
|
|
for range numSymlinks {
|
|
fileName := randomName(options)
|
|
|
|
if err := createRandomSymlink(filepath.Join(dirname, fileName), fileNames, options, counters); err != nil {
|
|
return errors.Wrap(err, "unable to create random symlink")
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func createRandomFile(filename string, options DirectoryTreeOptions, counters *DirectoryTreeCounters) error {
|
|
f, err := os.Create(filename)
|
|
if err != nil {
|
|
return errors.Wrap(err, "unable to create random file")
|
|
}
|
|
defer f.Close()
|
|
|
|
maxFileSize := int64(intOrDefault(options.MaxFileSize, 100000))
|
|
|
|
length := rand.Int63n(maxFileSize)
|
|
|
|
if mfs := int64(options.MinFileSize); length < mfs {
|
|
length = mfs
|
|
}
|
|
|
|
if err := iocopy.JustCopy(f, io.LimitReader(rand.New(rand.NewSource(clock.Now().UnixNano())), length)); err != nil {
|
|
return errors.Wrap(err, "file create error")
|
|
}
|
|
|
|
counters.Files++
|
|
counters.TotalFileSize += length
|
|
|
|
if length > counters.MaxFileSize {
|
|
counters.MaxFileSize = length
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func createRandomSymlink(filename string, existingFiles []string, options DirectoryTreeOptions, counters *DirectoryTreeCounters) error {
|
|
counters.Symlinks++
|
|
|
|
if len(existingFiles) == 0 || rand.Intn(100) < options.NonExistingSymlinkTargetPercentage {
|
|
return os.Symlink(randomName(options), filename)
|
|
}
|
|
|
|
return os.Symlink(existingFiles[rand.Intn(len(existingFiles))], filename)
|
|
}
|