Skip to content

Commit cb2dfff

Browse files
authored
Add package to generate random filesystem hierarchies for testing (#13)
1 parent 507b315 commit cb2dfff

File tree

5 files changed

+440
-0
lines changed

5 files changed

+440
-0
lines changed

random/files/doc.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// Package files provides functionality for creating filesystem hierarchies of
2+
// random files an directories. This is useful for testing that needs to
3+
// operate on directory trees. Random results are reproducible by reusing the
4+
// same seed. Random values are not cryptographically secure.
5+
package files

random/files/files.go

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
package files
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"io"
7+
"math/rand"
8+
"os"
9+
"path/filepath"
10+
11+
"github.com/ipfs/go-test/random"
12+
)
13+
14+
const (
15+
fileNameSize = 16
16+
fileNameAlpha = "abcdefghijklmnopqrstuvwxyz01234567890-_"
17+
)
18+
19+
type Config struct {
20+
// Depth is the depth of the directory tree including the root directory.
21+
Depth int
22+
// Dirs is the number of subdirectories at each depth.
23+
Dirs int
24+
// Files is the number of files at each depth.
25+
Files int
26+
// FileSize sets the number of random bytes in each file.
27+
FileSize int64
28+
// Where to write display output, such as os.Stdout. Default is nil.
29+
Out io.Writer
30+
// RandomDirss specifies whether or not to randomize the number of
31+
// subdirectoriess from 1 to the value configured by Dirs.
32+
RandomDirs bool
33+
// RandomFiles specifies whether or not to randomize the number of files
34+
// from 1 to the value configured by Files.
35+
RandomFiles bool
36+
// RandomSize specifies whether or not to randomize the file size from 1 to
37+
// the value configured by FileSize.
38+
RandomSize bool
39+
// Seed sets the seen for the random number generator when set to a
40+
// non-zero value.
41+
Seed int64
42+
}
43+
44+
func DefaultConfig() Config {
45+
return Config{
46+
Depth: 2,
47+
Dirs: 5,
48+
Files: 10,
49+
FileSize: 4096,
50+
RandomDirs: false,
51+
RandomFiles: false,
52+
RandomSize: true,
53+
}
54+
}
55+
56+
func validateConfig(cfg *Config) error {
57+
if cfg.Depth < 1 || cfg.Depth > 64 {
58+
return errors.New("depth out of range, must be between 1 and 64")
59+
}
60+
if cfg.Dirs < 0 || cfg.Dirs > 64 {
61+
return errors.New("dirs out of range, must be between 0 and 64")
62+
}
63+
if cfg.Files < 0 || cfg.Files > 64 {
64+
return errors.New("files out of range, must be between 0 and 64")
65+
}
66+
if cfg.FileSize < 0 {
67+
return errors.New("file size out of range, must be 0 or greater")
68+
}
69+
if cfg.Depth > 1 && cfg.Dirs < 1 {
70+
return errors.New("dirs must be at least 1 for depth > 1")
71+
}
72+
73+
return nil
74+
}
75+
76+
func Create(cfg Config, paths ...string) error {
77+
if len(paths) == 0 {
78+
return errors.New("must provide at least 1 root directory path")
79+
}
80+
err := validateConfig(&cfg)
81+
if err != nil {
82+
return err
83+
}
84+
85+
var rnd *rand.Rand
86+
if cfg.Seed == 0 {
87+
rnd = random.NewRand()
88+
} else {
89+
rnd = random.NewSeededRand(cfg.Seed)
90+
}
91+
92+
for _, root := range paths {
93+
err := os.MkdirAll(root, 0755)
94+
if err != nil {
95+
return err
96+
}
97+
98+
err = writeTree(rnd, root, 1, &cfg)
99+
if err != nil {
100+
return err
101+
}
102+
103+
}
104+
105+
return nil
106+
}
107+
108+
func writeTree(rnd *rand.Rand, root string, depth int, cfg *Config) error {
109+
nFiles := cfg.Files
110+
if nFiles != 0 {
111+
if cfg.RandomFiles && nFiles > 1 {
112+
nFiles = rnd.Intn(nFiles) + 1
113+
}
114+
115+
for i := 0; i < nFiles; i++ {
116+
if err := writeFile(rnd, root, cfg); err != nil {
117+
return err
118+
}
119+
}
120+
}
121+
122+
return writeSubdirs(rnd, root, depth, cfg)
123+
}
124+
125+
func writeSubdirs(rnd *rand.Rand, root string, depth int, cfg *Config) error {
126+
if depth == cfg.Depth {
127+
return nil
128+
}
129+
depth++
130+
131+
nDirs := cfg.Dirs
132+
if cfg.RandomDirs && nDirs > 1 {
133+
nDirs = rnd.Intn(nDirs) + 1
134+
}
135+
136+
for i := 0; i < nDirs; i++ {
137+
if err := writeSubdir(rnd, root, depth, cfg); err != nil {
138+
return err
139+
}
140+
}
141+
142+
return nil
143+
}
144+
145+
func writeSubdir(rnd *rand.Rand, root string, depth int, cfg *Config) error {
146+
name := randomFilename(rnd)
147+
root = filepath.Join(root, name)
148+
if err := os.MkdirAll(root, 0755); err != nil {
149+
return err
150+
}
151+
152+
if cfg.Out != nil {
153+
fmt.Fprintln(cfg.Out, root+"/")
154+
}
155+
156+
return writeTree(rnd, root, depth, cfg)
157+
}
158+
159+
func randomFilename(rnd *rand.Rand) string {
160+
n := rnd.Intn(fileNameSize-4) + 4
161+
b := make([]byte, n)
162+
for i := 0; i < n; i++ {
163+
b[i] = fileNameAlpha[rnd.Intn(len(fileNameAlpha))]
164+
}
165+
return string(b)
166+
}
167+
168+
func writeFile(rnd *rand.Rand, root string, cfg *Config) error {
169+
name := randomFilename(rnd)
170+
filePath := filepath.Join(root, name)
171+
f, err := os.Create(filePath)
172+
if err != nil {
173+
return err
174+
}
175+
176+
if cfg.FileSize > 0 {
177+
fileSize := cfg.FileSize
178+
if cfg.RandomSize && fileSize > 1 {
179+
fileSize = rnd.Int63n(fileSize) + 1
180+
}
181+
182+
if _, err := io.CopyN(f, rnd, fileSize); err != nil {
183+
f.Close()
184+
return err
185+
}
186+
}
187+
188+
if cfg.Out != nil {
189+
fmt.Fprintln(cfg.Out, filePath)
190+
}
191+
192+
return f.Close()
193+
}

