mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-01 14:58:14 +00:00
docs: how to write a pspautotest and run it on a real PSP
We had a doc for running the existing tests against headless, but nothing on the other half - bringing up PSPLink and usbhostfs_pc, what gentest.py does, and how to get an .expected out of real hardware. Write that down, including the parts that cost time to rediscover: usbhostfs_pc's working directory is host0:/ so it has to start in the pspautotests root, gentest.py makes the whole test directory and several old tests no longer build under pspdev's GCC 15 (use -k), rebuilding a .prx with a newer toolchain balloons it, and host0: is not FAT so anything testing FAT semantics needs ms0:. Also adds the io/shortname test the doc uses as its worked example. It stays in tests_next: hardware preserves the case of d_name where we uppercase it, and appends ~1 to the short name of anything that isn't already valid uppercase 8.3 where we only do that on a collision. threads/tls/create moves to tests_next as well. It's collateral from the submodule bump - upstream 1dcefeb regenerated its .expected on a PSP with less free memory, so allocations at 1MB and above now expect failure, and partitions 8 and 9 now expect 800200D2 where we return 800200D1.
This commit is contained in:
1 parent
fb8c99ad49
commit
87bb9dd965
4 files changed
+203
-3
No files matched your search
@@ -0,0 +1,197 @@
|
||||
# Writing pspautotests and running them on a real PSP
|
||||
|
||||
[docs/pspautotests.md](pspautotests.md) covers running the *existing* tests against PPSSPPHeadless.
|
||||
This document covers the other half: connecting a real PSP over USB, running a test on it, and
|
||||
recording its output as the `.expected` file that becomes the ground truth.
|
||||
|
||||
You need this whenever the answer to "what does the hardware actually do?" isn't already in an
|
||||
`.expected` file - which is most of the time when you're implementing something new.
|
||||
|
||||
## What you need
|
||||
|
||||
- A PSP with custom firmware and a USB cable. (Verified against firmware 6.61 / PSPLink v3.0.)
|
||||
- **PSPLink** installed on the PSP, in `PSP/GAME/psplink`, and *running* - launched from the game
|
||||
menu. This is not the same as connecting the PSP in USB mass-storage mode; if the PC sees
|
||||
"PSP Type A" you're in USB mode, and you want "PSP Type B".
|
||||
- The **pspdev toolchain** on the PC, which supplies `psp-gcc`, `pspsh` and `usbhostfs_pc`.
|
||||
On macOS it lives in `~/pspdev` by default and is *not* on `PATH`, so prefix commands with
|
||||
`PATH="$HOME/pspdev/bin:$PATH"` or export it once per shell. On macOS you also need
|
||||
`brew install libusb-compat`; on Windows, the libusbK driver via Zadig.
|
||||
|
||||
Ask the user to connect the PSP and start PSPLink - you can't do it for them. To confirm it's
|
||||
there before touching anything else:
|
||||
|
||||
```bash
|
||||
ioreg -p IOUSB -l -w 0 | grep -i "USB Product Name" # macOS; want "PSP Type B"
|
||||
```
|
||||
|
||||
## The three moving parts
|
||||
|
||||
```
|
||||
PSP (PSPLink) <--USB--> usbhostfs_pc <--TCP 3000--> pspsh / gentest.py
|
||||
```
|
||||
|
||||
- **`usbhostfs_pc -b 3000`** bridges USB to a TCP port and serves the PC filesystem to the PSP as
|
||||
`host0:/`. **Start it from the `pspautotests` root**, because `host0:/` is literally its working
|
||||
directory, and that's where a test's output files land.
|
||||
- **`pspsh -p 3000`** is a shell on the PSP. `pspsh -p 3000 -e "<cmd>"` runs one command and exits.
|
||||
Handy ones: `ls`, `pwd`, `reset` (reboot PSPLink after a hung test), `pspver`, `power`, `scrshot`.
|
||||
- **`gentest.py`** builds a test, runs it through `pspsh`, waits for it to finish and copies the
|
||||
output over the `.expected` file. It starts `usbhostfs_pc` itself if the port isn't already open.
|
||||
|
||||
Bringing it up, once, from the repo root:
|
||||
|
||||
```bash
|
||||
export PATH="$HOME/pspdev/bin:$PATH"
|
||||
cd pspautotests
|
||||
(cd common && make) # builds libcommon.a; gentest.py refuses to run without it
|
||||
usbhostfs_pc -b 3000 & # leave running; prints "Connected to device"
|
||||
pspsh -p 3000 -e ls # sanity check - should list the pspautotests directory
|
||||
```
|
||||
|
||||
If `ls` shows `host0:/` contents you have a working chain. If it hangs or shows nothing, PSPLink
|
||||
isn't running or the USB driver didn't bind.
|
||||
|
||||
## How a test reports its results
|
||||
|
||||
`common/common.c` wraps every test. Before `main()` it redirects the process's `stdout` and
|
||||
`stderr` to `host0:/__testoutput.txt` and `host0:/__testerror.txt`, and after `main()` returns it
|
||||
writes `host0:/__testfinish.txt` as a completion marker. So in the `pspautotests` root you'll see:
|
||||
|
||||
| File | Meaning |
|
||||
|---|---|
|
||||
| `__testoutput.txt` | the test's stdout - this is what becomes the `.expected` file |
|
||||
| `__testerror.txt` | stderr; **non-empty means `gentest.py` refuses to write `.expected`** |
|
||||
| `__testfinish.txt` | written on clean exit; its absence is how a timeout is detected |
|
||||
| `__screenshot.bmp` | written by `emulatorEmitScreenshot()`; becomes `.expected.bmp` |
|
||||
|
||||
`gentest.py` deletes all four before each run, so a stale file can't be mistaken for a result.
|
||||
|
||||
That redirection is also why `usbhostfs_pc`'s working directory matters: get it wrong and the
|
||||
output files appear somewhere unexpected, or the test can't open its data files.
|
||||
|
||||
## Writing a new test
|
||||
|
||||
A test is one directory under `pspautotests/tests/`, holding a `Makefile`, the source, the built
|
||||
`.prx`, and the `.expected`. Minimal `Makefile` - use `common.mk`, not the older verbose style
|
||||
still found in some directories:
|
||||
|
||||
```make
|
||||
TARGETS = shortname
|
||||
|
||||
COMMON_DIR = ../../../common
|
||||
include $(COMMON_DIR)/common.mk
|
||||
```
|
||||
|
||||
`TARGETS` lists every test in that directory (one `.c`/`.cpp` per entry); `COMMON_DIR` is a
|
||||
relative path, so count the levels. `common.mk` sets `BUILD_PRX = 1` and links `libcommon`.
|
||||
|
||||
The source includes `<common.h>`, which `#define`s `main` to `test_main` so the wrapper above runs.
|
||||
Add `<sysmem-imports.h>` if you need `sceKernelSetCompiledSdkVersion*`. Then just `printf`.
|
||||
|
||||
```c
|
||||
#include <common.h>
|
||||
#include <pspiofilemgr.h>
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
printf("result: %08x\n", sceIoSomething());
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
Two things `common.h` gives you that are easy to miss: `ARRAY_SIZE`, and `checkpoint()`, which
|
||||
prefixes each line with `[r]`/`[x]` to record whether a reschedule happened - see
|
||||
[docs/pspautotests.md](pspautotests.md) for what those markers mean. Use plain `printf` when
|
||||
scheduling isn't what you're testing.
|
||||
|
||||
Then generate the expected output, from the `pspautotests` root:
|
||||
|
||||
```bash
|
||||
python3 gentest.py io/shortname/shortname # path under tests/, without .prx
|
||||
```
|
||||
|
||||
It runs `make` in the test's directory first, then runs it on the PSP and writes
|
||||
`tests/io/shortname/shortname.expected`.
|
||||
|
||||
Finally register the test in the repo root's `test.py`: new tests go in **`tests_next`**, and move
|
||||
to `tests_good` only once PPSSPP passes them. Then check PPSSPP against the hardware:
|
||||
|
||||
```bash
|
||||
python3 test.py --graphics=software io/shortname/shortname
|
||||
```
|
||||
|
||||
### Design rules for a test that can actually pass
|
||||
|
||||
- **Only print things that are the same on hardware and in the emulator.** Kernel pointers, heap
|
||||
addresses and absolute times are not - PSPLink shifts the memory layout, so a test that prints
|
||||
them can never pass. Print offsets from a base, or ranges, instead.
|
||||
- **Sort anything whose order isn't the point.** `sceIoDread` returns a real FAT directory in
|
||||
creation order and a host directory in whatever order the host filesystem gives; if you're
|
||||
testing names, `qsort` by name and the difference disappears.
|
||||
- **Make it repeatable.** If the test creates files, delete them at the start *and* the end - an
|
||||
aborted run otherwise leaves state that changes the next run's output.
|
||||
- **Choose an encoding with no ambiguity.** When dumping a buffer, don't render NUL as `.` if the
|
||||
data can contain a literal `.`. Pick a character the data can't contain (`|` is forbidden in FAT
|
||||
names, so it works there) - otherwise the `.expected` quietly lies.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **`gentest.py` runs `make` for the whole test directory, not just your test.** Several older
|
||||
tests no longer compile with the modern pspdev GCC (15.x turns `-Wint-conversion` into an error,
|
||||
e.g. `tests/misc/dcache.c`), and the build failure aborts the run before it reaches the PSP.
|
||||
Work around it with `gentest.py -k` (`--keep`, skips `make` entirely) after building your own
|
||||
target by hand with `make yourtest.prx`.
|
||||
- **Rebuilding a `.prx` with a newer toolchain balloons it** - `testgp.prx` went from 117 KB to
|
||||
191 KB with no source change. The `.prx` files are committed, so check `git status` and revert
|
||||
any you didn't mean to touch; don't sweep unrelated rebuilds into your commit.
|
||||
- **`host0:` is not a FAT volume.** It's PSPLink's bridge to the PC, and it has its own rules -
|
||||
`tests/io/directory` shows it uppercasing short names, and there are no 8.3 short names at all.
|
||||
Anything testing FAT semantics has to run against `ms0:`, i.e. create a scratch directory on the
|
||||
real memory stick (and clean it up).
|
||||
- **A test that hangs leaves the PSP wedged.** `gentest.py` issues `pspsh -e reset` after a
|
||||
timeout, but if you ran the PRX by hand, do that yourself. Default timeout is 10s; raise it with
|
||||
`-t SECONDS`.
|
||||
- **`tests_to_generate` in `gentest.py`** - the list used when you pass no arguments - is stale;
|
||||
several paths in it no longer exist. Always name the test you want.
|
||||
- **`--sdkver` matters for some APIs.** `gentest.py --sdkver=6060010 --sdkver-func=606` makes the
|
||||
test call `sceKernelSetCompiledSdkVersion606()` at startup, and `-a`/`--all-versions` sweeps every
|
||||
known version reporting which ones behave differently - a fast way to find version-gated
|
||||
behavior. A test can also call `sceKernelSetCompiledSdkVersion*()` mid-run to cover several
|
||||
versions in one `.expected`.
|
||||
- **The module must be named `TESTMODULE`** (`common.c` does this) or `gentest.py` prints the load
|
||||
line as an unexpected result.
|
||||
|
||||
## Worked example: FAT short names
|
||||
|
||||
`tests/io/shortname` was written this way and is a decent template. It creates a scratch directory
|
||||
on `ms0:`, fills it with names that exercise the 8.3 rules, and dumps the raw `d_private` block
|
||||
that `sceIoDread` fills in.
|
||||
|
||||
It was written to *dump* rather than *decode* on purpose, and that immediately paid off: the
|
||||
struct in the SDK's own `pspiofilemgr_dirent.h` does not match what firmware 6.61 writes when the
|
||||
compiled SDK version is unset or below 3.08. The real layouts are
|
||||
|
||||
| Compiled SDK version | Layout |
|
||||
|---|---|
|
||||
| unset, or <= 3.07 | short name at byte 0 (13 bytes), long name at byte 13 - no size field |
|
||||
| >= 3.08 | caller-supplied size at byte 0, short name at byte 4 (16 bytes), long name at byte 20 |
|
||||
|
||||
which is what `Core/HLE/sceIo.cpp` implements, now confirmed on hardware rather than inherited from
|
||||
JPCSP. Decoding with the SDK struct instead would have produced strings truncated at the front and
|
||||
an `.expected` that silently enshrined the mistake.
|
||||
|
||||
The test also caught three real emulator/hardware differences, still open in `tests_next`:
|
||||
hardware preserves the original case in `d_name` (`readme.txt`) where PPSSPP uppercases it
|
||||
(`README.TXT`), and hardware appends `~1` to the short name of any name that isn't already valid
|
||||
uppercase 8.3 (`readme.txt` -> `README~1.TXT`, `MiXeD.txt` -> `MIXED~1.TXT`) where PPSSPP only does
|
||||
so on a collision.
|
||||
|
||||
## Committing
|
||||
|
||||
`pspautotests` is a git submodule, so a new test is two commits:
|
||||
|
||||
1. Commit and push inside `pspautotests/` (the new test directory, including the built `.prx` and
|
||||
the generated `.expected`).
|
||||
2. Commit in the PPSSPP repo, including **both** the `test.py` change and the bumped submodule
|
||||
pointer (`git add pspautotests`) - otherwise CI checks out the old submodule and the test
|
||||
doesn't exist.
|
||||
Reference in new issue
Block a user