Skip to content

Commit 5930a19

Browse files
committed
zstream: add comprehensive test suite
This PR defines a `zstream` test category and adds tests to exercise all features of the `zstream` command. It was originally intended to guarantee that PR #18509 did not disrupt any user-facing behavior, but during the development of that latter PR, some bugs were fixed and tests were added for them. As a result, some of the tests in this PR will fail when run against the current version of `zstream`. I would suggest not integrating this PR in advance of PR #18509, but if it's preferred to have the tests in first, I can add known-issue flags for the failing tests. Or alternatively, this PR can be merged into PR #18509. Signed-off-by: Garth Snyder <garth@garthsnyder.com>
1 parent 9ae9f2e commit 5930a19

66 files changed

Lines changed: 1744 additions & 23 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

cmd/zstream/scripts/add-xattrs.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
#!/tmp/zstream-venv/bin/python3
2+
"""Add random extended attributes to files until 600 bytes of xattrs are added."""
3+
4+
import argparse
5+
import os
6+
import random
7+
import sys
8+
from lorem_text import lorem
9+
10+
ADJECTIVES = [
11+
"boogie", "funky", "wobbly", "snazzy", "jazzy", "groovy", "zippy",
12+
"bouncy", "fluffy", "crunchy", "sparkly", "fuzzy", "spiffy", "dandy",
13+
"peppy", "snappy", "sassy", "zesty", "swanky", "nifty", "plucky",
14+
"quirky", "wacky", "goofy", "dizzy", "breezy", "cheery", "perky",
15+
"frisky", "chirpy", "feisty", "jolly", "lively", "merry", "spunky",
16+
"zippy", "vivid", "brisk", "sunny", "witty", "kinky",
17+
]
18+
19+
NOUNS = [
20+
"woogie", "monkey", "noodle", "pickle", "muffin", "waffle", "pebble",
21+
"wobble", "doodle", "tangle", "giggle", "wiggle", "jiggle", "sparkle",
22+
"crinkle", "twinkle", "frizzle", "drizzle", "sizzle", "fizzle",
23+
"puddle", "bubble", "muddle", "huddle", "cuddle", "juggle", "muggle",
24+
"snuggle", "tuggle", "buggle", "nugget", "widget", "gadget", "gibbet",
25+
"trinket", "bracket", "racket", "jacket", "ticket", "cricket", "thicket",
26+
"biscuit", "circuit", "summit", "muppet", "trumpet", "basket", "casket",
27+
]
28+
29+
TARGET_BYTES = 1024
30+
31+
32+
def random_attr_name(used: set) -> str:
33+
for _ in range(1000):
34+
name = f"user.{random.choice(ADJECTIVES)}-{random.choice(NOUNS)}"
35+
if name not in used:
36+
return name
37+
base = f"user.{random.choice(ADJECTIVES)}-{random.choice(NOUNS)}"
38+
i = 2
39+
while f"{base}-{i}" in used:
40+
i += 1
41+
return f"{base}-{i}"
42+
43+
44+
def random_value(length: int) -> bytes:
45+
# Pull words from lorem sentences and trim/pad to exact length
46+
text = ""
47+
while len(text) < length:
48+
text += lorem.sentence() + " "
49+
return text[:length].encode()
50+
51+
52+
def add_xattrs(path: str) -> int:
53+
"""Add xattrs to path until TARGET_BYTES total value bytes added. Returns bytes added."""
54+
used_names = set()
55+
total = 0
56+
while total < TARGET_BYTES:
57+
remaining = TARGET_BYTES - total
58+
length = min(random.randint(40, 200), remaining) if remaining < 40 else random.randint(40, min(200, remaining))
59+
# If remaining < 40, just do one final attr to hit the target
60+
if remaining < 40:
61+
length = remaining
62+
name = random_attr_name(used_names)
63+
used_names.add(name)
64+
value = random_value(length)
65+
os.setxattr(path, name, value)
66+
total += len(value)
67+
return total
68+
69+
70+
def main():
71+
parser = argparse.ArgumentParser(
72+
description=f"Add random xattrs to files until {TARGET_BYTES} bytes of xattr values are added."
73+
)
74+
parser.add_argument("files", nargs="+", help="Files to annotate with xattrs")
75+
args = parser.parse_args()
76+
77+
errors = 0
78+
for path in args.files:
79+
try:
80+
added = add_xattrs(path)
81+
print(f" {path} ({added:,} bytes in xattrs)")
82+
except OSError as e:
83+
print(f" {path} error: {e}", file=sys.stderr)
84+
errors += 1
85+
86+
if errors:
87+
sys.exit(1)
88+
89+
90+
if __name__ == "__main__":
91+
main()
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
#!/tmp/zstream-venv/bin/python3
2+
"""Generate files with random names filled with lorem ipsum paragraphs."""
3+
4+
import argparse
5+
import random
6+
import sys
7+
from pathlib import Path
8+
from lorem_text import lorem
9+
10+
ADJECTIVES = [
11+
"boogie", "funky", "wobbly", "snazzy", "jazzy", "groovy", "zippy",
12+
"bouncy", "fluffy", "crunchy", "sparkly", "fuzzy", "spiffy", "dandy",
13+
"peppy", "snappy", "sassy", "zesty", "swanky", "nifty", "plucky",
14+
"quirky", "wacky", "goofy", "dizzy", "breezy", "cheery", "perky",
15+
"frisky", "chirpy", "feisty", "jolly", "lively", "merry", "spunky",
16+
"frisky", "zippy", "vivid", "brisk", "sunny", "witty", "kinky",
17+
]
18+
19+
NOUNS = [
20+
"woogie", "monkey", "noodle", "pickle", "muffin", "waffle", "pebble",
21+
"wobble", "doodle", "tangle", "giggle", "wiggle", "jiggle", "sparkle",
22+
"crinkle", "twinkle", "frizzle", "drizzle", "sizzle", "fizzle",
23+
"puddle", "bubble", "muddle", "huddle", "cuddle", "juggle", "muggle",
24+
"snuggle", "tuggle", "buggle", "nugget", "widget", "gadget", "gibbet",
25+
"trinket", "bracket", "racket", "jacket", "ticket", "cricket", "thicket",
26+
"biscuit", "circuit", "summit", "muppet", "trumpet", "basket", "casket",
27+
]
28+
29+
def random_name(used: set) -> str:
30+
for _ in range(1000):
31+
name = f"{random.choice(ADJECTIVES)}-{random.choice(NOUNS)}"
32+
if name not in used:
33+
return name
34+
# Fallback: append a number
35+
base = f"{random.choice(ADJECTIVES)}-{random.choice(NOUNS)}"
36+
i = 2
37+
while f"{base}-{i}" in used:
38+
i += 1
39+
return f"{base}-{i}"
40+
41+
42+
def fill_file(path: Path, target_size: int, repeat=False) -> None:
43+
content_parts = []
44+
total = 0
45+
para = lorem.paragraph()
46+
while total < target_size:
47+
content_parts.append(para)
48+
total += len(para) + 1 # +1 for newline
49+
if not repeat:
50+
para = lorem.paragraph()
51+
path.write_text("\n\n".join(content_parts) + "\n")
52+
53+
54+
def main():
55+
parser = argparse.ArgumentParser(
56+
description="Generate files with random names and lorem ipsum content."
57+
)
58+
parser.add_argument("count", type=int, help="Number of files to create")
59+
parser.add_argument("-d", "--directory", default=".", help="Target directory (default: .)")
60+
parser.add_argument("-r", "--repeat", action="store_true", help="Fill files with reps of a single paragraph")
61+
parser.add_argument("--min-size", type=int, default=16384, help="Minimum file size in bytes (default: 2048)")
62+
parser.add_argument("--max-size", type=int, default=128000, help="Maximum file size in bytes (default: 128000)")
63+
args = parser.parse_args()
64+
65+
if args.min_size >= args.max_size:
66+
print(f"error: min-size ({args.min_size}) must be less than max-size ({args.max_size})", file=sys.stderr)
67+
sys.exit(1)
68+
69+
directory = Path(args.directory)
70+
directory.mkdir(parents=True, exist_ok=True)
71+
72+
used_names = set()
73+
for i in range(args.count):
74+
name = random_name(used_names)
75+
used_names.add(name)
76+
target_size = random.randint(args.min_size, args.max_size)
77+
path = directory / name
78+
fill_file(path, target_size, args.repeat)
79+
print(f" {path} ({path.stat().st_size:,} bytes)")
80+
81+
82+
if __name__ == "__main__":
83+
main()
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#!/bin/sh
2+
3+
if [ $# -ne 1 ]; then
4+
echo "Usage: $0 <device>" >&2
5+
exit 1
6+
fi
7+
8+
DEVICE="$1"
9+
SCRIPTDIR="$(cd "$(dirname "$0")" && pwd)"
10+
11+
zpool create -o ashift=12 test "$DEVICE"
12+
zfs set compression=on xattr=sa test
13+
zfs create test/source
14+
15+
"$SCRIPTDIR/gen-lorem-files.py" -r -d /test/source --min-size 2048 \
16+
--max-size 32000 3
17+
"$SCRIPTDIR/add-xattrs.py" /test/source/*
18+
echo "very small" > /test/source/small
19+
echo "password" > /test/source/to-be-redacted
20+
chmod 400 /test/source/to-be-redacted
21+
22+
zfs snapshot -r test/source@baseline
23+
zfs clone test/source@baseline test/redacted
24+
rm /test/redacted/to-be-redacted
25+
"$SCRIPTDIR/gen-lorem-files.py" -r -d /test/redacted --min-size 4096 \
26+
--max-size 32000 3
27+
"$SCRIPTDIR/add-xattrs.py" /test/redacted/*
28+
cd /test/redacted
29+
tar cf /tmp/dups.tar .
30+
mkdir copies
31+
cd copies
32+
tar xvf /tmp/dups.tar
33+
34+
echo "password" > /test/redacted/new-key
35+
zfs create -o encryption=on -o keylocation=file:///test/redacted/new-key \
36+
-o keyformat=passphrase test/redacted/encrypted
37+
"$SCRIPTDIR/gen-lorem-files.py" -r -d /test/redacted/encrypted 3
38+
echo "very small" > /test/redacted/encrypted/small-encrypted
39+
# "$SCRIPTDIR/add-xattrs.py" /test/redacted/encrypted/*
40+
41+
zfs snapshot -r test/redacted@clean
42+
43+
zfs redact test/source@baseline redaction-bookmark test/redacted@clean
44+
zfs send -ce --redact redaction-bookmark test/source@baseline > /tmp/all-record-types-base.zsend
45+
zfs send -Rcew -i test/source@baseline test/redacted@clean > /tmp/all-record-types-incr.zsend
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#!/bin/sh
2+
3+
if [ $# -ne 1 ]; then
4+
echo "Usage: $0 <device>" >&2
5+
exit 1
6+
fi
7+
8+
DEVICE="$1"
9+
SCRIPTDIR="$(cd "$(dirname "$0")" && pwd)"
10+
11+
zpool create -o ashift=12 test "$DEVICE"
12+
echo "password" > /test/password
13+
14+
zfs create -o compression=zstd-5 test/unencrypted
15+
"$SCRIPTDIR/gen-lorem-files.py" -r -d /test/unencrypted --min-size 12000 \
16+
--max-size 40000 2
17+
"$SCRIPTDIR/gen-lorem-files.py" -r -d /test/unencrypted --min-size 140000 \
18+
--max-size 160000 1
19+
20+
zfs create -o compression=lz4 -o encryption=on -o keylocation=file:///test/password -o keyformat=passphrase test/encrypted
21+
"$SCRIPTDIR/gen-lorem-files.py" -r -d /test/encrypted --min-size 12000 \
22+
--max-size 40000 3
23+
24+
zfs snapshot -r test@decompression
25+
zfs send -cw test/unencrypted@decompression > /tmp/decompression.zsend
26+
zfs send -cw test/encrypted@decompression > /tmp/decompression-crypt.zsend
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
#!/usr/bin/env python3
2+
"""Run old and new zstream dump -v on stream files, producing abbreviated dump outputs."""
3+
import argparse
4+
import subprocess
5+
import sys
6+
from pathlib import Path
7+
8+
def abbreviate(filename: str) -> str:
9+
"""Split filename at dashes, take the first letter of each segment, lowercased."""
10+
stem = Path(filename).stem
11+
# Strip common compression suffixes to get the logical stem
12+
for ext in (".zfs", ".gz", ".bz2", ".xz", ".zst", ".lz4"):
13+
if stem.endswith(ext):
14+
stem = stem[: -len(ext)]
15+
return "".join(seg[0] for seg in stem.split("-") if seg).lower()
16+
17+
def run_dump(zstream: Path, stream: Path, output: Path) -> bool:
18+
"""Run `zstream dump -v < stream > output`. Returns True on success."""
19+
try:
20+
with open(stream, "rb") as inf, open(output, "w") as outf:
21+
proc = subprocess.run(
22+
[str(zstream), "dump", "-v"],
23+
stdin=inf,
24+
stdout=outf,
25+
stderr=outf,
26+
)
27+
if proc.returncode != 0:
28+
print(
29+
f" WARNING: {zstream} exited {proc.returncode} for {stream.name}",
30+
file=sys.stderr,
31+
)
32+
if proc.stderr:
33+
print(f" stderr: {proc.stderr.decode(errors='replace').rstrip()}",
34+
file=sys.stderr)
35+
return True
36+
except Exception as e:
37+
print(f" ERROR: {e}", file=sys.stderr)
38+
return False
39+
40+
41+
def main():
42+
parser = argparse.ArgumentParser(
43+
description="Run old and new zstream dump -v on stream files."
44+
)
45+
parser.add_argument("old_zstream", type=Path, help="Path to old zstream binary")
46+
parser.add_argument("new_zstream", type=Path, help="Path to new zstream binary")
47+
parser.add_argument(
48+
"streams", nargs="+", type=Path, help="Compressed stream files to process"
49+
)
50+
args = parser.parse_args()
51+
52+
for zs in (args.old_zstream, args.new_zstream):
53+
if not zs.is_file():
54+
parser.error(f"zstream binary not found: {zs}")
55+
56+
for stream in args.streams:
57+
if not stream.is_file():
58+
print(f"Skipping missing file: {stream}", file=sys.stderr)
59+
continue
60+
61+
abbrev = abbreviate(stream.name)
62+
out_dir = stream.parent
63+
64+
old_out = out_dir / f"{abbrev}-old.dump"
65+
new_out = out_dir / f"{abbrev}-new.dump"
66+
67+
print(f"{stream.name} -> {abbrev}")
68+
69+
print(f" old: {old_out}")
70+
run_dump(args.old_zstream, stream, old_out)
71+
72+
print(f" new: {new_out}")
73+
run_dump(args.new_zstream, stream, new_out)
74+
75+
76+
if __name__ == "__main__":
77+
main()
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#!/bin/sh
2+
3+
if [ $# -ne 1 ]; then
4+
echo "Usage: $0 <device>" >&2
5+
exit 1
6+
fi
7+
8+
DEVICE="$1"
9+
SCRIPTDIR="$(cd "$(dirname "$0")" && pwd)"
10+
11+
zpool create -o ashift=12 test "$DEVICE"
12+
13+
zfs set compression=off recordsize=16MiB test
14+
15+
# We are testing 8MB blocks, so write one short file, 8.5MB
16+
# file, and one 24.5MB file.
17+
18+
"$SCRIPTDIR/gen-lorem-files.py" -d /test -r --min-size 20000 \
19+
--max-size 24000 1
20+
"$SCRIPTDIR/gen-lorem-files.py" -d /test -r --min-size 8500000 \
21+
--max-size 8510000 1
22+
"$SCRIPTDIR/gen-lorem-files.py" -d /test -r --min-size 24500000 \
23+
--max-size 24510000 1
24+
25+
zfs snapshot test@long-payloads
26+
zfs send -L test@long-payloads > /tmp/long-payloads.zsend

cmd/zstream/scripts/make-venv.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/bin/sh
2+
3+
python3 -m venv /tmp/zstream-venv
4+
. /tmp/zstream-venv/bin/activate
5+
pip install lorem_text

tests/runfiles/common.run

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1003,7 +1003,7 @@ tests = ['recv_dedup', 'recv_dedup_encrypted_zvol', 'rsend_001_pos',
10031003
'rsend_030_pos', 'rsend_031_pos', 'rsend-exclude_001_pos',
10041004
'rsend-exclude_002_pos', 'send-c_verify_ratio',
10051005
'send-c_verify_contents', 'send-c_props', 'send-c_incremental',
1006-
'send-c_volume', 'send-c_zstream_recompress', 'send-c_zstreamdump',
1006+
'send-c_volume',
10071007
'send-c_lz4_disabled', 'send-c_recv_lz4_disabled',
10081008
'send-c_mixed_compression', 'send-c_stream_size_estimate',
10091009
'send-c_embedded_blocks', 'send-c_resume', 'send-cpL_varied_recsize',
@@ -1012,7 +1012,7 @@ tests = ['recv_dedup', 'recv_dedup_encrypted_zvol', 'rsend_001_pos',
10121012
'send_encrypted_props', 'send_encrypted_truncated_files',
10131013
'send_freeobjects', 'send_realloc_files', 'send_realloc_encrypted_files',
10141014
'send_spill_block', 'send_holds', 'send_hole_birth', 'send_mixed_raw',
1015-
'send-wR_encrypted_zvol', 'send-zstream_drop_record',
1015+
'send-wR_encrypted_zvol',
10161016
'send_partial_dataset', 'send_invalid',
10171017
'send_large_blocks_incremental', 'send_large_blocks_initial',
10181018
'send_large_microzap_incremental', 'send_large_microzap_transitive',
@@ -1131,6 +1131,20 @@ tests = ['zoned_uid_001_pos', 'zoned_uid_002_pos', 'zoned_uid_003_pos',
11311131
'zoned_uid_029_neg', 'zoned_uid_031_pos']
11321132
tags = ['functional', 'zoned_uid']
11331133

1134+
[tests/functional/zstream]
1135+
tests = ['zstream_checksum_001_pos',
1136+
'zstream_decompress_001_pos', 'zstream_decompress_002_pos',
1137+
'zstream_decompress_003_neg', 'zstream_decompress_004_pos',
1138+
'zstream_decompress_005_pos', 'zstream_decompress_006_neg',
1139+
'zstream_drop_record_001_pos',
1140+
'zstream_dump_001_pos', 'zstream_dump_002_pos',
1141+
'zstream_dump_003_pos', 'zstream_dump_004_neg',
1142+
'zstream_recompress_001_pos', 'zstream_recompress_002_pos',
1143+
'zstream_recompress_003_pos', 'zstream_recompress_004_pos',
1144+
'zstream_recompress_005_pos',
1145+
'zstream_redup_001_pos']
1146+
tags = ['functional', 'zstream']
1147+
11341148
[tests/functional/zvol/zvol_ENOSPC]
11351149
tests = ['zvol_ENOSPC_001_pos']
11361150
tags = ['functional', 'zvol', 'zvol_ENOSPC']

0 commit comments

Comments
 (0)