random/files/files_test.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package files_test
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
"os"
7+
"testing"
8+
9+
"github.com/ipfs/go-test/random/files"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
func TestRandomFiles(t *testing.T) {
14+
var b bytes.Buffer
15+
cfg := files.DefaultConfig()
16+
cfg.Depth = 2
17+
cfg.Dirs = 5
18+
cfg.Files = 3
19+
cfg.Out = &b
20+
21+
roots := []string{"foo"}
22+
err := files.Create(cfg, roots...)
23+
require.NoError(t, err)
24+
t.Cleanup(func() {
25+
for _, root := range roots {
26+
os.RemoveAll(root)
27+
}
28+
})
29+
30+
t.Logf("Created file hierarchy:\n%s", b.String())
31+
32+
var lines int
33+
scanner := bufio.NewScanner(&b)
34+
for scanner.Scan() {
35+
lines++
36+
}
37+
require.NoError(t, scanner.Err())
38+
39+
subdirs := 0
40+
if cfg.Depth > 1 {
41+
dirsAtDepth := cfg.Dirs
42+
subdirs += dirsAtDepth
43+
for i := 0; i < cfg.Depth-2; i++ {
44+
dirsAtDepth *= cfg.Dirs
45+
subdirs += dirsAtDepth
46+
}
47+
}
48+
linesPerSubdir := cfg.Files + 1
49+
expect := ((subdirs * linesPerSubdir) + cfg.Files) * len(roots)
50+
require.Equal(t, expect, lines)
51+
}
52+
53+
func TestRandomFilesValidation(t *testing.T) {
54+
cfg := files.DefaultConfig()
55+
err := files.Create(cfg)
56+
require.Error(t, err)
57+
58+
cfg.Depth = 0
59+
require.Error(t, files.Create(cfg, "foo"))
60+
cfg.Depth = 65
61+
require.Error(t, files.Create(cfg, "foo"))
62+
63+
cfg = files.DefaultConfig()
64+
65+
cfg.Dirs = -1
66+
require.Error(t, files.Create(cfg, "foo"))
67+
cfg.Dirs = 65
68+
require.Error(t, files.Create(cfg, "foo"))
69+
70+
cfg = files.DefaultConfig()
71+
72+
cfg.Files = -1
73+
require.Error(t, files.Create(cfg, "foo"))
74+
cfg.Files = 65
75+
require.Error(t, files.Create(cfg, "foo"))
76+
77+
cfg = files.DefaultConfig()
78+
79+
cfg.FileSize = -1
80+
require.Error(t, files.Create(cfg, "foo"))
81+
82+
cfg = files.DefaultConfig()
83+
84+
cfg.Depth = 2
85+
cfg.Dirs = 0
86+
require.Error(t, files.Create(cfg, "foo"))
87+
}

