test/others: Cover mixed compression chains

Successive layers in an incremental dump can use different compression
modes. Restore must interpret each layer through its own inventory and
pagemap metadata instead of applying the final image mode to the parent
chain.

Add chains with two pre-dumps and a final dump for plain-to-page,
page-to-plain, plain-to-region, and region-to-plain transitions. Toggle
a page between layers so every image carries real payload, check the
exact inventory version and compression settings, and restore the ZDTM
workload to verify its memory contents.

Run the test in CI when both page compression and memory dirty tracking
are available.

Assisted-by: Codex:GPT-5
Signed-off-by: Radostin Stoyanov <rstoyanov@fedoraproject.org>
This commit is contained in:
Radostin Stoyanov 2026-07-10 17:09:00 +01:00
parent ce933af777
commit f5fc0ddb56
5 changed files with 327 additions and 0 deletions

View file

@ -360,6 +360,13 @@ run_non_shardable_tests() {
echo "Skipping hugetlb compression tests"
fi
# Incremental compression parent chains.
if criu/criu check --feature compress && criu/criu check --feature mem_dirty_track; then
make -C test/others/compression/incremental run
else
echo "Skipping compression/incremental test"
fi
./test/zdtm.py run -t zdtm/transition/pid_reuse --pre 2 # start time based pid reuse detection
./test/zdtm.py run -t zdtm/transition/pidfd_store_sk --rpc --pre 2 # pidfd based pid reuse detection

View file

@ -0,0 +1 @@
dump-*/

View file

@ -0,0 +1,3 @@
run:
./run.sh
.PHONY: run

View file

@ -0,0 +1,126 @@
#!/usr/bin/env python3
import argparse
import os
import sys
import pycriu.images
PE_PRESENT = 4
EXPECTED_REGION_SIZE = 64 * 1024
def inventory_compression(directory, mode, version):
expected_mode = {"plain": 0, "page": 1, "region": 2}[mode]
expected_version = int(version)
with open(os.path.join(directory, "inventory.img"), "rb") as image:
inventory = pycriu.images.load(image)["entries"][0]
if inventory["img_version"] != expected_version:
print(
"FAIL: %s inventory version is %s, expected %s"
% (directory, inventory["img_version"], expected_version)
)
return 1
if int(inventory.get("compress", 0)) != expected_mode:
print(
"FAIL: %s inventory compression mode is %s, expected %s"
% (directory, inventory.get("compress"), expected_mode)
)
return 1
region_size = inventory.get("compress_region_size")
if expected_mode == 2:
if int(region_size or 0) != EXPECTED_REGION_SIZE:
print(
"FAIL: %s inventory region size is %s, expected 65536"
% (directory, region_size)
)
return 1
elif region_size is not None:
print(
"FAIL: %s has unexpected compression region size %s"
% (directory, region_size)
)
return 1
return 0
def present_payload(directory, pid):
path = os.path.join(directory, "pagemap-%s.img" % pid)
with open(path, "rb") as image:
entries = pycriu.images.load(image)["entries"][1:]
if not any(int(entry.get("flags", 0)) & PE_PRESENT for entry in entries):
print("FAIL: %s contains no present page payload" % path)
return 1
return 0
def toggle_page(directory, pid, address_file):
path = os.path.join(directory, "pagemap-%s.img" % pid)
if os.path.exists(address_file):
with open(address_file) as saved:
address = int(saved.read().strip(), 16)
else:
with open(path, "rb") as image:
entries = pycriu.images.load(image)["entries"][1:]
present = [
entry
for entry in entries
if int(entry.get("flags", 0)) & PE_PRESENT
]
if not present:
print("FAIL: no present mapping available to modify in %s" % path)
return 1
entry = max(present, key=lambda item: int(item.get("nr_pages", 0)))
address = int(entry["vaddr"])
with open(address_file, "w") as saved:
saved.write("%x\n" % address)
with open("/proc/%s/mem" % pid, "r+b", buffering=0) as memory:
value = os.pread(memory.fileno(), 1, address)
if len(value) != 1:
raise RuntimeError("short read from workload memory")
if os.pwrite(memory.fileno(), bytes([value[0] ^ 0xFF]), address) != 1:
raise RuntimeError("short write to workload memory")
return 0
def main():
parser = argparse.ArgumentParser(
description="Inspect incremental compression images and toggle test memory"
)
subparsers = parser.add_subparsers(dest="command")
inventory = subparsers.add_parser("inventory-compression")
inventory.add_argument("directory")
inventory.add_argument("mode", choices=("plain", "page", "region"))
inventory.add_argument("version", type=int)
payload = subparsers.add_parser("present-payload")
payload.add_argument("directory")
payload.add_argument("pid")
toggle = subparsers.add_parser("toggle-page")
toggle.add_argument("directory")
toggle.add_argument("pid")
toggle.add_argument("address_file")
args = parser.parse_args()
if args.command is None:
parser.error("a command is required")
if args.command == "inventory-compression":
return inventory_compression(args.directory, args.mode, args.version)
if args.command == "present-payload":
return present_payload(args.directory, args.pid)
return toggle_page(args.directory, args.pid, args.address_file)
if __name__ == "__main__":
sys.exit(main())

View file

@ -0,0 +1,190 @@
#!/bin/bash
# Test incremental checkpoint chains that change compression mode by layer.
#
# Exercise both ways of mixing compressed and uncompressed iterative
# checkpoint images. Readers must use the compression metadata from their
# own pagemap entries, not from the top image inventory.
set -euo pipefail
# shellcheck source=test/others/env.sh
source ../../env.sh
function fail {
echo "FAIL: $*"
exit 1
}
set -x
CRIU_CMD=("${CRIU}" --no-default-config)
ZDTM_DIR="../../../zdtm/static"
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
IMAGE_TOOL="${SCRIPT_DIR}/image_tool.py"
PID=""
function cleanup {
if [ -n "$PID" ] && kill -0 "$PID" 2>/dev/null; then
kill -TERM "$PID" 2>/dev/null || true
for _ in $(seq 1 50); do
kill -0 "$PID" 2>/dev/null || break
sleep 0.1
done
if kill -0 "$PID" 2>/dev/null; then
kill -KILL "$PID" 2>/dev/null || true
fi
fi
rm -rf dump-*
}
trap cleanup EXIT
function start_test {
(
cd "$ZDTM_DIR" || exit 1
make compress_pages00.cleanout
make compress_pages00
make compress_pages00.pid || exit 1
)
PID=$(cat "$ZDTM_DIR/compress_pages00.pid")
kill -0 "$PID" || fail "Test didn't start"
}
function stop_test {
(
cd "$ZDTM_DIR" || exit 1
make compress_pages00.stop
grep PASS compress_pages00.out || exit 1
) || fail "$1: memory content verification failed"
PID=""
}
function run_image_tool {
PYTHONPATH="${BASE_DIR}/lib${PYTHONPATH:+:${PYTHONPATH}}" \
python3 "$IMAGE_TOOL" "$@"
}
function assert_inventory_compression {
local directory="$1"
local expected_mode="$2"
local expected_version="$3"
run_image_tool inventory-compression \
"$directory" "$expected_mode" "$expected_version"
}
function assert_present_payload {
local directory="$1"
run_image_tool present-payload "$directory" "$PID"
}
function toggle_test_page {
local directory="$1"
local address_file="$2"
# Pick the largest present mapping from the preceding image. Toggling its
# first byte after each pre-dump makes the next layer contain real payload;
# the second toggle restores the byte expected by the ZDTM workload.
run_image_tool toggle-page "$directory" "$PID" "$address_file"
}
function validate_compression_mode {
local chain_name="$1"
local stage="$2"
local mode="$3"
case "$mode" in
plain | page | region) ;;
*) fail "$chain_name: unknown $stage compression mode $mode" ;;
esac
}
function create_checkpoint_layer {
local chain_name="$1"
local stage="$2"
local operation="$3"
local directory="$4"
local previous="$5"
local compression_mode="$6"
local expected_version="$7"
local args=("$operation" -D "$directory" -o dump.log -t "$PID" -v4)
args+=(--track-mem)
if [ "$operation" = "pre-dump" ]; then
args+=(-R)
fi
if [ -n "$previous" ]; then
args+=(--prev-images-dir="$previous")
fi
case "$compression_mode" in
page) args+=(--compress) ;;
region) args+=(--compress-region=64K) ;;
plain) ;;
esac
echo "=== $chain_name: $stage ==="
mkdir "$directory"
"${CRIU_CMD[@]}" "${args[@]}" || fail "$chain_name: $stage failed"
assert_present_payload "$directory" || \
fail "$chain_name: $stage contains no payload"
assert_inventory_compression \
"$directory" "$compression_mode" "$expected_version" || \
fail "$chain_name: $stage inventory compression state is invalid"
}
function run_chain {
local name="$1"
local predump_compress="$2"
local dump_compress="$3"
local imgdir="dump-$name"
local parent_version=2
local final_version
validate_compression_mode "$name" "pre-dump" "$predump_compress"
validate_compression_mode "$name" "final dump" "$dump_compress"
if [ "$predump_compress" != "plain" ]; then
parent_version=3
fi
final_version=$parent_version
if [ "$dump_compress" != "plain" ]; then
final_version=3
fi
rm -rf "$imgdir"
mkdir "$imgdir"
start_test
create_checkpoint_layer "$name" "pre-dump 1" pre-dump \
"$imgdir/1/" "" "$predump_compress" "$parent_version"
toggle_test_page "$imgdir/1/" "$imgdir/toggle-address" \
|| fail "$name: unable to modify workload"
create_checkpoint_layer "$name" "pre-dump 2" pre-dump \
"$imgdir/2/" "../1/" "$predump_compress" "$parent_version"
toggle_test_page "$imgdir/2/" "$imgdir/toggle-address" \
|| fail "$name: unable to restore workload byte"
create_checkpoint_layer "$name" "final dump" dump \
"$imgdir/3/" "../2/" "$dump_compress" "$final_version"
echo "=== $name: restore ==="
"${CRIU_CMD[@]}" restore -D "$imgdir/3/" -o restore.log -v4 -d \
|| fail "$name: restore failed"
stop_test "$name"
rm -rf "$imgdir"
}
run_chain "plain-parents-compressed-final" plain page
run_chain "compressed-parents-plain-final" page plain
run_chain "plain-parents-region-final" plain region
run_chain "region-parents-plain-final" region plain
echo "Test PASSED"