blob: e8f1419cab93e97d2606b3a3d60c698b1efefb8b [file]
#!/usr/bin/env python3
"""
# numberio_overlap.py
#
# Verification tests for sequential and random write overlap with numberio.
#
# When io_size > size, writes wrap around and revisit every offset a second
# time. fio_offset_overlap_risk() must return true so that io_hist uses an
# rb-tree and retains only the latest io_piece per offset.
#
# Each TC is run twice: once with the async engine and once with the sync
# engine selected based on the platform (Linux: io_uring/psync,
# Windows: windowsaio/sync, other: posixaio/psync).
#
# rw=write offline TC 1: basic 2× overlap
# online TC 2: do_verify=1
#
# rw=randwrite offline TC 3: norandommap
# online TC 4: norandommap
#
# rw=rw offline TC 5
# online TC 6
#
# rw=randrw offline TC 7: norandommap
# online TC 8: norandommap
#
# filesize<size offline TC 9: rb-tree triggered before first I/O
#
# nrfiles=2 offline TC 10: write, 2× overlap across 2 files
# online TC 11: write, do_verify=1 across 2 files
# offline TC 12: randwrite, norandommap
# offline TC 13: write, filesize < size/nrfiles
#
# USAGE
# see python3 t/numberio_overlap.py --help
#
# EXAMPLES
# python3 t/numberio_overlap.py --file /tmp/fio-overlap-test.dat
# python3 t/numberio_overlap.py --file /tmp/fio-overlap-test.dat -f ./fio
#
# REQUIREMENTS
# Python 3.6
#
"""
import copy
import os
import platform
import sys
import json
import time
import locale
import logging
import argparse
import subprocess
from pathlib import Path
from fiotestlib import FioJobCmdTest, run_fio_tests
from fiotestcommon import SUCCESS_DEFAULT, Requirements
class OfflineOverlapVerifyTest(FioJobCmdTest):
"""
Two-phase offline overlap verification test.
Phase 1 — write:
rw=write, io_size = N * size (N >= 2).
Every offset is written N times; the N-th pass has the highest numberio.
fio_offset_overlap_risk() must return true so that the rb-tree backend
is used and only the latest io_piece per offset is kept.
Phase 2 — verify:
verify_only=1, verify_state_load=1.
A dry-run write pass detects overlaps ("iolog: overlap") and advances
f->last_pos to wrap back to 0. The subsequent READ phase issues exactly
(size / bs) reads — one per block — checking the latest numberio only.
Key assertions
- Write phase exits 0 and produces io_size bytes written.
- Verify phase exits 0 and reports 0 verify_errors.
- Verify phase reads exactly size bytes — not io_size bytes — confirming
that only the latest numberio is verified once per block.
"""
def __init__(self, fio_path, success, testnum, artifact_root, fio_opts,
basename=None):
super().__init__(fio_path, success, testnum, artifact_root, fio_opts,
basename)
stub = os.path.join(self.paths['test_dir'],
f"{self.basename}{self.testnum:03d}")
# write-phase output file (verify phase reuses self.filenames['output'])
self.filenames['output_write'] = os.path.abspath(f"{stub}.write.output")
def setup(self, parameters):
"""Create test directory; args are built dynamically in run()."""
if not os.path.exists(self.paths['test_dir']):
os.mkdir(self.paths['test_dir'])
self.parameters = [] # satisfy FioExeTest.run() if it ever gets called
def _filename_str(self):
"""Return the fio --filename value: colon-joined list for nrfiles > 1."""
base = self.fio_opts['filename']
nrfiles = self.fio_opts.get('nrfiles', 1)
if nrfiles > 1:
return ':'.join(f"{base}.{i}" for i in range(nrfiles))
return base
def _fio_args(self, phase):
"""Return fio CLI args for *phase* ('write' or 'verify')."""
ioengine = self.fio_opts.get('ioengine', 'io_uring')
fallocate = "--fallocate=truncate"
common = [
"--name=overlap-verify",
f"--ioengine={ioengine}",
fallocate,
]
rw = self.fio_opts.get('rw', 'write')
common += [
f"--filename={self._filename_str()}",
f"--bs={self.fio_opts['bs']}",
f"--size={self.fio_opts['size']}",
f"--io_size={self.fio_opts['io_size']}",
f"--verify={self.fio_opts['verify']}",
"--output-format=json",
]
if self.fio_opts.get('norandommap'):
common.append("--norandommap=1")
if phase == 'write':
return common + [
f"--rw={rw}",
"--do_verify=0",
f"--output={self.filenames['output_write']}",
]
else:
# Verify phase replays the same rw mode as the write phase so that
# do_dry_run produces an identical offset→numberio mapping.
# - randwrite/randrw with norandommap=1: no axmap limit so
# do_dry_run runs to io_size. Both phases use the same RNG
# sequence because randrepeat=1 is fio's default (options.c
# ".def = 1"), giving identical offset→numberio in io_hist.
# - rw/randrw (sequential/random mixed): IOs advance position the
# same way in write and verify phases; only writes go to io_hist.
verify_io_size = self.fio_opts.get('verify_io_size',
self.fio_opts['io_size'])
verify_args = [
"--name=overlap-verify",
f"--ioengine={ioengine}",
fallocate,
f"--filename={self._filename_str()}",
f"--rw={rw}",
f"--bs={self.fio_opts['bs']}",
f"--size={self.fio_opts['size']}",
f"--io_size={verify_io_size}",
f"--verify={self.fio_opts['verify']}",
"--output-format=json",
"--do_verify=1",
"--verify_only=1",
"--verify_write_sequence=1",
f"--output={self.filenames['output']}",
]
if self.fio_opts.get('norandommap'):
verify_args.append("--norandommap=1")
return verify_args
def _run_one(self, args, stdout_f, stderr_f, ec_f):
"""Spawn fio with *args* in test_dir; return the Popen object."""
command = [self.paths['exe']] + args
with open(self.filenames['cmd'], "a",
encoding=locale.getpreferredencoding()) as cmd_f:
cmd_f.write(" \\\n ".join(command) + "\n\n")
proc = subprocess.Popen(command,
stdout=stdout_f,
stderr=stderr_f,
cwd=self.paths['test_dir'],
universal_newlines=True)
proc.communicate(timeout=self.success['timeout'])
ec_f.write(f"{proc.returncode}\n")
logging.debug("Test %d phase %s: exit %d",
self.testnum, args[-1], proc.returncode)
return proc
def run(self):
"""Run write phase then verify phase sequentially."""
# If filesize is specified, pre-create each file at that size so that
# real_file_size < size/nrfiles, which is the scenario under test.
if 'filesize' in self.fio_opts:
base = self.fio_opts['filename']
nrfiles = self.fio_opts.get('nrfiles', 1)
fnames = [f"{base}.{i}" for i in range(nrfiles)] if nrfiles > 1 \
else [base]
for fname in fnames:
with open(fname, 'wb'):
pass
os.truncate(fname, self.fio_opts['filesize'])
try:
with open(self.filenames['stdout'], "w",
encoding=locale.getpreferredencoding()) as stdout_f, \
open(self.filenames['stderr'], "w",
encoding=locale.getpreferredencoding()) as stderr_f, \
open(self.filenames['exitcode'], "w",
encoding=locale.getpreferredencoding()) as ec_f:
proc_w = self._run_one(self._fio_args('write'),
stdout_f, stderr_f, ec_f)
if proc_w.returncode != 0:
self.output['proc'] = proc_w
self.output['failure'] = \
f"write phase exited with {proc_w.returncode}"
return
proc_v = self._run_one(self._fio_args('verify'),
stdout_f, stderr_f, ec_f)
self.output['proc'] = proc_v
except subprocess.TimeoutExpired:
self.output['failure'] = 'timeout'
except Exception:
self.output['failure'] = 'exception'
self.output['exc_info'] = sys.exc_info()
@staticmethod
def _load_json(path):
"""Return parsed JSON from a fio output file, or None on error."""
try:
with open(path, "r", encoding=locale.getpreferredencoding()) as f:
raw = f.read()
lines = raw.splitlines()
last = len(lines) - lines[::-1].index("}")
return json.loads("\n".join(lines[lines.index("{"):last]))
except Exception as exc:
logging.debug("JSON parse error in %s: %s", path, exc)
return None
def check_result(self):
"""Check both phases produced the expected byte counts."""
# basic exit-code / stderr checks via FioExeTest
if 'proc' not in self.output:
self.failure_reason = self.output.get('failure', 'did not run')
self.passed = False
return
if 'failure' in self.output:
self.failure_reason = self.output['failure']
self.passed = False
# still fall through to check JSON if proc is set
if self.output['proc'].returncode != 0:
self.failure_reason += (
f" verify phase exited with {self.output['proc'].returncode}")
self.passed = False
# --- write phase: must have issued io_size bytes total ---
# For write-only modes (write, randwrite): all bytes are writes.
# For mixed modes (rw, randrw): writes + reads together equal io_size.
jw = self._load_json(self.filenames['output_write'])
if not jw:
self.failure_reason += " cannot parse write-phase JSON"
self.passed = False
else:
job_w = jw['jobs'][0]
written = job_w['write']['io_bytes']
rw = self.fio_opts.get('rw', 'write')
if rw in ('rw', 'readwrite', 'randrw'):
total_issued = written + job_w['read']['io_bytes']
if total_issued != self.fio_opts['io_size']:
self.failure_reason += (
f" write phase: issued {total_issued} bytes "
f"(write={written} read={job_w['read']['io_bytes']}), "
f"expected {self.fio_opts['io_size']} total")
self.passed = False
logging.debug("write job: %s",
json.dumps(job_w, indent=2))
else:
if written != self.fio_opts['io_size']:
self.failure_reason += (
f" write phase: wrote {written} bytes, "
f"expected {self.fio_opts['io_size']}")
self.passed = False
logging.debug("write job: %s",
json.dumps(job_w, indent=2))
# --- verify phase: no errors, reads exactly size bytes ---
jv = self._load_json(self.filenames['output'])
if not jv:
self.failure_reason += " cannot parse verify-phase JSON"
self.passed = False
else:
job = jv['jobs'][0]
read_data = job.get('read', {})
verify_errors = read_data.get('verify_errors', 0)
if verify_errors != 0:
self.failure_reason += (
f" verify phase: {verify_errors} verify error(s)")
self.passed = False
# For sequential write-only modes (write) every block is written
# exactly once per pass, so verify reads exactly size bytes.
# Skip the assertion for:
# - mixed modes (rw, randrw): only written blocks are in io_hist
# - norandommap: random sampling may leave some blocks unvisited
rw_mode = self.fio_opts.get('rw', 'write')
if rw_mode not in ('rw', 'readwrite', 'randrw') \
and not self.fio_opts.get('norandommap'):
expected_read = self.fio_opts['size']
read_bytes = read_data.get('io_bytes', 0)
if read_bytes != expected_read:
self.failure_reason += (
f" verify phase: read {read_bytes} bytes, "
f"expected {expected_read} "
f"(one pass — latest numberio only)")
self.passed = False
logging.debug("verify job: %s", json.dumps(job, indent=2))
class OnlineOverlapVerifyTest(FioJobCmdTest):
"""
Single-phase online overlap verification test (--do_verify=1).
fio writes and verifies in the same job. With io_size > size the write
phase makes multiple passes over the file (or revisits offsets with
norandommap); the verify (read) phase runs after all writes, checking
only the latest numberio per block.
This exercises:
- fio_offset_overlap_risk() enabling the rb-tree io_hist when
io_size > size, for all rw modes (write, randwrite, rw, randrw).
- backend.c: total_bytes for sequential write-only jobs is now bounded
by io_size (not size), so the second write pass actually happens.
- io_u.c: last_pos wrap-around is now triggered by do_verify=1 (not
only verify_only=1), so the verify READ phase starts from offset 0.
Key assertions
rw=write / rw=randwrite (TC 9-10, 17-18):
write == io_size (all overlap passes completed)
read == size (one verify pass — latest numberio only)
verify_errors == 0
rw=rw / rw=randrw (TC 19-22):
verify_errors == 0 only — write and read byte counts are skipped
because mixed I/O means write.io_bytes < io_size (50 % writes)
and read.io_bytes includes both the rw= read IOs and the verify
reads, making an exact assertion impractical.
"""
def setup(self, parameters):
"""Create test directory; args are built dynamically in run()."""
if not os.path.exists(self.paths['test_dir']):
os.mkdir(self.paths['test_dir'])
self.parameters = []
def _filename_str(self):
"""Return the fio --filename value: colon-joined list for nrfiles > 1."""
base = self.fio_opts['filename']
nrfiles = self.fio_opts.get('nrfiles', 1)
if nrfiles > 1:
return ':'.join(f"{base}.{i}" for i in range(nrfiles))
return base
def _fio_args(self):
ioengine = self.fio_opts.get('ioengine', 'io_uring')
rw = self.fio_opts.get('rw', 'write')
args = [
"--name=online-overlap-verify",
f"--ioengine={ioengine}",
"--fallocate=truncate",
f"--filename={self._filename_str()}",
f"--rw={rw}",
f"--bs={self.fio_opts['bs']}",
f"--size={self.fio_opts['size']}",
f"--io_size={self.fio_opts['io_size']}",
f"--verify={self.fio_opts['verify']}",
"--do_verify=1",
"--verify_write_sequence=1",
"--output-format=json",
f"--output={self.filenames['output']}",
]
if self.fio_opts.get('norandommap'):
args.append("--norandommap=1")
return args
def _run_one(self, args, stdout_f, stderr_f, ec_f):
command = [self.paths['exe']] + args
with open(self.filenames['cmd'], "w",
encoding=locale.getpreferredencoding()) as cmd_f:
cmd_f.write(" \\\n ".join(command) + "\n")
proc = subprocess.Popen(command,
stdout=stdout_f,
stderr=stderr_f,
cwd=self.paths['test_dir'],
universal_newlines=True)
proc.communicate(timeout=self.success['timeout'])
ec_f.write(f"{proc.returncode}\n")
logging.debug("Test %d online: exit %d", self.testnum, proc.returncode)
return proc
def run(self):
try:
with open(self.filenames['stdout'], "w",
encoding=locale.getpreferredencoding()) as stdout_f, \
open(self.filenames['stderr'], "w",
encoding=locale.getpreferredencoding()) as stderr_f, \
open(self.filenames['exitcode'], "w",
encoding=locale.getpreferredencoding()) as ec_f:
proc = self._run_one(self._fio_args(), stdout_f, stderr_f, ec_f)
self.output['proc'] = proc
except subprocess.TimeoutExpired:
self.output['failure'] = 'timeout'
except Exception:
self.output['failure'] = 'exception'
self.output['exc_info'] = sys.exc_info()
@staticmethod
def _load_json(path):
try:
with open(path, "r", encoding=locale.getpreferredencoding()) as f:
raw = f.read()
lines = raw.splitlines()
last = len(lines) - lines[::-1].index("}")
return json.loads("\n".join(lines[lines.index("{"):last]))
except Exception as exc:
logging.debug("JSON parse error in %s: %s", path, exc)
return None
def check_result(self):
if 'proc' not in self.output:
self.failure_reason = self.output.get('failure', 'did not run')
self.passed = False
return
if 'failure' in self.output:
self.failure_reason = self.output['failure']
self.passed = False
if self.output['proc'].returncode != 0:
self.failure_reason += (
f" job exited with {self.output['proc'].returncode}")
self.passed = False
jv = self._load_json(self.filenames['output'])
if not jv:
self.failure_reason += " cannot parse JSON"
self.passed = False
return
job = jv['jobs'][0]
verify_errors = job.get('read', {}).get('verify_errors', 0)
if verify_errors != 0:
self.failure_reason += f" {verify_errors} verify error(s)"
self.passed = False
rw_mode = self.fio_opts.get('rw', 'write')
is_mixed = rw_mode in ('rw', 'readwrite', 'randrw')
has_norandommap = self.fio_opts.get('norandommap', False)
# For mixed modes (rw, randrw): write.io_bytes < io_size (50 % writes)
# and read.io_bytes merges write-phase reads with verify reads.
# For write-only modes: assert written == io_size.
if not is_mixed:
written = job.get('write', {}).get('io_bytes', 0)
if written != self.fio_opts['io_size']:
self.failure_reason += (
f" wrote {written} bytes, expected {self.fio_opts['io_size']}"
f" (both overlap passes must complete)")
self.passed = False
logging.debug("job: %s", json.dumps(job, indent=2))
# For norandommap: random sampling may not cover all blocks, so
# read_bytes may be less than size. Skip the assertion in that case.
if not is_mixed and not has_norandommap:
read_bytes = job.get('read', {}).get('io_bytes', 0)
if read_bytes != self.fio_opts['size']:
self.failure_reason += (
f" verify read {read_bytes} bytes, expected {self.fio_opts['size']}"
f" (one pass — latest numberio only)")
self.passed = False
logging.debug("job: %s", json.dumps(job, indent=2))
TEST_LIST = [
# ══════════════════════════════════════════════════════════════════
# rw=write (TC 1-2)
# ══════════════════════════════════════════════════════════════════
# write, offline (TC 1): basic 2× overlap (io_size = 2 × size)
# 8 blocks written twice; verify reads each once.
{
"test_id": 1,
"fio_opts": {
"filename": None, # filled in from --file at runtime
"bs": "128k",
"size": 1 * 1024 * 1024, # 1 MiB
"io_size": 2 * 1024 * 1024, # 2 MiB → 2× overlap
"verify": "crc32c",
},
"test_class": OfflineOverlapVerifyTest,
"success": SUCCESS_DEFAULT,
},
# write, online (TC 2): do_verify=1, 2× overlap.
# Exercises the backend.c fix (total_bytes bounded by io_size for
# sequential write-only jobs) and the io_u.c fix (last_pos wrap
# triggered by do_verify=1, not only verify_only=1).
{
"test_id": 2,
"fio_opts": {
"filename": None,
"bs": "128k",
"size": 1 * 1024 * 1024,
"io_size": 2 * 1024 * 1024,
"verify": "crc32c",
},
"test_class": OnlineOverlapVerifyTest,
"success": SUCCESS_DEFAULT,
},
# ══════════════════════════════════════════════════════════════════
# rw=randwrite (TC 3-4)
# norandommap=1 allows the RNG to revisit the same offsets (genuine
# write overlap) and avoids the axmap-full problem in do_dry_run.
# Both offline and online phases use fio's default randrepeat=1, so
# write and verify dry-run see the same random offset sequence.
# ══════════════════════════════════════════════════════════════════
# randwrite, offline (TC 3): norandommap, offline verify.
{
"test_id": 3,
"fio_opts": {
"filename": None,
"rw": "randwrite",
"bs": "128k",
"size": 1 * 1024 * 1024,
"io_size": 2 * 1024 * 1024,
"norandommap": True,
"verify": "crc32c",
},
"test_class": OfflineOverlapVerifyTest,
"success": SUCCESS_DEFAULT,
},
# randwrite, online (TC 4): norandommap, online verify.
# total_bytes=io_size so the full 2M is written in one do_io() call;
# random offsets may be revisited (genuine overlap).
# verify_errors==0 and written==io_size are asserted; read_bytes is
# skipped because random sampling may not cover every block.
{
"test_id": 4,
"fio_opts": {
"filename": None,
"rw": "randwrite",
"bs": "128k",
"size": 1 * 1024 * 1024,
"io_size": 2 * 1024 * 1024,
"norandommap": True,
"verify": "crc32c",
},
"test_class": OnlineOverlapVerifyTest,
"success": SUCCESS_DEFAULT,
},
# ══════════════════════════════════════════════════════════════════
# rw=rw (sequential mixed read/write) (TC 5-6)
# io_size=4×size; with 50/50 rwmix the write fraction gives overlap.
# total_bytes=size per outer iteration → 4 iters → write overlap.
# Only verify_errors==0 is asserted; byte-count assertions skipped
# because write.io_bytes < io_size and read.io_bytes merges phases.
# ══════════════════════════════════════════════════════════════════
# rw=rw, offline (TC 5): both phases use rw=rw so the dry-run produces
# the same offset→numberio mapping (writes at even-numbered offsets only).
{
"test_id": 5,
"fio_opts": {
"filename": None,
"rw": "rw",
"bs": "128k",
"size": 1 * 1024 * 1024,
"io_size": 4 * 1024 * 1024,
"verify": "crc32c",
},
"test_class": OfflineOverlapVerifyTest,
"success": SUCCESS_DEFAULT,
},
# rw=rw, online (TC 6).
{
"test_id": 6,
"fio_opts": {
"filename": None,
"rw": "rw",
"bs": "128k",
"size": 1 * 1024 * 1024,
"io_size": 4 * 1024 * 1024,
"verify": "crc32c",
},
"test_class": OnlineOverlapVerifyTest,
"success": SUCCESS_DEFAULT,
},
# ══════════════════════════════════════════════════════════════════
# rw=randrw (random mixed read/write) (TC 7-8)
# norandommap=1 allows offsets to be revisited (genuine overlap) and
# avoids the axmap-full problem in do_dry_run (offline) / do_io (online).
# fio's default randrepeat=1 ensures both phases replay the same mixed
# I/O sequence so the dry-run io_hist matches the write phase.
# ══════════════════════════════════════════════════════════════════
# randrw, offline (TC 7): norandommap, offline verify.
{
"test_id": 7,
"fio_opts": {
"filename": None,
"rw": "randrw",
"bs": "128k",
"size": 1 * 1024 * 1024,
"io_size": 4 * 1024 * 1024,
"norandommap": True,
"verify": "crc32c",
},
"test_class": OfflineOverlapVerifyTest,
"success": SUCCESS_DEFAULT,
},
# randrw, online (TC 8): norandommap, online verify.
# Only verify_errors==0 is asserted.
{
"test_id": 8,
"fio_opts": {
"filename": None,
"rw": "randrw",
"bs": "128k",
"size": 1 * 1024 * 1024,
"io_size": 4 * 1024 * 1024,
"norandommap": True,
"verify": "crc32c",
},
"test_class": OnlineOverlapVerifyTest,
"success": SUCCESS_DEFAULT,
},
# ══════════════════════════════════════════════════════════════════
# filesize < size (TC 9)
# Real file size smaller than size=: fio_offset_overlap_risk() returns
# true (io_size > real_file_size) so the rb-tree is used even before
# the first write. Covers the case where the rb-tree is initialised
# at setup time rather than at the first overlap.
# ══════════════════════════════════════════════════════════════════
# write, offline, filesize < size (TC 9):
# filesize=512k < size=1M, io_size=2M → 2× overlap over 1M.
{
"test_id": 9,
"fio_opts": {
"filename": None,
"bs": "128k",
"filesize": 512 * 1024, # initial file size (< size)
"size": 1 * 1024 * 1024, # 1 MiB > filesize
"io_size": 2 * 1024 * 1024, # 2 MiB → io_size > size → rb-tree
"verify": "crc32c",
},
"test_class": OfflineOverlapVerifyTest,
"success": SUCCESS_DEFAULT,
},
# ══════════════════════════════════════════════════════════════════
# Multiple files (nrfiles=2) (TC 10-13)
#
# With nrfiles=2 fio distributes I/O across two files. Each file
# receives size/2 bytes of address space and io_size/2 of I/O,
# giving 2× overlap per file. fio_offset_overlap_risk() fires at
# the job level (io_size > size), so the rb-tree is used for the
# shared io_hist. nr_done_files is reset when io_size > size so
# that fio continues I/O after visiting every file once.
#
# Assertions are identical to the single-file cases:
# write.io_bytes == io_size (total across both files)
# read.io_bytes == size (one verify pass per file, total)
# ══════════════════════════════════════════════════════════════════
# nrfiles=2, write, offline (TC 10): basic 2× overlap across 2 files.
{
"test_id": 10,
"fio_opts": {
"filename": None,
"nrfiles": 2,
"bs": "128k",
"size": 1 * 1024 * 1024, # 1 MiB total (512k per file)
"io_size": 2 * 1024 * 1024, # 2 MiB total → 2× overlap per file
"verify": "crc32c",
},
"test_class": OfflineOverlapVerifyTest,
"success": SUCCESS_DEFAULT,
},
# nrfiles=2, write, online (TC 11): do_verify=1 across 2 files.
{
"test_id": 11,
"fio_opts": {
"filename": None,
"nrfiles": 2,
"bs": "128k",
"size": 1 * 1024 * 1024,
"io_size": 2 * 1024 * 1024,
"verify": "crc32c",
},
"test_class": OnlineOverlapVerifyTest,
"success": SUCCESS_DEFAULT,
},
# nrfiles=2, randwrite, norandommap, offline (TC 12).
# Random writes distribute across both files via file_service_type;
# norandommap=1 allows offsets within each file to be revisited.
# read.io_bytes assertion is skipped (norandommap may not cover all blocks).
{
"test_id": 12,
"fio_opts": {
"filename": None,
"nrfiles": 2,
"rw": "randwrite",
"bs": "128k",
"size": 1 * 1024 * 1024,
"io_size": 2 * 1024 * 1024,
"norandommap": True,
"verify": "crc32c",
},
"test_class": OfflineOverlapVerifyTest,
"success": SUCCESS_DEFAULT,
},
# nrfiles=2, write, offline, filesize < size/nrfiles (TC 13).
# Each file is pre-created at 256k (< size/2 = 512k), so
# real_file_size < size/nrfiles at setup → rb-tree initialised before
# the first I/O (exercises the early-init path alongside TC 9).
{
"test_id": 13,
"fio_opts": {
"filename": None,
"nrfiles": 2,
"bs": "128k",
"filesize": 256 * 1024, # pre-created size per file (< size/2)
"size": 1 * 1024 * 1024, # 1 MiB total → 512k per file > filesize
"io_size": 2 * 1024 * 1024, # 2 MiB total → 2× overlap per file
"verify": "crc32c",
},
"test_class": OfflineOverlapVerifyTest,
"success": SUCCESS_DEFAULT,
},
]
def parse_args():
parser = argparse.ArgumentParser(
description="Offline overlap verify test for fio numberio")
parser.add_argument('-f', '--fio',
help='path to fio executable (default: ./fio)')
parser.add_argument('-a', '--artifact-root',
help='root directory for test artifacts')
parser.add_argument('-d', '--debug', action='store_true',
help='enable debug messages')
parser.add_argument('-s', '--skip', nargs='+', type=int,
help='test IDs to skip')
parser.add_argument('-o', '--run-only', nargs='+', type=int,
help='run only these test IDs')
parser.add_argument('-k', '--skip-req', action='store_true',
help='skip requirements checking')
parser.add_argument('--file',
default='fio-numberio-overlap-test.dat',
help='file path for all TCs '
'(default: fio-numberio-overlap-test.dat in cwd). '
'WARNING: data will be overwritten',
dest='file')
parser.add_argument('--ioengines',
default=None,
help='comma-separated list of ioengines to test '
'(default: platform-specific async,sync pair, '
'e.g. --ioengines psync,libaio)')
return parser.parse_args()
def main():
args = parse_args()
logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO)
artifact_root = args.artifact_root or \
f"numberio-overlap-test-{time.strftime('%Y%m%d-%H%M%S')}"
os.mkdir(artifact_root)
print(f"Artifact directory: {artifact_root}")
fio_path = str(Path(args.fio).absolute()) if args.fio else \
os.path.join(os.path.dirname(__file__), '../fio')
print(f"fio path: {fio_path}")
fio_root = str(Path(__file__).absolute().parent.parent)
if not args.skip_req:
Requirements(fio_root, args)
for test in TEST_LIST:
test['fio_opts']['filename'] = args.file
if args.ioengines:
engines = [e.strip() for e in args.ioengines.split(',')]
elif platform.system() == 'Linux':
engines = ['io_uring', 'psync']
elif platform.system() == 'Windows':
engines = ['windowsaio', 'sync']
else:
engines = ['posixaio', 'psync']
total_failed = 0
for engine in engines:
engine_tests = copy.deepcopy(TEST_LIST)
for test in engine_tests:
test['fio_opts']['ioengine'] = engine
engine_artifact = os.path.join(artifact_root, engine)
os.mkdir(engine_artifact)
test_env = {
'fio_path': fio_path,
'fio_root': fio_root,
'artifact_root': engine_artifact,
'basename': 'numberio-overlap',
}
print(f"\nRunning with ioengine={engine}")
_, failed, _ = run_fio_tests(engine_tests, test_env, args)
total_failed += failed
sys.exit(total_failed)
if __name__ == '__main__':
main()