All test data are synthetic phantoms generated by reference/src/nifti_preview_ref/synthetic.py
(nifti-preview-ref make-fixtures DIR), DICOM files written by the tests, a few
codec streams made from synthetic images (backend/src/formats/dicom/testdata/),
a short bzip2 stream (backend/src/formats/testdata/) and the test files that ship
with pydicom. No subject data is downloaded.
| Suite | Command | What it covers |
|---|---|---|
| Rust unit + robustness | cd backend && cargo test && cargo test --release |
header parsing and repairs, orientation, percentiles, label colours; 7 200 mutated/truncated files (real extreme float values, NIfTI-2, big-endian, 2D, 5D, every datatype) must fail cleanly within a deadline; vox_offset 0; bgzip |
| Hostile files | (same) tests/hostile.rs |
huge vox_offset in a tiny gzip, giant slices, an 8 GB sparse .nii, a 300 MB mapped file, FIFO, /dev/zero, path replaced after open, lying gzip trailer, trailing garbage, decompression bomb, NaN sizes and affines, control characters in descrip, a NIfTI-2 header claiming 2^62 volumes |
| Spectroscopy | (same) tests/mrs.rs, and the FFT against a direct DFT |
explicit chemical-shift references (including zero and negative shifts), standard uppercase nuclei and zero echo time; NIfTI-MRS single voxel with dephased coils and dynamics (upright NAA, Cr, Cho at their ppm), edit, ISIS, phase-cycle, indirect and user dimensions taken at index 0 (an edited pair whose difference matters), spectroscopic imaging with a spectrum per voxel, axis in Hz or points without frequency or dwell time, big-endian complex128 without orientation, metadata counts for enormous higher dimensions without building their index lists; JSON extensions that are invalid, huge or badly sized, data above the limit or truncated, too many points (refused before every FID is read) |
| Resampling | (same) tests/resample.rs |
an oblique image in the anatomical modes: a linear function of the world coordinates survives the trilinear interpolation, labels come from the nearest voxel, RGB is interpolated per channel, black outside; the stored mode and flat or straight images keep the voxel grid; cursor carried from grid to grid, values of the nearest voxel |
| Protocol and sandbox | (same) tests/protocol.rs, tests/sandbox.rs |
malformed, deeply nested and oversized requests, out-of-range indices, environment limits; in a child process, writes, exec, TCP, UDP, Unix sockets, socketpair and big allocations fail under the sandbox while reading and threads still work; an inherited socket is closed by serve; images at the limit still load |
| Values | (same) tests/values.rs |
dump writes little-endian values whatever the byte order of the file (float, integer, complex; RGB untouched); uint32, int64/uint64 and float64 labels stay distinct (exact readout, range, count, volume and colour; both NIfTI formats, byte orders and gzip); finite extreme/subnormal grayscale ranges keep contrast; compensated float32/float64 scaling is applied before reducing display precision (both NIfTI formats and byte orders); coordinates in mm for headers in mm, m and µm; render --view with an unknown view fails; under an inherited memory limit, a 64 MiB image gives an answer or an error, never an abort |
| Other formats | (same) tests/formats.rs |
the same 4D array written as MGH/MGZ, MRtrix (every layout, gzip, a .mih with two data files), NRRD (raw, gzip, bzip2, ascii, hex; attached and detached, list axis first, LIST, %d patterns, byte skip, line skip) and MetaImage gives the grid, affine, slices, values and time courses of the NIfTI file; a data file behind a symbolic link is read only when the link stays in the header’s folder (also through a linked folder; raw, zlib and text data); the bzip2 decoder against the bzip2 tool (runs, every byte value, incompressible data, several blocks, damaged copies); hostile headers and data |
| DICOM | (same) tests/dicom.rs, and unit tests of each decoder |
every transfer syntax a test can write (implicit/explicit VR, both byte orders, deflated, RLE, lossless JPEG) decodes to the same values; JPEG-LS (lossless, near-lossless, sample-interleaved colour), JPEG baseline and JPEG 2000 streams; geometry (LPS→RAS, oblique), series rebuilt from a folder (shuffled names, 4D, a copy of an instance, another series, opening on the file chosen; a folder opened on its longest series), missing slices, irregular spacing, gantry tilt, cines, RT dose, enhanced files (per-frame positions, dimension indices, per-frame rescale), echoes that get no times (enhanced dimension indices, classic EchoNumbers), images of another pixel spacing left out, double float images each with its rescale, a Siemens mosaic, RGB planar, palettes, MONOCHROME1, 1-bit frames, DICOMDIR (case of the names, .. and links leading outside refused), series_list/open_series/dicom_window through the protocol; hostile files and 600 mutants; truncated pixel data read up to the cut (with a note), compressed ones refused; windows of float data and LINEAR_EXACT; a straight-line VOI LUT applied as its window, a curve or a SIGMOID window with a warning; reports, structure sets and ECGs described (implicit and explicit VR, no names of people) |
| Network | (same) tests/network.rs, unit tests of storage.rs |
every file taken as remote: nothing read with defer_network, a background load with progress then the values of the file (uncompressed and gzip), other volumes read in the background, requests refused while loading, a load given up by open, no thumbnail unless asked; the mount of a path (escaped names, the longest mount point), remote and local file system types |
| Viewer state and panel | (same) tests/view.rs, unit tests of panel.rs |
the start and label cursors, readouts (FreeSurfer names, voxels, mm³, RGB, NaN; outside a resampled grid, checked against an independent nearest voxel), the label list, cursors carried between the stored and resampled grids, single-slice images, volume times, the error codes of series requests, a load on a helper thread (its progress and rate, cancelled, lost), the slice cache (replaced, refused, nothing else evicted); empty label names and windows that are not finite; numbers written as JavaScript writes them (checked with gjs: toPrecision and toFixed ties, exponents) |
| nitty, units | cd backend && cargo test -p nitty --lib |
the input parser (keys, modifiers, SGR and X10 mouse, other buttons and the horizontal wheel ignored, every query answer, sequences cut between reads, a lone Escape, 200 000 random chunks without growth), character widths (CJK, emoji), a panic on another thread leaving the terminal alone, 256 and 16 colours (every palette entry maps to itself, grey levels within 13), the cell diff (only changed cells, minimal SGR), half blocks byte for byte, base64 (RFC 4648), Kitty chunks and tmux passthrough, Sixel decoded back (exact grey levels, median cut within the registers, run-length), layout (one scale, tiny and huge terminals), picture sizes per protocol, the choice of protocol and colours from replies and environment (iTerm2 named by a forwarded LC_TERMINAL behind tmux not trusted), options |
| nitty, quick look | cargo test -p nitty --test glance |
the output played on a strict terminal (tests/common/vt.rs, which fails on any unknown sequence): right on the right in neurological and on the left in radiological, letters on the right edges, front at the top, one scale across views, FreeSurfer colours in 24-bit and 256 colours, dumb, unknown and Linux-console terminals, the three image protocols placed on their cells, Kitty through tmux, missing/folder/truncated/CIFTI files, escape sequences in a header and in a file name never written, a too narrow width, several files, options checked against the image, a closed pipe, --panel-json (and its volume line); the viewer’s warnings before the panel, cut to the width; the exit status of every run; the Linux console in 16 colours in front and behind; option errors cleaned of escape sequences, -i refused with TERM=dumb, an empty folder, --help into a closed pipe |
| nitty, interactive | cargo test -p nitty --test interactive |
nitty in a pseudo-terminal (tests/common/pty.rs, which reads the settings before nitty starts, closes its own copy of the terminal side and reads stderr as it comes) that plays a terminal answering the queries or not, on the strict terminal in raw mode (a line feed keeps the column): drawing, cursor keys and slices, views, modes, panel, help (Ctrl-L keeps it; ? on a failed image does nothing), clicks on the expected voxels (image corners, the bright block and its value), an X10 mouse report read as a click and not as keys, the wheel, resizing down to 12 × 5 and back (each time a whole frame of the new size), SIGTERM, SIGQUIT, a SIGTERM while the terminal is asked (status 143), Ctrl-C, Ctrl-Z and an outside SIGTSTP (the alternate screen left, then taken again), the termios settings given back unchanged, the main screen and mouse restored, the sandbox in /proc (no new privileges; seccomp where the kernel has it, required with NITTY_REQUIRE_SANDBOX=1), Kitty/Sixel/half blocks chosen from the answers (and no answer at all within the wait), with Kitty one view sent again per move and the crosshair in the margins, 4D volumes and their time course, an unreadable file; skipped without a pseudo-terminal unless NITTY_REQUIRE_PTY=1 (both set in CI) |
| nitty in tmux | cargo test -p nitty --test tmux |
a real tmux (its own named socket, removed after): drawing in colour, keys, quitting back to the shell with status 0, never Kitty or iTerm2 sequences; skipped without tmux unless NITTY_REQUIRE_TMUX=1 (CI) |
| Fuzzing | cd backend && cargo +nightly fuzz run open_load_render CORPUS (and decoders, terminal) |
any file content through open, load, render and value; the DICOM decoders (JPEG, JPEG-LS, RLE, JPEG 2000) on any bytes (seeds: python3 fuzz/make_seeds.py CORPUS); nitty’s input parser (the same events whole, cut anywhere and byte by byte) and its Sixel and half-block encoders on any picture, at every colour depth and against a previous screen; 2 + 1 + 1 minutes in CI |
| Supply chain | cd backend && cargo deny --workspace check and cargo deny --manifest-path fuzz/Cargo.toml check advisories sources; tools/check-notices.sh; shellcheck -x -S warning on every script |
RustSec advisories, licences, sources (the fuzzing tools: advisories and sources); every crate compiled in named in THIRD-PARTY-NOTICES.md; shell script lint |
| DICOM against pydicom | pip install -e "reference[test,dicom]" pylibjpeg pylibjpeg-libjpeg pylibjpeg-openjpeg pyjpegls && pytest -rs reference |
the pixels of 46 test files of pydicom (every transfer syntax, colour, palettes, RT dose) as the backend decodes them, equal to pydicom’s (lossy JPEG within 1 to 3 levels); files needing a missing decoder plugin are skipped, or fail with NIFTI_PREVIEW_REQUIRE_CODECS=1 (CI, Python 3.12) |
| Reference + differential | pip install -e "reference[test]" && pytest reference |
nibabel reference (orientation markers in 8 storage orders × 3 modes, windowing, labels, datatypes, 4D/5D, errors, protocol); then the Rust binary must reproduce it on 108 synthetic files: metadata, affine, plans, statistics, slices at 4 positions × 3 views × every mode and colormap (±1 grey level, labels and RGB exact), including oblique and random affines resampled onto the world axes, the cursor carried from grid to grid, values on the grid, error codes; label identifiers above 2^24 and 2^53, uint64 limits and float64 labels (also resampled); finite extreme contrast; spectra of 10 NIfTI-MRS files, one with edit conditions (relative error 1e-4) |
| GJS client | NIFTI_PREVIEW_BACKEND=backend/target/release/nifti-preview [NITTY_BIN=…/nitty] gjs tests/gjs/test_client.js DIR |
with NITTY_BIN, nitty’s panel and its warnings, start cursor, readout, colormap, label list and volume line equal the viewer’s on ten fixtures (T1, LPS, oblique, 4D, segmentations, RGB, spectroscopy, a name with spaces), a NaN voxel and another label given with --at, and a DICOM series with the patient hidden and shown; nitty exits with 0, and refuses a point outside the image; pipelining, error mapping, missing binary, backend crash, response lines up to 8 MiB (a 9 MiB one refused, a 3 MiB one read), spectra inconsistent with their length refused, accessible descriptions, label identifiers in full (including decimal strings beyond 2^53), scroll coalescing, click navigation, mode switch, an oblique image resampled with its cursor kept across modes, an RGB image in colour and its L switch, spectroscopy (spectrum drawn, real part ↔ magnitude, range, the spectrum of the cursor voxel, metadata), 20 open/close cycles without leftover process; a scripted fake backend sending oversized, inconsistent, misnumbered or malformed replies, or none (deadlines); exceptions in viewer callbacks; unreadable settings; a DICOM series (opened on the file chosen, window from the file, patient hidden then shown, the series list, another series, a single image, a file alone); a file over the network (metadata only until the user agrees, progress, then the slices; read without asking under the limit); a DICOM file without an image (the refusal carries what it holds; malformed sections dropped); the window row hidden for colours |
| Sushi registry | XDG_DATA_HOME=… xvfb-run gjs tests/gjs/sushi_mimehandler.js 46.0 |
the real Sushi 46/50 renderer registry routes NIfTI to this viewer and every built-in type to its usual viewer |
| MIME | tests/mime/test_mime.sh |
.nii, .nii.gz (spaces, upper case, invalid content) get the NIfTI types; .tar.gz, .txt.gz, .gz, PNG, text unchanged; removal restores the defaults |
| Install | tests/install/test_install.sh [BINARY] |
Cargo-reported executable outside the source target directory; expected files, idempotent reinstall, upgrade cleanup, exact rollback of the home directory, other MIME packages preserved, “Open With” cases; root refused, relative XDG paths, a manifest pointing outside our files, invalid or symlinked mimeapps.list, viewers in folders others can write to, archive tools inherited from gzip not offered for .nii.gz without a viewer (and restored on uninstall), viewers in PATH or behind a desktop entry that others can change, folder names with quotes, $(), backslashes and backquotes (launched with the file), a home folder with %, quotes and $ (the “no viewer” launcher still runs), a link planted in our folder that the manifest points through, mimeapps.list with comments and empty groups restored byte for byte (edits between installs kept even when a later install discovers a viewer; both pre-existing and installer-created files, permissions preserved), a GNOME desktop whose gnome-mimeapps.list makes Files the default for gzip (the default becomes “a NIfTI viewer…”, replaced by a viewer installed later, removed on uninstall), restart of this session’s previewer only |
| One-command install | tests/install/test_get.sh [BINARY] |
get.sh through a pipe against a local release built by tools/package.sh: curl and wget-only installs, upgrade, --uninstall and the copied uninstall.sh restore the home directory, corrupted download, a .sha256 naming another file and network errors install nothing, no use of /tmp, root refused, missing Sushi prompts and stops cleanly |
| Terminal install | tests/install/test_terminal.sh STATIC_NITTY [NIFTI_PREVIEW] |
on a PATH without GNOME: install.sh --terminal (only ~/.local/bin/nitty, reinstall, PATH hint, XDG_BIN_HOME, --bin-dir with a space or a slash at the end, --terminal after the other options, no HOME needed with --bin-dir), refusals (relative folder, --bin-dir without --terminal, another program, one that never answers or prints escape sequences, GNOME options); links (a dangling link replaced, never written through; --binary given as a link); root simulated by an id that answers 0 (no HOME needed; a file of another user, or that others can change, or in a folder they can change, refused; a sticky folder accepted; no build); the build with a pretend cargo (this machine’s target named, the binary taken there, not a stale one); uninstall.sh --terminal (another program, a link, a dangling link left alone; -h); tools/package.sh --terminal (layout, a dynamic binary refused, readelf required); the archive’s install.sh without --terminal; get.sh --terminal and --terminal --uninstall (the same removal lines as uninstall.sh) from a local release without Sushi, a corrupted download, a release without nitty; the two installations ignoring each other |
| Real Sushi | tests/integration/sushi_xvfb.sh DIR OUT [BINARY] |
see COMPATIBILITY.md, plus the metadata panel shown once complete, 45 rapid file changes leaving at most one backend, spectra (L and Z redraw), keyboard only (V then Page Down) and the “Open With a NIfTI viewer…” button on a simulated Ubuntu desktop; writes screenshots to OUT; a DICOM report shows its content instead of the error page |
CI runs both the Rust tests and Clippy with stable and the Rust 1.85 MSRV, with
and without default features, for the backend and for nitty (-p nitty). Local
lint: cargo clippy --workspace --all-targets --locked -- -D warnings (and again
with --no-default-features).
The shell tests run in a throw-away home folder with every XDG directory inside
it (tests/lib/home.sh): they never touch the real home folder, its
mimeapps.list or its running previewer. The Rust tests write in folders
$TMPDIR/np-test-*, each removed when its test ends, also when it fails (and
those of earlier runs that were killed, at the next run); CI checks that none
is left. The real-Sushi test stops the services its D-Bus session started
before removing its home folder.
The real-Sushi test needs gnome-sushi gjs xvfb xdotool imagemagick x11-utils
dbus-x11 procps python3. CI runs it on Ubuntu 24.04 (Sushi 46), 26.04 (Sushi 50)
and 26.10 (Sushi 51, allowed to fail).
./install.sh from a clone)..nii.gz and press Space: three slices and the metadata appear.pgrep -x nifti-preview prints nothing.nitty -p IMAGE prints three views in half blocks;
nitty IMAGE opens the viewer (the status bar says blocks truecolor); move with the
arrows, Page Up/Down, O, L, Tab, the mouse; resize the window; Ctrl-Z then fg; q.
The prompt comes back as it was (stty -a unchanged).xterm -ti vt340): the status bar names the
protocol (kitty, iterm2, sixel) and the views are pictures.ssh localhost and run it there: half blocks (or Sixel with a tmux that
draws it); NITTY_PROBE_MS bounds the wait for the terminal’s answers.NITTY_TIMING=1 nitty IMAGE 2> timing.log: the time and bytes of each frame., .), a segmentation (Tab: names and cm³), a spectrum.