random/files/random-files/README.md

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# random-files - create random filesystem hierarchies
2+
3+
random-files creates random filesystem hierarchies for testing
4+
5+
## Install
6+
7+
```
8+
go install github.com/ipfs/go-test/random/files/random-files
9+
```
10+
11+
## Usage
12+
13+
```sh
14+
> random-files -help
15+
usage: ./random-files [options] <path>...
16+
Write a random filesystem hierarchy to each <path>
17+
18+
Options:
19+
-depth int
20+
depth of the directory tree including the root directory (default 2)
21+
-dirs int
22+
number of subdirectories at each depth (default 5)
23+
-files int
24+
number of files at each depth (default 10)
25+
-filesize int
26+
file fize, or the max file size id RandomSize is true (default 4096)
27+
-q do not print files and directories
28+
-random-dirs
29+
randomize number of subdirectories, from 1 to -Dirs
30+
-random-files
31+
randomize number of files, from 1 to -Files
32+
-random-size
33+
randomize file size, from 1 to -FileSize (default true)
34+
-seed int
35+
random seed, 0 for current time
36+
```
37+
38+
## Examples
39+
40+
```sh
41+
> random-files -depth=2 -files=3 -seed=1701 foo
42+
foo/rwd67uvnj9yz-
43+
foo/7vovyvr9
44+
foo/fjv0w0
45+
foo/gyubi50rec5/
46+
foo/gyubi50rec5/vr6x-ce4uupj
47+
foo/gyubi50rec5/ob9ud0e8lt_2e
48+
foo/gyubi50rec5/11gip6zea
49+
foo/nzu5j29-sh-ku4/
50+
foo/nzu5j29-sh-ku4/vcs1629n
51+
foo/nzu5j29-sh-ku4/rky_i_qsxrp
52+
foo/nzu5j29-sh-ku4/xr1usy5ic0
53+
foo/w30dzrx2w4_d/
54+
foo/w30dzrx2w4_d/7ued6
55+
foo/w30dzrx2w4_d/r1d3j
56+
foo/w30dzrx2w4_d/av7d09i-av
57+
foo/s6ha-58/
58+
foo/s6ha-58/nukjsxg7t
59+
foo/s6ha-58/7of_84
60+
foo/s6ha-58/h0jgq8mu1n7u
61+
foo/tq_8/
62+
foo/tq_8/sx-a2jgmz_mk6
63+
foo/tq_8/9hzrksz8
64+
foo/tq_8/8b5swu
65+
```
66+
67+
It made:
68+
69+
```sh
70+
> tree foo
71+
foo
72+
├── 7vovyvr9
73+
├── fjv0w0
74+
├── gyubi50rec5
75+
│   ├── 11gip6zea
76+
│   ├── ob9ud0e8lt_2e
77+
│   └── vr6x-ce4uupj
78+
├── nzu5j29-sh-ku4
79+
│   ├── rky_i_qsxrp
80+
│   ├── vcs1629n
81+
│   └── xr1usy5ic0
82+
├── rwd67uvnj9yz-
83+
├── s6ha-58
84+
│   ├── 7of_84
85+
│   ├── h0jgq8mu1n7u
86+
│   └── nukjsxg7t
87+
├── tq_8
88+
│   ├── 8b5swu
89+
│   ├── 9hzrksz8
90+
│   └── sx-a2jgmz_mk6
91+
└── w30dzrx2w4_d
92+
├── 7ued6
93+
├── av7d09i-av
94+
└── r1d3j
95+
96+
6 directories, 18 files
97+
```
98+
99+
Note: Specifying the same seed will produce the same results.
100+
101+
102+
### Acknowledgments
103+
104+
Credit to Juan Benet as the author of [`go-random-files`](https://github.com/jbenet/go-random-files) from which this code was derived.

0 commit comments

Comments
 (0)