Two layers:
- Unit tests —
../src/tests.c, built and run bymake test. They exercise pure functions (get_extent, block length, the scan's seen-inode set, …). - Integration tests —
integration/, run bymake integration. Python stdlibunittest(no third-party dependencies). They drive the builtoansbinary against a scratch directory tree and assert on the resulting hashfile (a SQLite database, read with the stdlibsqlite3module) and on actual on-disk extent sharing (read with theFIEMAPioctl). This covers behavior the unit tests can't reach: the scan/dedupe pipeline, incremental rescans, rename and hardlink handling, excludes, sparse files, and end-to-end data integrity — including the two bugs that motivated the suite (the batched-writer hardlink corruption and theFIDEDUPERANGEEINVAL rejection), both of which only reproduce end to end.
Run both with make check.
make integration # build oans and run the suite
make integration TEST_JOBS=1 # ... sequentially (see -j below)
python3 tests/run.py # run directly (binary must be built)
python3 tests/run.py hardlink dedupe # only tests whose id matches a pattern
python3 tests/run.py -j 8 # 8 workers ('auto' by default)
python3 -m unittest discover -s tests/integration -v # plain unittest, no bannerThe suite runs across worker processes by default, so tests must keep to the
per-test scratch setUp hands them. -j 1 runs them one at a time, which is
where to start if a test only fails in company.
Environment:
DUPEREMOVE=/path/to/oans— test a specific binary (defaults to the one in the repo root). Handy for A/B-ing against another build.DUPEREMOVE_TEST_DIR=/mnt/scratch— where scratch trees are created. Dedupe tests need this to be on a reflink-capable filesystem (btrfs or xfs); they are skipped otherwise. Defaults to.itest-scratch/next to the repo.
Exit status is non-zero if any test fails, so it drops straight into CI.
tests/
run.py runner: banner + substring filtering over unittest
integration/
harness.py DuperemoveTest base class + helpers
test_*.py the tests, grouped by theme
Subclass DuperemoveTest (from harness) and add test_* methods. Each test
gets a fresh scratch directory in self.work and a per-test hashfile in
self.hf; both are cleaned up automatically.
That isolation is what lets the suite run in parallel, so keep to it — a test
reaching outside its own self.work will flake. If a test asserts on the
physical extent layout (the fsync-forced extent boundary trick, or fiemap
extent counts), set serial = True on the class: no amount of file isolation
helps there, because concurrent I/O changes how the kernel lays extents out.
from harness import DuperemoveTest, requires_reflink
@requires_reflink # skip the whole class if the fs can't dedupe
class ExampleTest(DuperemoveTest):
def test_two_copies_get_shared(self):
a, b = self.mkdup("a", "b", 1 << 20) # identical, independent copies
self.assertNotShared(a, b) # not sharing yet
self.dedupe(self.path(".")) # scan + dedupe into self.hf
self.assertDmOk() # no error signatures in output
self.assertShared(a, b) # kernel reflinked themKey helpers on DuperemoveTest (see harness.py for the full set):
| Helper | Purpose |
|---|---|
scan(path, *extra) / dedupe(path, *extra) |
run oans readonly / with -d into self.hf |
dm(*args) |
run with arbitrary args; output in self.out, code in self.rc |
assertDmOk() |
fail if the run printed any error signature (exit code alone is unreliable — oans can exit 0 after per-file failures) |
hf_count(table) / hf_query(sql) / hf_scalar(sql) |
query the hashfile via stdlib sqlite3 |
files_fingerprint() / extents_fingerprint() |
stable digest of a table, for equivalence checks |
assertShared(a, b) / assertNotShared(a, b) |
do two files share physical extents? (via FIEMAP) |
net_change() / assertNoNewSharing() |
the reported "net change in shared extents" |
tree_digest(dir) |
content hash of a whole tree, for integrity checks |
mkrand / mkdup / make_sparse / write / hardlink |
build test data (copies are plain writes, so the pre-dedupe state is genuinely unshared) |
requires_reflink (class decorator) |
skip tests that need dedupe when the fs can't |
- The suite is self-validating: running it against a binary with a known bug fails the relevant tests. The EINVAL cases fail on a pre-fix build, and the hardlink cases fail on a build without the batched-writer seen-inode guard.
- Sharing is detected by reading each file's physical extents with the
FIEMAPioctl (harness.phys_extents) and intersecting them — the same mechanismfilefraguses, but in-process, so no output parsing. - On btrfs,
cpreflinks by default; the builders make duplicate inputs with plain writes so they start out with independent storage and dedupe has real work to do.