chore: merge integ into fix-launch-names

This commit is contained in:
Abdessamad Derraz committed 2026-11-09 17:47:35 +01:00
commit a6aea81c17
36 files changed
+2347 -101

No files matched your search

+69
View File
@@ -636,5 +636,74 @@ class ContributorChecksFetchThePlatformsFiles(unittest.TestCase):
self.assertIn("export_native.py --all --refresh-cache", _read("wiki/testing-guide.md"))
class ArchiveRecipesAreCountedFromTheRecipes(unittest.TestCase):
"""Every archive has a recipe in recipes/archives.json: the README and
the site's page say how many rebuild exact and canonical, counted from
that file, never typed."""
def setUp(self) -> None:
import archive_build # noqa: PLC0415
records = archive_build.load_ledger().values()
self.counts = {
"total": len(records),
"exact": sum(r["rebuild"] == "exact" for r in records),
"canonical": sum(r["rebuild"] == "canonical" for r in records),
}
self.assertEqual(archive_build.rebuild_counts(), self.counts)
self.assertEqual(self.counts["exact"] + self.counts["canonical"], self.counts["total"])
def test_the_readme_line(self) -> None:
from generate_readme import archive_line # noqa: PLC0415
line = archive_line(self.counts)
self.assertIn(line, _read("README.md").splitlines())
self.assertIn(f"**{self.counts['total']:,} archives**", line)
self.assertIn(f"{self.counts['exact']:,} byte for byte", line)
self.assertIn(f"{self.counts['canonical']:,} with the same files inside", line)
self.assertIn(f"({SITE_URL}wiki/archive-recipes/)", line)
def test_the_page_counts(self) -> None:
import generate_site # noqa: PLC0415
page = _read("wiki/archive-recipes.md")
self.assertEqual(generate_site.with_archive_counts(page, self.counts), page,
"wiki/archive-recipes.md counts are stale: run scripts/generate_site.py")
for line in generate_site.archive_recipe_lines(self.counts):
self.assertIn(line, page)
def test_the_page_command_is_the_tools(self) -> None:
command = re.search(r"python scripts/romset_recipes.py (.+)", _read("wiki/archive-recipes.md"))
source = _read("scripts/romset_recipes.py")
for flag in re.findall(r"--[a-z-]+", command.group(1)):
with self.subTest(flag=flag):
self.assertIn(f'"{flag}"', source)
class TheReadmeBadges(unittest.TestCase):
"""One row of four badges under the links, each linking its source."""
EXPECTED = (
'<p align="center">'
'<a href="https://pypi.org/project/retrobios/"><img src="https://img.shields.io/pypi/v/retrobios" alt="PyPI version"></a> '
'<a href="https://pypi.org/project/retrobios/"><img src="https://img.shields.io/pypi/pyversions/retrobios" alt="Python versions"></a> '
f'<a href="{REPO}/actions/workflows/validate.yml"><img src="{REPO}/actions/workflows/validate.yml/badge.svg?branch=main" alt="Validation"></a> '
'<a href="LICENSE"><img src="https://img.shields.io/github/license/Abdess/retrobios" alt="License"></a>'
"</p>"
)
def test_exactly_these_four(self) -> None:
readme = _read("README.md")
self.assertEqual(len(re.findall(r"<img src=\"https://(?:img\.shields\.io|github\.com)/", readme)), 4)
lines = readme.splitlines()
self.assertIn(self.EXPECTED, lines)
links = next(i for i, line in enumerate(lines) if ">Get the files</a>" in line)
self.assertEqual(lines[links + 2], self.EXPECTED)
def test_the_workflow_and_the_license_exist(self) -> None:
self.assertTrue((REPO_ROOT / ".github" / "workflows" / "validate.yml").is_file())
self.assertTrue((REPO_ROOT / "LICENSE").is_file())
if __name__ == "__main__":
unittest.main()
+17
View File
@@ -1183,6 +1183,23 @@ class TestE2E(unittest.TestCase):
recalbox = {"cores": ["mame0278"], "systems": {}}
self.assertEqual(resolve_platform_cores(recalbox, profiles), {"mame0278"})
def test_a_platform_s_own_libretro_build_comes_with_its_retroarch(self):
"""ROCKNIX ships yabadabadoo with its RetroArch and names it nowhere
else: a list naming retroarch takes the libretro builds whose
built_by names that platform, and no other platform's."""
profiles = {
"yabause": {"type": "libretro", "systems": ["saturn"], "files": []},
"yabadabadoo": {"type": "libretro", "built_by": ["rocknix"],
"systems": ["saturn"], "files": []},
}
rocknix = {"_key": "rocknix", "cores": ["retroarch"], "systems": {}}
self.assertEqual(resolve_platform_cores(rocknix, profiles),
{"yabause", "yabadabadoo"})
other = {"_key": "batocera", "cores": ["retroarch"], "systems": {}}
self.assertEqual(resolve_platform_cores(other, profiles), {"yabause"})
everyone = {"_key": "retroarch", "cores": "all_libretro", "systems": {}}
self.assertEqual(resolve_platform_cores(everyone, profiles), {"yabause"})
def test_resolve_cores_explicit_list(self):
"""Explicit cores list matches against profile dict keys."""
config = {"cores": ["fbneo", "opera"], "systems": {"arcade": {"files": []}}}
+13 -6
View File
@@ -366,6 +366,13 @@ HOME = Path("/home/user")
SYSTEM_DIR = HOME / ".config" / "retroarch" / "system"
def screen_text(lines) -> str:
"""The screen as one line of words, a path broken after a separator
joined again: a folder under the checkout is as long as the checkout's
own path, and a deep clone wrapped it across two lines."""
return re.sub(r"(?<=[/\\]) ", "", " ".join(" ".join(lines).split()))
def fresh_state() -> install.TuiState:
state = install.TuiState(os_type="linux", home=HOME)
state.setups = [("retroarch", SYSTEM_DIR, "RetroArch"),
@@ -1856,7 +1863,7 @@ class TicksBarAndPaths(unittest.TestCase):
state.stack = ["folder"]
self.assertEqual(install._choose(state, "minui", card / "Bios"), [])
self.assertEqual(state.screen, "cardroot")
text = " ".join(line.strip() for line in install.tui_view(state, 120, 40))
text = screen_text(install.tui_view(state, 120, 40))
self.assertIn(f"under the card's root: {card}", text)
self.assertEqual(press(state, "enter"), [("fetch", "minui")])
self.assertEqual(state.dest, card)
@@ -2343,7 +2350,7 @@ class CardsSpaceAndConfirm(unittest.TestCase):
state, effects = install.tui_start(_args(), "linux")
label = f"muOS SD card ({root.name})"
self.assertEqual(state.setups, [("retroarch", root / "MUOS" / "bios", label)])
text = " ".join(" ".join(install.tui_view(state, 120, 40)).split())
text = screen_text(install.tui_view(state, 120, 40))
self.assertIn(f"> {label}", text)
self.assertIn(f"BIOS folder: {root / 'MUOS' / 'bios'}", text)
(root / "MUOS" / "bios").mkdir()
@@ -4135,7 +4142,7 @@ class AFolderThisUserCannotOpen(unittest.TestCase):
self.dest = self.lock / "system"
def said(self, state: install.TuiState) -> str:
return " ".join(" ".join(install.tui_view(state, 200, 40)).split())
return screen_text(install.tui_view(state, 200, 40))
def test_typed_it_stays_on_the_path_screen(self):
state = fresh_state()
@@ -6507,7 +6514,7 @@ class OneCardReading(unittest.TestCase):
self.assertEqual(state.screen, "checking" if one else "create")
if not one:
self.assertIn(note.split(":")[0],
" ".join(" ".join(install.tui_view(state, 120, 40)).split()))
screen_text(install.tui_view(state, 120, 40)))
install.tui_update(state, Key("enter"))
self.assertEqual(state.card_note, note)
@@ -11117,7 +11124,7 @@ class TapsGameDataAndExitCodes(_CardFixtures, unittest.TestCase):
_title, lines = install.tui_outcome(state)
self.assertIn(f"BIOS folder: {card / 'Bios'}", lines)
state.stack = ["checking"]
self.assertIn(str(card / "Bios"), " ".join(install.tui_view(state, 120, 40)))
self.assertIn(str(card / "Bios"), screen_text(install.tui_view(state, 120, 40)))
# Root cause 7: colour roles read the words.
@@ -11263,7 +11270,7 @@ class TapsGameDataAndExitCodes(_CardFixtures, unittest.TestCase):
folder = next(i for i, line in enumerate(lines) if line.startswith("BIOS folder:"))
named = next(i for i, line in enumerate(lines) if line.startswith("Named by"))
# Nothing between the folder, wrapped or not, and what named it.
self.assertTrue(all(line.startswith("/") for line in lines[folder + 1:named]), lines)
self.assertEqual("".join(lines[folder:named]), f"BIOS folder: {state.dest}", lines)
self.assertLess(named, next(i for i, line in enumerate(lines)
if line.startswith(("For ", "Copy them"))))
+655
View File
@@ -0,0 +1,655 @@
"""EmuELEC's launcher layer read from its packages.
Each fixture is a cut-down copy of the lines an extractor reads at EmuELEC
v4.8 (02b45fe53b85c45c75dbad8e44a648092dec2e72). The records must name the
core emuelecRunEmu.sh dispatches, carry the names in the order the code
tries them, point at the fixture's own lines, and a construct that is gone
must stop the scrape.
"""
from __future__ import annotations
import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache
import sys
import textwrap
import unittest
from pathlib import Path
import yaml
REPO_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO_ROOT / "scripts"))
from scraper import emuelec_launchers as el # noqa: E402
from scraper.emuelec_scraper import launcher_revision # noqa: E402
from scraper.shell_launchers import LauncherPatternMissing, RepoTree # noqa: E402
EMU = el.EMULATORS
def text(block: str) -> str:
return textwrap.dedent(block).lstrip("\n")
RUN_EMU = text(
"""
if [[ "${EMULATOR}" = "libretro" ]]; then
EMU="${CORE}_libretro"
fi
if [[ "${EMULATOR}" = "retrorun" ]]; then
EMU="${CORE}_libretro"
fi
MIDI_OUTPUT=$(get_ee_setting "ra_midi_output" "${PLATFORM}" "${BASEROMNAME}")
if [[ ! -z "${MIDI_OUTPUT}" ]]; then
emuelec-utils set_midi_source "${MIDI_OUTPUT}" "${EMULATOR}"
fi
if [ ${EMU} == "freej2me_libretro" ]; then
freej2me.sh
JAVA_HOME='/storage/roms/bios/jdk'
export JAVA_HOME
fi
case ${PLATFORM} in
"atarist")
if [ "${EMU}" = "HATARISA" ]; then
RUNTHIS='${TBASH} hatari.start "${ROMNAME}"'
fi
;;
"dreamcast"|"naomi")
if [ "${EMU}" = "flycastsa" ]; then
RUNTHIS='${TBASH} flycast.sh "${ROMNAME}"'
elif [ "${EMU}" = "flycast_dojo" ]; then
RUNTHIS='flycastdojo.sh "${ROMNAME}"'
fi
;;
"psx")
if [ "${EMU}" = "duckstation" ]; then
RUNTHIS='${TBASH} duckstation.sh "${ROMNAME}"'
fi
;;
"mame")
if [ "${EMU}" = "AdvanceMame" ]; then
RUNTHIS='${TBASH} advmame.sh "${PLATFORM}" "${ROMNAME}"'
fi
;;
"n64")
if [ "${EMU}" = "rice" ]; then
RUNTHIS='${TBASH} m64p.sh "${ROMNAME}"'
elif [ "${EMU}" = "glide64mk2" ]; then
RUNTHIS='${TBASH} m64p.sh "${ROMNAME}" m64p_gl64mk2'
fi
;;
"scummvm")
if [ "${EMU}" = "SCUMMVMSA" ]; then
RUNTHIS='${TBASH} scummvm.start sa "${ROMNAME}"'
fi
;;
"wii"|"gamecube")
if [ "${EMU}" = "dolphin" ]; then
RUNTHIS='${TBASH} dolphin.sh "${ROMNAME}"'
fi
;;
"pico8")
RUNTHIS='${TBASH} pico8.sh "${ROMNAME}"'
;;
"intellivision")
if [ "${EMU}" = "jzintv" ]; then
RUNTHIS='jzintv.sh "${ROMNAME}"'
fi
;;
"saturn")
if [ "${EMU}" = "yabasanshiroSA" ]; then
RUNTHIS='yabasanshiro.sh "${ROMNAME}"'
elif [ "${EMU}" = "yabasanshiroSA1_5" ]; then
RUNTHIS='yabasanshiro1_5.sh "${ROMNAME}"'
fi
;;
esac
case ${PLATFORM} in
"arcade"|"mame")
if [ "$EMU" = "mame_libretro" ]; then
mame.sh
fi
;;
esac
RUNTHIS='${RABIN} ${VERBOSE} ${RA_ARGS} -L /tmp/cores/${EMU}.so --config ${RACONF} "${ROMNAME}"'
RUNTHIS="retrorun"
RUNTHIS+=' ${CMD_ROTATE} --triggers -g -d /storage/roms/bios /tmp/cores/${EMU}.so "${ROMNAME}"'
"""
)
UTILS = text(
"""
function set_midi_source()
{
case ${midi_source} in
fluidsynth)
fluidsynth -is /storage/roms/bios/sc/SC-55.sf2 &
;;
timidity)
timidity -iA --module 1 &
;;
mt32)
mt32d -o 2 -i 24 -f /roms/bios &
;;
esac
}
"""
)
RETROARCH_MK = text(
"""
sed -i -e "s/# savestate_directory =/savestate_directory =\\/storage\\/roms\\/savestates/" ${INSTALL}/etc/retroarch.cfg
sed -i -e "s/# system_directory =/system_directory =\\/storage\\/roms\\/bios/" ${INSTALL}/etc/retroarch.cfg
"""
)
FREEJ2ME_SH = text(
"""
install_java
if [[ -f "/storage/roms/bios/jdk/eeversion" ]]; then
cp -rf /usr/lib/libretro/freej2me-lr.jar ${HOME}/roms/bios
else
exit 1
fi
"""
)
PICO8_SH = text(
"""
if [[ -e "/storage/roms/bios/pico-8" ]]; then
mkdir -p /emuelec/bin/pico-8
cp -rf /storage/roms/bios/pico-8/* /emuelec/bin/pico-8
rm -rf /storage/roms/bios/pico-8
chmod +x /emuelec/bin/pico-8/pico8_dyn
fi
if [[ "${CART}" == *"/splore"* ]]; then
/emuelec/bin/pico-8/pico8_dyn -splore -home /emuelec/configs/pico-8
else
/emuelec/bin/pico-8/pico8_dyn -run "${CART}" -home /emuelec/configs/pico-8
fi
"""
)
FLYCAST_SH = text(
"""
if [ ! -L "/storage/.local/share/flycast" ]; then
mkdir -p "/storage/roms/bios/dc"
rm -rf "/storage/.local/share/flycast"
ln -sf "/storage/roms/bios/dc" "/storage/.local/share/flycast"
fi
"""
)
FLYCASTDOJO_SH = text(
"""
LOCAL_DATA="/storage/roms/bios/dc_dojo"
LOCAL_HOME="/storage/.local/share/flycast-dojo"
if [ ! -L "${LOCAL_HOME}" ]; then
rm -rf "${LOCAL_HOME}"
ln -sf "${LOCAL_DATA}" "${LOCAL_HOME}"
fi
"""
)
AMIBERRY_MK = text(
"""
cp -a ${PKG_DIR}/config/* ${INSTALL}/usr/config/amiberry/
ln -s /storage/roms/bios ${INSTALL}/usr/config/amiberry/kickstarts
"""
)
TIMIDITY_MK = text(
"""
ln -sf /storage/roms/bios/timidity/freepats ${INSTALL}/usr/config/timidity/freepats
ln -sf /storage/.config/timidity/freepats.cfg ${INSTALL}/etc/timidity/freepats.cfg
"""
)
DUCKSTATION_INI = text(
"""
[BIOS]
SearchDirectory = /storage/roms/bios
PathNTSCU =
PathNTSCJ =
PathPAL =
"""
)
DUCKSTATION_MK = text(
"""
cp -rf ${PKG_DIR}/config/* ${INSTALL}/usr/config/emuelec/configs/duckstation
"""
)
DUCKSTATION_SH = text(
"""
CONFIG_DIR="/emuelec/configs/duckstation"
duckstation-nogui -fullscreen -settings "${CONFIG_DIR}/settings.ini" -- "${1}"
"""
)
def yabasanshiro_sh(program: str) -> str:
return text(
f"""
BIOS=""
HLEBIOS=$(get_ee_setting hlebios saturn "${{ROMNAME}}")
if [ "${{HLEBIOS}}" != 1 ]; then
if [ -e "/storage/roms/bios/saturn_bios.bin" ]; then
BIOS="-b /storage/roms/bios/saturn_bios.bin"
else
BIOS=""
fi
fi
{{ {program} -r 2 -i "${{1}}" ${{BIOS}}; }} > /emuelec/logs/emuelec.log 2>&1
"""
)
JZINTV_SH = text(
"""
exec jzintv -f1 -z "$RES" -p /storage/roms/bios/ "$ROM" $KBD_OPT
"""
)
M64P_SH = text(
"""
BIOSDIR="/storage/roms/bios/Mupen64plus"
IPLROM="${BIOSDIR}/N64DD IPLROM (J).n64"
[ ! -f "${IPLROM}" ] && IPLROM="${BIOSDIR}/IPL.n64"
if [ -f "${GAMEDIR}/${GAMENAME}.ndd" ] && [ -f "${IPLROM}" ]; then
RENMODE="1"
else
IPLROM=""
fi
mupen64plus --fullscreen --dd-ipl-rom "${IPLROM}" --dd-disk "${DDDISK}" "${FILE}" ;;
"""
)
HATARI_START = text(
"""
HATARI_TMP_CONFIG="$HATARI_TMP_DIR"/.hatari-temp.cfg
cp $HATARI_CONFIG_DIR/Atari-ST-default.cfg "$HATARI_TMP_CONFIG"
hatari --configfile "$HATARI_TMP_CONFIG" --disk-a "$1"
"""
)
HATARI_CFG = text(
"""
[HardDisk]
szHardDiskDirectory = /storage/.hatari
[ROM]
szTosImageFileName = /storage/roms/bios/tos.img
szCartridgeImageFileName =
"""
)
HATARI_MK = text(
"""
cp -R ${PKG_DIR}/config/* ${INSTALL}/usr/config/hatari
"""
)
ADVMAME_MK = text(
"""
if [ "${DEVICE}" == "OdroidGoAdvance" ]; then
cp -r ${PKG_DIR}/config/advmame.rc_oga ${INSTALL}/usr/config/emuelec/configs/advmame/advmame.rc
else
cp -r ${PKG_DIR}/config/advmame.rc ${INSTALL}/usr/config/emuelec/configs/advmame/advmame.rc
fi
"""
)
ADVMAME_RC = text(
"""
dir_rom /storage/roms/mame
dir_sample /storage/roms/bios/advmame/samples
"""
)
ADVMAME_SH = text(
"""
SDL_AUDIODRIVER=alsa advmame ${ARG} -quiet
"""
)
MAME_SH = text(
"""
if [ ! -f "/storage/roms/bios/mame/hash/fmtowns_cd.xml" ]; then
mkdir -p /storage/roms/bios/mame/hash
cp -rf "/usr/config/emuelec/configs/mame/hash/"fm* "/storage/roms/bios/mame/hash"
fi
if [ ! -f "/storage/roms/bios/mame/ini/mame.ini" ]; then
mkdir -p /storage/roms/bios/mame/ini
cp -rf "/usr/config/emuelec/configs/mame/ini/"mame* "/storage/roms/bios/mame/ini"
fi
if [ ! -f "/storage/.config/retroarch/config/MAME/MAME.opt" ]; then
cp -rf "/usr/config/emuelec/configs/mame/MAME/"MAME* "/storage/.config/retroarch/config/MAME"
fi
"""
)
MAME_INI = text(
"""
rompath /storage/roms/bios/mame/bios;/storage/roms/mame;/storage/roms/arcade
samplepath /storage/roms/bios/mame/samples
cfg_directory /storage/roms/bios/mame/mame/cfg
"""
)
MAME_OPT = text(
"""
mame_boot_to_bios = "disabled"
mame_read_config = "enabled"
"""
)
SCUMMVM_START = text(
"""
CONFIG_DIR="/storage/.config/scummvm"
if [ -d "/storage/.config/scummvm/extra" ]; then
EXTRA="--extrapath=/storage/.config/scummvm/extra"
fi
case $1 in
"sa")
scummvm --fullscreen --joystick=0 $EXTRA "${GAME}"
;;
"libretro")
retroarch -L /tmp/cores/scummvm_libretro.so --config ${RATMPCONF} "${GAME}"
;;
"add")
scummvm --add --path="/storage/roms/scummvm" --recursive
cp ${CONFIG_DIR}/scummvm.ini /storage/roms/bios/scummvm.ini
;;
esac
"""
)
DOLPHIN_SH = text(
"""
XDG_CONFIG_HOME=/emuelec/configs XDG_DATA_HOME=/storage/roms/dolphin /usr/bin/dolphin-emu-nogui -p fbdev "${1}"
"""
)
FILES = {
el.RUN_EMU: RUN_EMU,
el.UTILS: UTILS,
el.RETROARCH_MK: RETROARCH_MK,
f"{el.LIBRETRO}/freej2me/scripts/freej2me.sh": FREEJ2ME_SH,
f"{el.BIN}/pico8.sh": PICO8_SH,
f"{EMU}/flycastsa/scripts/flycast.sh": FLYCAST_SH,
f"{EMU}/flycast-dojo/scripts/flycastdojo.sh": FLYCASTDOJO_SH,
f"{EMU}/amiberry/package.mk": AMIBERRY_MK,
f"{EMU}/amiberry-lite/package.mk": AMIBERRY_MK.replace("amiberry/", "amiberry-lite/"),
f"{el.SX05RE}/tools/sound/timidity/package.mk": TIMIDITY_MK,
f"{EMU}/duckstation/config/settings.ini": DUCKSTATION_INI,
f"{EMU}/duckstation/package.mk": DUCKSTATION_MK,
f"{EMU}/duckstation/scripts/duckstation.sh": DUCKSTATION_SH,
el.YABASANSHIRO_SCRIPTS[0]: yabasanshiro_sh("yabasanshiro"),
el.YABASANSHIRO_SCRIPTS[1]: yabasanshiro_sh("yabasanshiro1_5"),
f"{EMU}/jzintv/scripts/jzintv.sh": JZINTV_SH,
f"{EMU}/mupen64plussa/mupen64plussa-core/m64p.sh": M64P_SH,
f"{EMU}/hatarisa/scripts/hatari.start": HATARI_START,
f"{EMU}/hatarisa/config/conf/Atari-ST-default.cfg": HATARI_CFG,
f"{EMU}/hatarisa/package.mk": HATARI_MK,
f"{EMU}/advancemame/package.mk": ADVMAME_MK,
f"{EMU}/advancemame/config/advmame.rc": ADVMAME_RC,
f"{EMU}/advancemame/config/advmame.rc_oga": ADVMAME_RC,
f"{EMU}/advancemame/bin/advmame.sh": ADVMAME_SH,
f"{el.LIBRETRO}/mame/scripts/mame.sh": MAME_SH,
f"{el.LIBRETRO}/mame/config/ini/mame.ini": MAME_INI,
f"{el.LIBRETRO}/mame/config/MAME/MAME.opt": MAME_OPT,
f"{EMU}/scummvmsa/bin/scummvm.start": SCUMMVM_START,
f"{EMU}/dolphinSA/scripts/dolphin.sh": DOLPHIN_SH,
}
def tree(**changes: str | None) -> RepoTree:
files = dict(FILES)
for path, content in changes.items():
if content is None:
files.pop(path, None)
else:
files[path] = content
return RepoTree.from_files(files)
def records(extractor, **changes: str | None) -> list[dict]:
built = tree(**changes)
return [r.entry() for r in extractor(built, el.Runs(built))]
def ref(path: str, lines: str) -> str:
return f"{path}:{lines}"
class Dispatch(unittest.TestCase):
def test_each_script_is_named_after_the_core_that_starts_it(self):
runs = el.Runs(tree())
self.assertEqual(runs.emulator("flycast.sh"), "flycastsa")
self.assertEqual(runs.emulator("flycastdojo.sh"), "flycast_dojo")
self.assertEqual(runs.emulator("m64p.sh"), "rice, glide64mk2")
self.assertEqual(runs.emulator("mame.sh"), "mame")
self.assertEqual(runs.emulator("freej2me.sh"), "freej2me")
def test_an_arm_without_a_core_test_names_its_platform(self):
self.assertEqual(el.Runs(tree()).emulator("pico8.sh"), "pico8")
def test_a_script_no_longer_started_raises(self):
runs = el.Runs(tree())
with self.assertRaises(LauncherPatternMissing):
runs.of("gone.sh")
def test_a_dispatch_without_any_script_raises(self):
with self.assertRaises(LauncherPatternMissing):
el.Runs(tree(**{el.RUN_EMU: "echo nothing\n"}))
class LibretroRecords(unittest.TestCase):
def test_retroarch_system_directory_comes_from_the_sed(self):
(record,) = records(el.retroarch)
self.assertEqual(record["search_dir"], "/storage/roms/bios")
self.assertEqual(record["kind"], "passes_path")
self.assertEqual(
record["source_ref"], f"{ref(el.RETROARCH_MK, '2')}, {ref(el.RUN_EMU, '80')}"
)
def test_retrorun_is_handed_the_folder_with_d(self):
(record,) = records(el.retrorun)
self.assertEqual(
(record["emulator"], record["search_dir"], record["passes_to"]),
("retrorun", "/storage/roms/bios", "retrorun -d"),
)
self.assertEqual(record["source_ref"], ref(el.RUN_EMU, "4, 81-82"))
def test_freej2me_gets_java_home_and_its_jar_copied(self):
java, jar = records(el.freej2me)
self.assertEqual(java["passes_to"], "environment JAVA_HOME")
self.assertEqual(java["search_dir"], "/storage/roms/bios/jdk")
self.assertEqual(jar["kind"], "copies")
self.assertEqual(jar["names"], ["freej2me-lr.jar"])
self.assertEqual(jar["passes_to"], "${HOME}/roms/bios")
self.assertEqual(jar["depends_on_option"], "/storage/roms/bios/jdk/eeversion exists")
def test_mame_copies_its_own_files_and_reads_only_path_options(self):
found = records(el.mame)
copies = [r for r in found if r["kind"] == "copies"]
self.assertEqual([r["names"] for r in copies], [["fm*"], ["mame*"]])
folders = [(r["passes_to"], r["search_dir"]) for r in found if r["kind"] == "passes_path"]
self.assertEqual(
folders,
[
("mame.ini rompath", "/storage/roms/bios/mame/bios"),
("mame.ini samplepath", "/storage/roms/bios/mame/samples"),
],
)
self.assertTrue(all(r["depends_on_option"] == "mame_read_config = enabled"
for r in found if r["kind"] == "passes_path"))
def test_mame_without_the_read_config_option_raises(self):
with self.assertRaises(LauncherPatternMissing):
records(el.mame, **{f"{el.LIBRETRO}/mame/config/MAME/MAME.opt": "x = 1\n"})
class StandaloneRecords(unittest.TestCase):
def test_pico8_moves_the_folder_and_runs_the_binary(self):
(record,) = records(el.pico8)
self.assertEqual(record["kind"], "moves")
self.assertEqual(record["names"], ["pico8_dyn"])
self.assertEqual(record["search_dir"], "/storage/roms/bios/pico-8")
self.assertEqual(record["passes_to"], "/emuelec/bin/pico-8")
self.assertEqual(
record["source_ref"],
f"{ref(el.RUN_EMU, '57-58')}, {ref(f'{el.BIN}/pico8.sh', '1-5, 8-10')}",
)
def test_pico8_without_the_removal_is_not_a_move(self):
changed = PICO8_SH.replace(" rm -rf /storage/roms/bios/pico-8\n", "")
with self.assertRaises(LauncherPatternMissing):
records(el.pico8, **{f"{el.BIN}/pico8.sh": changed})
def test_links_from_scripts_and_packages(self):
found = records(el.links)
self.assertEqual(
[(r["emulator"], r["search_dir"]) for r in found],
[
("flycastsa", "/storage/roms/bios/dc"),
("flycast_dojo", "/storage/roms/bios/dc_dojo"),
("amiberry", "/storage/roms/bios"),
("amiberry-lite", "/storage/roms/bios"),
("timidity", "/storage/roms/bios/timidity/freepats"),
],
)
self.assertEqual(
found[1]["source_ref"],
f"{ref(el.RUN_EMU, '26-27')}, "
f"{ref(f'{EMU}/flycast-dojo/scripts/flycastdojo.sh', '1-5')}",
)
self.assertEqual(
found[2]["passes_to"], "symlink /usr/config/amiberry/kickstarts -> /storage/roms/bios"
)
def test_a_script_without_a_bios_link_raises(self):
with self.assertRaises(LauncherPatternMissing):
records(el.links, **{f"{EMU}/flycastsa/scripts/flycast.sh": "flycast \"${1}\"\n"})
def test_duckstation_scans_the_folder_its_config_names(self):
(record,) = records(el.duckstation)
self.assertEqual((record["kind"], record["search_dir"]), ("scans_dir", "/storage/roms/bios"))
def test_duckstation_with_a_fixed_bios_path_raises(self):
changed = DUCKSTATION_INI.replace("PathPAL =", "PathPAL = /storage/roms/bios/scph5502.bin")
with self.assertRaises(LauncherPatternMissing):
records(el.duckstation, **{f"{EMU}/duckstation/config/settings.ini": changed})
def test_yabasanshiro_passes_the_bios_unless_hle_is_chosen(self):
new, old = records(el.yabasanshiro)
self.assertEqual((new["emulator"], old["emulator"]), ("yabasanshiroSA", "yabasanshiroSA1_5"))
self.assertEqual(new["names"], ["saturn_bios.bin"])
self.assertEqual(new["passes_to"], "yabasanshiro -b")
self.assertEqual(old["passes_to"], "yabasanshiro1_5 -b")
self.assertEqual(new["depends_on_option"], "hlebios != 1")
self.assertIs(new["fails_when_missing"], False)
def test_jzintv_is_handed_the_bios_folder(self):
(record,) = records(el.jzintv)
self.assertEqual((record["search_dir"], record["passes_to"]), ("/storage/roms/bios", "jzintv -p"))
def test_mupen64plus_tries_its_ipl_names_in_order(self):
(record,) = records(el.mupen64plus)
self.assertEqual(record["kind"], "names_searched")
self.assertEqual(record["names"], ["N64DD IPLROM (J).n64", "IPL.n64"])
self.assertEqual(record["search_dir"], "/storage/roms/bios/Mupen64plus")
self.assertEqual(record["passes_to"], "mupen64plus --dd-ipl-rom")
self.assertEqual(record["depends_on_option"], '-f "${GAMEDIR}/${GAMENAME}.ndd"')
def test_mupen64plus_without_a_fallback_raises(self):
changed = M64P_SH.replace('[ ! -f "${IPLROM}" ] && IPLROM="${BIOSDIR}/IPL.n64"\n', "")
with self.assertRaises(LauncherPatternMissing):
records(el.mupen64plus, **{f"{EMU}/mupen64plussa/mupen64plussa-core/m64p.sh": changed})
def test_hatari_reads_the_tos_named_by_its_default_config(self):
(record,) = records(el.hatari)
self.assertEqual(record["names"], ["tos.img"])
self.assertEqual(
record["passes_to"], "Atari-ST-default.cfg [ROM] szTosImageFileName (--configfile)"
)
def test_advancemame_one_record_when_every_device_agrees(self):
(record,) = records(el.advancemame)
self.assertEqual(record["search_dir"], "/storage/roms/bios/advmame/samples")
self.assertNotIn("device", record)
def test_advancemame_devices_that_disagree_raise(self):
changed = ADVMAME_RC.replace("bios/advmame", "bios/other")
with self.assertRaises(LauncherPatternMissing):
records(el.advancemame, **{f"{EMU}/advancemame/config/advmame.rc_oga": changed})
def test_scummvm_extrapath_and_the_ini_copied_for_the_core(self):
extra, ini = records(el.scummvm)
self.assertEqual(extra["passes_to"], "scummvm --extrapath")
self.assertEqual(extra["depends_on_option"], "/storage/.config/scummvm/extra exists")
self.assertEqual((ini["emulator"], ini["build"]), ("scummvm", "libretro"))
self.assertEqual(ini["search_dir"], "/storage/.config/scummvm")
self.assertEqual(ini["passes_to"], "/storage/roms/bios/scummvm.ini")
def test_dolphin_data_home(self):
(record,) = records(el.dolphin)
self.assertEqual(record["passes_to"], "environment XDG_DATA_HOME")
self.assertEqual(record["search_dir"], "/storage/roms/dolphin")
def test_midi_sources_that_read_the_bios_folder(self):
found = records(el.midi)
self.assertEqual(
[(r["emulator"], r["kind"], r["names"], r["search_dir"]) for r in found],
[
("fluidsynth", "fixed_name", ["sc/SC-55.sf2"], "/storage/roms/bios"),
("mt32d", "passes_path", [], "/roms/bios"),
],
)
self.assertEqual(found[0]["depends_on_option"], "ra_midi_output = fluidsynth")
self.assertEqual(
found[1]["source_ref"], f"{ref(el.RUN_EMU, '7-9')}, {ref(el.UTILS, '1, 10-11')}"
)
def test_every_extractor_runs_on_the_fixtures(self):
found = el.extract_launchers(tree())
self.assertEqual(len(found), 26)
class Revision(unittest.TestCase):
def test_the_revision_is_the_one_the_bios_list_is_read_at(self):
url = (
"https://raw.githubusercontent.com/EmuELEC/EmuELEC/v4.8/"
"packages/sx05re/emuelec/bin/batocera/batocera-systems"
)
self.assertEqual(launcher_revision(url), "v4.8")
def test_a_url_without_a_revision_raises(self):
with self.assertRaises(ValueError):
launcher_revision("https://example.org/batocera-systems")
class PlatformRecords(unittest.TestCase):
"""The launchers platforms/emuelec.yml carries, as the scraper wrote them."""
@classmethod
def setUpClass(cls) -> None:
cls.config = yaml.safe_load((REPO_ROOT / "platforms" / "emuelec.yml").read_text())
def test_the_platform_carries_launchers(self):
self.assertTrue(self.config.get("launchers"))
def test_every_reference_is_in_the_emuelec_packages(self):
for record in self.config["launchers"]:
for part in record["source_ref"].split(", "):
if ":" in part:
self.assertTrue(part.startswith(f"{el.SX05RE}/"), record)
if __name__ == "__main__":
unittest.main()
+49
View File
@@ -50,6 +50,55 @@ class TheGuideLeadsToTheFewestFiles(unittest.TestCase):
self.assertTrue(target.is_file() or (target / "index.html").is_file(), url)
class TheGuideDrivesTheInstaller(unittest.TestCase):
"""What an agent needs to run the installer for a user: the package
command, the plan as JSON, the exit codes, and plan, consent, act. Each
fact is held to install.py, so the guide cannot drift from it."""
def setUp(self) -> None:
import generate_installer_doc # noqa: PLC0415
self.doc = generate_installer_doc
self.text = siteagents.llms_txt([("retroarch", "RetroArch")])
self.section = self.text.split("\n## Driving the installer for a user\n", 1)[1].split("\n## ", 1)[0]
def test_the_exit_codes_are_the_installer_page_table(self) -> None:
page = (REPO_ROOT / "wiki" / "installer.md").read_text(encoding="utf-8")
table = page.split("<!-- generated: exit-codes -->", 1)[1].split("<!-- /generated: exit-codes -->")[0]
in_table = [int(code) for code in re.findall(r"^\| `(\d+)` \|", table, re.M)]
self.assertEqual(in_table, self.doc.exit_codes())
self.assertEqual([code for code, _ in siteagents.EXIT_CODES], in_table)
listed = [int(code) for code in re.findall(r"^ - `(\d+)`: ", self.section, re.M)]
self.assertEqual(listed, in_table)
def test_the_event_schema_is_the_installers(self) -> None:
import json # noqa: PLC0415
self.assertEqual(siteagents.EVENT_SCHEMA, self.doc._install.EVENT_SCHEMA)
schema = json.loads((REPO_ROOT / "schemas" / "install-events.schema.json").read_text(encoding="utf-8"))
self.assertEqual(schema["$id"], siteagents.SITE + siteagents.EVENTS_SCHEMA_PATH)
self.assertIn(f"event schema {siteagents.EVENT_SCHEMA} ", self.section)
def test_every_option_and_event_named_exists(self) -> None:
options = {flag for action in self.doc._install.build_parser()._actions
for flag in action.option_strings}
for flag in set(re.findall(r"(?<![\w-])(--[a-z][a-z-]*)", self.section)):
with self.subTest(flag=flag):
self.assertIn(flag, options)
keys = self.doc._install.EVENT_KEYS
self.assertIn("state", keys["file"])
self.assertIn("bytes_needed", keys["check"])
self.assertIn("pending", keys["summary"])
self.assertEqual(tuple(keys["end"]), ("code",))
def test_the_package_commands_and_the_pattern(self) -> None:
self.assertIn("`uvx retrobios --for-games --required-only --dry-run`", self.section)
self.assertIn("`pipx run retrobios`", self.section)
self.assertIn("`--dry-run --events`", self.section)
steps = re.findall(r"^\d\. (\w+)", self.section, re.M)
self.assertEqual(steps, ["Plan", "Consent", "Act"])
class TheSkillIsAnAgentSkill(unittest.TestCase):
"""ARD reserves text/markdown for agent skills (Lighthouse warns on a
plain Markdown entry): the setup steps are published as one."""
+1
View File
@@ -41,6 +41,7 @@ START_PAGES = {
# Reference pages written for anyone, held to the Start here budgets.
START_LEVEL_PAGES = {
"wiki/measurements.md": 300,
"wiki/archive-recipes.md": 160,
}
# Level 1: the guide part of each setup page, above its Reference heading.
SETUP_CEILING = 220
+2 -1
View File
@@ -956,7 +956,8 @@ class EventsTranscript(unittest.TestCase):
self.assertEqual(list(event), ["event", *keys[event["event"]]])
self.assertEqual(events[0]["event"], "start")
self.assertEqual(events[0]["mode"], "check")
self.assertEqual(events[-1], {"event": "end", "code": 1})
# --check that finds files missing ends on 3 (the Exit status table).
self.assertEqual(events[-1], {"event": "end", "code": 3})
class OneOptionsTable(unittest.TestCase):