Files
ps5upload/TESTING.md
T
PhantomPtrandClaude Opus 5.5 9e5c258ad8 6.0.0: CI fixes, upgrade clean-up, docs and version (#377)
* docs: 6.0.0 changelog, README, FAQ and contributor docs

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff

* Release prep: version 6.0.0

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff

* fix(ci): keep the leaked test pools reachable for LSan; skip the tiny-file floor under coverage

- ava1-ctest diag/mgmt_fs: the &'static Pool each test leaks for
  AvaTransport::with_pool is now held in a static list, so LeakSanitizer sees
  it (and the sessions on its threads) as reachable instead of reporting
  ~140 KB of indirect leaks the frame-name suppression could never match.
  Verified under clang ASan+LSan in Linux Docker: diag 13/13, mgmt_fs 40/40.
- download.rs tiny-file throughput floor is skipped when cargo-llvm-cov runs
  (CARGO_LLVM_COV set): instrumentation slows every call several times over.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff

* fix(test): data_rust servers use the clients' fast timing

Five folder-host tests gave the client fast timing (dead after 500 ms) but
left the server on the default (ping every 2 s). On a slow runner a half-second
gap made the client drop healthy lanes and the job stalled until the 36 s
progress limit (both CI runs on #374/#377). The ordered-download tests already
set both ends; these now do too. The app uses one timing on both ends.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff

* fix(test): the C lane socket-buffer test accepts Linux's wmem_max cap

Same cap as the Rust lane test: Linux CI gives the send side 416 KiB, so the
1 MiB floor failed under ASan in CI. 128 KiB is still far above a default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff

* fix(ci): gcc unused-result in the FTP selftest, serial ctest in make test, sender state on failure

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff

* fix(ava1): flush a partial bundle after any reader message, not only a record

The last small-file record could arrive while a large file's chunk or root
was still queued behind it: the flush saw a non-empty channel, the next
message was not a record, and the records sat unsent until the receiver
cut the job as stalled (CI-only flake; 7/36 under local parallel load, 0/48 after).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff

* fix(validate): run the C interop tests serially in make ci too

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff

* fix(ava1): a resumed file goes in one-group pieces so a flapping link still makes progress

With kills every 2 s, the 4 MiB tail piece was cut before it was durable on every
attempt and the upload retried the same 28 MiB mark until it timed out (1 in 3
locally, CI coverage). Resumed files now send 1 MiB pieces: 5/5 in ~6 s.

Also: make docker-engine uses the repo root as context (as CI); governor
classification asserts are skipped under the sanitizers, where the C is the bottleneck.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff

* fix(test): single-instance gate tests hold the C-server lock; relay test cap leaves margin

Coverage runs the C tests in parallel; the two gate tests stop the one global C server.
The relay order test's 4 MiB/s cap finished in 5.9 s on CI against its >6 s precondition.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff

* fix(test): periodic-kill bound widened under coverage; tiny-file floor relaxed on CI runners

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff

* fix(test): lane-kill test kills until a resend; periodic-kill liveness race skipped under coverage

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff

* fix(test): a hung relay fails its test at the bound instead of hanging the CI job

The dest-ends relay test ran the relay under spawn_blocking: on a hang the timeout
panicked but the runtime then waited forever for the blocking thread (27 min on CI).
It now runs on a plain thread. The engine test steps get a 25-minute bound.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff

* fix(test): dest-ends relay test in its own binary on small files; relay logs its silent retries

The test hung a CI job twice inside the shared relay binary (behind the 384 MiB relays'
lock) and never reproduced locally or in Linux Docker. It now runs alone, on 4x32 MiB
(still twice the relay buffer), ~3 s. The relay's open-dropped and retry paths now say
so on stderr instead of looping silently. Governor classification asserts also skip
under coverage, where the instrumented sender is the bottleneck.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff

* fix(test): the partly-durable relay test caps its first attempt so it can be interrupted

On a fast runner the uncapped relay finished all 160 MiB before the first durable
report was observed. The first attempt now reads A through a 32 MiB/s proxy (~5 s);
the resume runs uncapped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff

* feat: one-time 6.0 upgrade clean-up that keeps every setting

Host (engine, once per data dir): pre-5.x files no 5.41/6.0 code reads (api_token,
ps5upload.ini, ps5upload_profiles.ini/.json, app-config.json) move to ~/.ps5upload/legacy-5x/.
App (desktop, once): the 1.x/2.x config.json/profiles.json move to legacy-5x/; the cached
ezremote-dpi.elf nothing loads is deleted. Client: the retired upload-streams key is dropped.
Console (once per console, on its first 6.0 helper answer): the FTX2 /data/ps5upload/tx and
/spool folders are deleted; a failure retries later. The health check no longer lists those
folders (its fix would have recreated them). Nothing current is moved or reset.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff

* fix(test): the download periodic-kill race is skipped under coverage, like its upload twin

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff

* fix(test): the tiny-zip floor only catches a stall on CI runners

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff

* fix(test): 'other calls still answer during launches' is a state check, not a wall-clock bound

Both mgmt_t6 tests asserted a sub-300 ms answer while a slow Sony call ran; on a loaded
runner that separates nothing. They now assert the answer arrived while the slow call
was still in flight.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff

* fix(ci): reword the 6.0 migration comments for check-no-ftx2

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 07:04:11 -07:00

8.9 KiB
Raw Blame History

Testing

Four layers, cheapest first. Push a check down to the cheapest layer that can hold it — a host self-test runs in milliseconds and cannot wedge a console, which a hardware test very much can.

Layer Command Needs
1. Host gate npm run validate nothing
2. Payload build + self-tests npm run validate:full PS5_PAYLOAD_SDK
3. Live console npm run validate:hardware a PS5 with the payload loaded
4. Fresh-install matrix manual, per release clean VMs

Layer 1 — the host gate

npm run validate      # or: make quality

Runs, and fails on any of: version drift against VERSION; script syntax (Node, Bash, Python, and PowerShell when pwsh is present); the script inventory audit; git whitespace (git diff --check); engine fmt, engine clippy (-D warnings) and engine tests; desktop cargo check, desktop clippy and desktop tests; client typecheck, client lint, client tests and client vite build; and i18n coverage.

Two of these catch things the others structurally cannot, and both have caught real regressions:

  • engine fmt fails on formatting no test exercises. Run cargo fmt --all in engine/ before assuming a failure is real.
  • client typecheck covers test files too. Vitest does not typecheck, so a test can pass while calling a function with the wrong argument types.

Running a narrower check and assuming the wider one follows is the most common way to burn a cycle here.

npm run validate is not the whole gate. Before a PR also run:

( cd client/src-tauri && cargo fmt --all -- --check )   # validate only checks fmt in engine/
( cd engine && cargo test -p ava1-ctest -- --test-threads=1 )
make test-payload                                       # needs PS5_PAYLOAD_SDK
make check-no-ftx2                                      # the retired protocol must stay gone

Layer 2 — payload build and self-tests

export PS5_PAYLOAD_SDK=/opt/ps5-payload-sdk
npm run validate:full          # or: make quality-full / make test-payload

Builds payload/ps5upload.elf and the DPI daemon, checks both are PS5 ELFs with valid gzip resources, then runs every host self-test with -Wall -Wextra -Werror:

Each self-test lives in payload/tests/*_selftest.c (hardware guards, ptrace recovery, app.db reading, FTP, SDK param rewriting, installer, cheats, wake watchdog and more). The AVA1 C itself is tested by ava1-ctest, which compiles it on the host and checks it against the Rust side (see the full gate below).

The SDK version is pinned in scripts/ps5-sdk.env (currently v0.43) and verified by checksum. Local installers and CI read the same file.

Adding payload logic? If it can be separated from the console, put it header-only in payload/include/ and give it a self-test here. That is the established convention and the reason the list above exists.

Layer 3 — live console

Nothing here is tied to one network. Every entry point takes an address:

export PS5_HOST=192.168.1.50            # console IP (loader + payload)
export PS5_ADDR=192.168.1.50            # console IP; the engine owns the port (9120)

make send-payload PS5_HOST=$PS5_HOST
npm run smoke:hardware                  # or: node tests/smoke-hardware.mjs
npm run validate:hardware               # host gate + make validate

Keep your own addresses in a gitignored local file rather than editing committed defaults — those stay generic on purpose.

Command Does
make validate Build, send, wait for :9120, smoke, sweep, write bench/reports/<ts>-sweep.{json,md}
make validate-xl Adds the 200k-file stress profile
node tests/smoke-hardware.mjs --no-spawn-engine Against an engine already running

Clean up test uploads afterwards:

curl -X POST http://127.0.0.1:19113/api/ps5/cleanup \
  -H 'content-type: application/json' \
  -d '{"addr":"'"$PS5_HOST"'","path":"/data/ps5upload/tests/manual"}'

Use a live console when touching: the payload C runtime, AVA1 (payload/ava1/), transfer/reconcile/resume, storage, mount, cleanup, the file browser, volume commands, or anything performance-sensitive.

Hardware testing has teeth

A payload bug can leave a console needing a power cycle — ports stay open but every connection resets, so payload takeover cannot recover it. That has happened, from a stack overflow in a file-transfer handler. Before testing a payload change:

  • Prefer a read-only probe first; confirm the payload still answers /api/ps5/status between steps.
  • Some reads (temperatures, power telemetry) ptrace-pause the system UI. They are safe on demand and unsafe on a loop.
  • Expect blanks that are not bugs: SoC clock, SoC power, CPU usage, fan duty, per-drive sensors and the console date/time are not exposed by retail firmware.

Layer 4 — fresh-install matrix, per release

CI proves the binaries build. It does not prove they launch on a machine that has never been a dev box. Every artifact has a historical "ships green, fails on fresh install" failure:

Platform Failure caught before Guard now in tree
Windows x64 / arm64 VCRUNTIME140.dll not found .cargo/config.toml sets +crt-static
Windows x64 / arm64 Explorer "Extract All" rejects the zip publish.yml packs via pwsh Compress-Archive
Linux x64 / arm64 AppImage needs FUSE ships PS5Upload.sh with APPIMAGE_EXTRACT_AND_RUN=1
Linux (any) glibc too new built on ubuntu-24.04 (glibc 2.39)
macOS x64 / arm64 Gatekeeper on first run documented; right-click → Open

Before tagging: unzip with the OS's own tool and launch on a clean Windows x64 VM, Windows arm64, Ubuntu 24.04, arm64 Linux, macOS arm64 and macOS x64. Sideload the APK on a real Android device.

See v2.5.1 and v2.5.2 for the "noticed after release, patched same day" playbook.

Coverage reports

npm run coverage            # or: make coverage
make coverage-client        # one side only
make coverage-engine

Outputs client/coverage/index.html, coverage/engine/html/index.html and coverage/engine/lcov.info. Rust coverage needs cargo-llvm-cov (npm run coverage -- --install-tools).

Cross-platform

CI checks the core crate for every shipped target (x86_64/aarch64 × linux-gnu, apple-darwin, pc-windows-msvc) and runs the desktop Rust suite natively on ubuntu-24.04, macos-14 and windows-2022 — that native matrix is what exercises the #[cfg(target_os = ...)] paths (keep-awake, update/download handling, launcher, sidecar extraction). A macOS dev box generally cannot build the Windows/Linux targets, so CI is the source of truth there.

Android

make android-deps      # verify SDK / JDK 17 / NDK / rustup target
make android-build     # debug APK, no device needed
make android-deploy    # build + install on connected devices
make run-android       # run on a device with live reload

Android links the engine in-process rather than spawning a sidecar, so engine startup failures surface differently — check the in-app log. .rar is desktop-only (the UnRAR C dependency is excluded).

Self-hosted engine

make docker-engine        # build the image
make docker-engine-run    # run it; exposes :19113

CI publishes the same image to GHCR on every release tag. The API is unauthenticated and can read, write and delete files on the console — LAN only, never the internet.

i18n

npm run i18n:check        # gate: fails on any non-allowlisted miss
npm run i18n:report       # same, always prints the per-language summary

client/src/i18n/locales/en.ts is the source of truth. A new tr() key must exist there or the gate fails. Then either translate it into the 18 locales or add it to scripts/i18n-known-missing.json as a deliberate, scoped deferral.

Do not run i18n:bootstrap. It rewrites the allowlist to the current state, silencing every gap at once — including whatever you were about to forget.

Script hygiene

npm run scripts:check     # syntax
npm run scripts:audit     # inventory

A lab or debug script with no caller is intentional, not dead code. Only remove generated artifacts, or scripts whose replacement is documented.

Multi-volume .rar

Multi-part archives work in both debug and release builds.

This needed a fix: upstream unrar 0.5.8 reads ~8 KB out of a much smaller buffer every time it crosses a volume boundary, which aborted any debug build that touched a multi-part .rar and was a latent out-of-bounds read in release. 0.5.8 is the latest release, so the crate is vendored at third_party/unrar with the fix and both workspaces redirect to it via [patch.crates-io].

See third_party/unrar/README.md for the patch. If you bump the crate, re-apply the hunk or drop the vendoring once upstream ships the fix.

The shipped .rar fixtures are all single-volume, so they never exercise this — verifying it needs a real multi-part archive.