mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-01 00:27:20 -05:00
feat: refuse placeholder versions in the schema
core_version is a string the code states, null or keyed by build mode, and the schema refuses Git, SVN, unknown, master, main, latest, dev and HEAD, alone or annotated. version_basis is defined per kind with the fields each one needs, and kind none requires a null version. tests/test_version_basis.py reads a recorded fixture of forge answers offline and fails on drift, holds every profile to a basis (tag versions in describe form, aliases equal to their profile, none as null), checks that a pin advance reads the version again or is refused, and that the schema refuses placeholders.
This commit is contained in:
1 parent
3199bc2a55
commit
91b7bd94d0
7 files changed
+708
-8
No files matched your search
@@ -63,6 +63,33 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "A profile whose code states no version says so with version_basis kind none and a null core_version, never a placeholder.",
|
||||
"if": {
|
||||
"properties": {
|
||||
"version_basis": {
|
||||
"properties": {
|
||||
"kind": {
|
||||
"const": "none"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"kind"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"version_basis"
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"core_version": {
|
||||
"type": "null"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
@@ -155,11 +182,50 @@
|
||||
]
|
||||
},
|
||||
"core_version": {
|
||||
"type": [
|
||||
"string",
|
||||
"number",
|
||||
"object",
|
||||
"null"
|
||||
"description": "The version of the code at the pin, read where version_basis says: the string the program states, git describe's form of the nearest tag, or the package read. Never a branch or VCS name (Git, SVN, master, latest, dev, unknown). null when version_basis is kind none. An object keys it by build mode, like source.",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "^(?!([Gg][Ii][Tt]|[Ss][Vv][Nn]|[Uu][Nn][Kk][Nn][Oo][Ww][Nn]|[Mm][Aa][Ss][Tt][Ee][Rr]|[Mm][Aa][Ii][Nn]|[Ll][Aa][Tt][Ee][Ss][Tt]|[Dd][Ee][Vv]|[Hh][Ee][Aa][Dd])([\\s(]|$)).*\\S"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"minProperties": 1,
|
||||
"additionalProperties": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "^(?!([Gg][Ii][Tt]|[Ss][Vv][Nn]|[Uu][Nn][Kk][Nn][Oo][Ww][Nn]|[Mm][Aa][Ss][Tt][Ee][Rr]|[Mm][Aa][Ii][Nn]|[Ll][Aa][Tt][Ee][Ss][Tt]|[Dd][Ee][Vv]|[Hh][Ee][Aa][Dd])([\\s(]|$)).*\\S"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"version_basis": {
|
||||
"description": "Where core_version is read, so a tool reads it again at any pin. An object keyed by build mode when core_version is.",
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/$defs/version_basis"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"minProperties": 1,
|
||||
"not": {
|
||||
"required": [
|
||||
"kind"
|
||||
]
|
||||
},
|
||||
"additionalProperties": {
|
||||
"$ref": "#/$defs/version_basis"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"alias_of": {
|
||||
@@ -963,5 +1029,188 @@
|
||||
"type": "string",
|
||||
"description": "Repository carrying the same tree, consulted after source and upstream. The only thing that keeps a profile checkable once its own forge is withdrawn or its host stops resolving."
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"version_basis": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"kind"
|
||||
],
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"file",
|
||||
"tag",
|
||||
"alias",
|
||||
"package",
|
||||
"none",
|
||||
"unverified"
|
||||
],
|
||||
"description": "file: a pattern in one file at the pin. tag: the nearest tag in the pin's history. alias: the version of the profile it aliases. package: an artefact read outside any source tree. none: the code at the pin states no version and no tag reaches it. unverified: no reading could be made, for the reason why gives. core_version stays as written, unconfirmed."
|
||||
},
|
||||
"why": {
|
||||
"enum": [
|
||||
"no_pin",
|
||||
"host_unsupported",
|
||||
"host_gone",
|
||||
"host_unreachable",
|
||||
"pin_off_branch",
|
||||
"no_artifact",
|
||||
"no_source"
|
||||
],
|
||||
"description": "Why a basis is unverified: no pinned commit, a host the tools cannot read, a forge gone, a forge that did not answer, a pin on another branch than the build the profile describes, no build or package named as read, no source declared."
|
||||
},
|
||||
"repo": {
|
||||
"enum": [
|
||||
"source",
|
||||
"upstream",
|
||||
"source_mirror"
|
||||
],
|
||||
"description": "The declared repository the version describes, pinned by source_commit (source, source_mirror) or upstream_commit. Default source."
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"description": "kind file: a repository outside the profile's own, read at commit (the package file of a build a platform ships). kind package: where the artefact was obtained."
|
||||
},
|
||||
"commit": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-f]{7,40}$",
|
||||
"description": "The revision of url the file is read at."
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "File read at the pin."
|
||||
},
|
||||
"pattern": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "Regular expression, multiline, whose groups hold the version."
|
||||
},
|
||||
"format": {
|
||||
"type": "string",
|
||||
"pattern": "\\{\\d+",
|
||||
"description": "How the groups are laid out, {1}.{2} or {4:02d}. Default: group 1."
|
||||
},
|
||||
"match": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "Glob a tag must fit to count, as git describe --match takes it."
|
||||
},
|
||||
"tag": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "The nearest tag in the pin's history."
|
||||
},
|
||||
"distance": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "Commits the pin has that the tag lacks."
|
||||
},
|
||||
"of": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "The profile this one aliases."
|
||||
},
|
||||
"artifact": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "The package, binary or archive the version was read from."
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"kind": {
|
||||
"const": "file"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": [
|
||||
"path",
|
||||
"pattern"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"kind": {
|
||||
"const": "tag"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": [
|
||||
"tag",
|
||||
"distance"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"kind": {
|
||||
"const": "alias"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": [
|
||||
"of"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"kind": {
|
||||
"const": "package"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": [
|
||||
"artifact"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"kind": {
|
||||
"const": "file"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"url"
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
"required": [
|
||||
"commit"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"kind": {
|
||||
"const": "unverified"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": [
|
||||
"why"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7799,6 +7799,7 @@ _UNVERIFIED = {
|
||||
"host_unsupported": "its host cannot be read",
|
||||
"host_gone": "its source is gone",
|
||||
"host_unreachable": "its host did not answer",
|
||||
"pin_off_branch": "its recorded revision is not the build described",
|
||||
"no_artifact": "no build recorded as read",
|
||||
"no_source": "no source declared",
|
||||
}
|
||||
|
||||
@@ -2736,7 +2736,8 @@ def set_version_fields(text: str, version: object, basis: dict | None) -> str:
|
||||
if span is None:
|
||||
anchor = _top_block(lines, "upstream_commit") or _top_block(lines, "source_commit") \
|
||||
or _top_block(lines, "profiled_date") or _top_block(lines, "upstream") \
|
||||
or _top_block(lines, "source")
|
||||
or _top_block(lines, "source") or _top_block(lines, "alias_of") \
|
||||
or _top_block(lines, "type")
|
||||
if anchor is None:
|
||||
raise YamlWriteError("core_version: no line to write it after")
|
||||
lines[anchor[1]:anchor[1]] = rendered
|
||||
|
||||
@@ -93,7 +93,11 @@ def _by_mode(profile: dict, field: str, mode: str) -> str | None:
|
||||
|
||||
|
||||
def is_placeholder(value: object) -> bool:
|
||||
return isinstance(value, str) and value.strip().casefold() in PLACEHOLDERS
|
||||
"""A VCS or branch word standing for a version, alone or annotated
|
||||
("SVN (2015 snapshot)"). The schema refuses the same strings."""
|
||||
if not isinstance(value, str):
|
||||
return False
|
||||
return re.split(r"[\s(]", value.strip(), maxsplit=1)[0].casefold() in PLACEHOLDERS
|
||||
|
||||
|
||||
def format_version(match: re.Match, template: str | None) -> str:
|
||||
|
||||
Vendored
+94
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"api": {
|
||||
"https://api.github.com/repos/libretro/dolphin/compare/f84df02055ab9610feec48e65648cac5a3c098fa...3aac3f052f733f8df2f1616843a4d2e14002ed11": {
|
||||
"ahead_by": 437,
|
||||
"behind_by": 0,
|
||||
"status": "ahead"
|
||||
},
|
||||
"https://api.github.com/repos/libretro/dolphin/git/ref/tags/2609": {
|
||||
"object": {
|
||||
"sha": "b134d026ab4e71df13b3657ea6787d499962664b",
|
||||
"type": "tag"
|
||||
}
|
||||
},
|
||||
"https://api.github.com/repos/libretro/dolphin/git/tags/b134d026ab4e71df13b3657ea6787d499962664b": {
|
||||
"object": {
|
||||
"sha": "f84df02055ab9610feec48e65648cac5a3c098fa",
|
||||
"type": "commit"
|
||||
}
|
||||
}
|
||||
},
|
||||
"files": {
|
||||
"github.com/libretro/FBNeo/32958a89abd645ed20387c8d31d21ede3bc48161/src/burn/version.h": "// Version number, written as vV.V.BB or vV.V.BBaa\n// (0xVVBBaa, in BCD notation)\n\n#define VER_MAJOR 1\n#define VER_MINOR 0\n#define VER_BETA 0\n#define VER_ALPHA 3\n\n#define BURN_VERSION (VER_MAJOR * 0x100000) + (VER_MINOR * 0x010000) + (((VER_BETA / 10) * 0x001000) + ((VER_BETA % 10) * 0x000100)) + (((VER_ALPHA / 10) * 0x000010) + (VER_ALPHA % 10))\n\n\n\n\n",
|
||||
"github.com/libretro/snes9x/fae2fea08f74180759ef540ee94259213f503480/snes9x.h": "/*****************************************************************************\\\n Snes9x - Portable Super Nintendo Entertainment System (TM) emulator.\n This file is licensed under the Snes9x License.\n For further information, consult the LICENSE file in the root directory.\n\\*****************************************************************************/\n\n#ifndef _SNES9X_H_\n#define _SNES9X_H_\n\n#ifndef VERSION\n#define VERSION\t\"1.63\"\n#endif\n\n#include \"port.h\"\n#include \"65c816.h\"\n#include \"messages.h\"\n\n/* All core file I/O goes through the libretro VFS via the\n file_stream_transforms wrappers (RFILE). The frontend supplies the\n VFS interface when available (see retro_set_environment);\n filestream falls back to its local implementation otherwise.\n Compressed (.gz) stream support from the old ZLIB build is gone --\n under libretro the frontend handles compressed content itself. */\n#include <streams/file_stream_transforms.h>\n#define FSTREAM\t\t\t\t\tRFILE *\n#define READ_FSTREAM(p, l, s)\trfread(p, 1, l, s)\n#define WRITE_FSTREAM(p, l, s)\trfwrite(p, 1, l, s)\n#define GETS_FSTREAM(p, l, s)\trfgets(p, l, s)\n#define GETC_FSTREAM(s)\t\t\trfgetc(s)\n#define OPEN_FSTREAM(f, m)\t\trfopen(f, m)\n#define FIND_FSTREAM(s)\t\t\trftell(s)\n#define REVERT_FSTREAM(s, o, p)\trfseek(s, o, p)\n#define CLOSE_FSTREAM(s)\t\t\trfclose(s)\n\n\n#define SNES_WIDTH\t\t\t\t\t256\n#define SNES_HEIGHT\t\t\t\t\t224\n#define SNES_HEIGHT_EXTENDED\t\t239\n#define MAX_SNES_WIDTH\t\t\t\t(SNES_WIDTH * 2)\n#define MAX_SNES_HEIGHT\t\t\t\t(SNES_HEIGHT_EXTENDED * 2)\n\n#define\tNTSC_MASTER_CLOCK\t\t\t21477272.727272 // 21477272 + 8/11 exact\n#define\tPAL_MASTER_CLOCK\t\t\t21281370.0\n#define NTSC_PROGRESSIVE_FRAME_RATE\t60.09881389744051\n#define NTSC_INTERLACED_FRAME_RATE\t59.94005994\n#define PAL_PROGRESSIVE_FRAME_RATE\t50.006977968\n\n\n#define SNES_MAX_NTSC_VCOUNTER\t\t262\n#define SNES_MAX_PAL_VCOUNTER\t\t312\n#define SNES_HCOUNTER_MAX\t\t\t341\n\n#ifndef ALLOW_CPU_OVERCLOCK\n#define ONE_CYCLE\t\t\t\t\t6\n#define SLOW_ONE_CYCLE\t\t\t\t8\n#define TWO_CYCLES\t\t\t\t\t12\n#else\n#define ONE_CYCLE (Settings.OneClockCycle)\n#define SLOW_ONE_CYCLE (Settings.OneSlowClockCycle)\n#define TWO_CYCLES (Settings.TwoClockCycles)\n#endif\n#define\tONE_DOT_CYCLE\t\t\t\t4\n\n#define SNES_CYCLES_PER_SCANLINE\t(SNES_HCOUNTER_MAX * ONE_DOT_CYCLE)\n#define SNES_SCANLINE_TIME\t\t\t(SNES_CYCLES_PER_SCANLINE / NTSC_MASTER_CLOCK)\n\n#define SNES_WRAM_REFRESH_HC_v1\t\t530\n#define SNES_WRAM_REFRESH_HC_v2\t\t538\n#define SNES_WRAM_REFRESH_CYCLES\t40\n\n#define SNES_HBLANK_START_HC\t\t1096\t\t\t\t\t// H=274\n#define\tSNES_HDMA_START_HC\t\t\t1106\t\t\t\t\t// FIXME: not true\n#define\tSNES_HBLANK_END_HC\t\t\t4\t\t\t\t\t\t// H=1\n#define\tSNES_HDMA_INIT_HC\t\t\t20\t\t\t\t\t\t// FIXME: not true\n#define\tSNES_RENDER_START_HC\t\t(128 * ONE_DOT_CYCLE)\t// FIXME: Snes9x renders a line at a time.\n\n#define SNES_TR_MASK\t\t(1 << 4)\n#define SNES_TL_MASK\t\t(1 << 5)\n#define SNES_X_MASK\t\t\t(1 << 6)\n#define SNES_A_MASK\t\t\t(1 << 7)\n#define SNES_RIGHT_MASK\t\t(1 << 8)\n#define SNES_LEFT_MASK\t\t(1 << 9)\n#define SNES_DOWN_MASK\t\t(1 << 10)\n#define SNES_UP_MASK\t\t(1 << 11)\n#define SNES_START_MASK\t\t(1 << 12)\n#define SNES_SELECT_MASK\t(1 << 13)\n#define SNES_Y_MASK\t\t\t(1 << 14)\n#define SNES_B_MASK\t\t\t(1 << 15)\n\n#define DEBUG_MODE_FLAG\t\t(1 << 0)\t// debugger\n#define TRACE_FLAG\t\t\t(1 << 1)\t// debugger\n#define SINGLE_STEP_FLAG\t(1 << 2)\t// debugger\n#define BREAK_FLAG\t\t\t(1 << 3)\t// debugger\n#define SCAN_KEYS_FLAG\t\t(1 << 4)\t// CPU\n#define HALTED_FLAG\t\t\t(1 << 12)\t// APU\n#define FRAME_ADVANCE_FLAG\t(1 << 9)\n\n#define ROM_NAME_LEN\t23\n#define AUTO_FRAMERATE\t200\n\nstruct SCPUState\n{\n\tuint32\tFlags;\n\tint32\tCycles;\n\tint32\tPrevCycles;\n\tint32\tV_Counter;\n\tuint8\t*PCBase;\n\tbool8\tNMIPending;\n\tbool8\tIRQLine;\n\tbool8\tIRQTransition;\n\tbool8\tIRQLastState;\n\tbool8\tIRQExternal;\n\tint32\tIRQPending;\n\tint32\tMemSpeed;\n\tint32\tMemSpeedx2;\n\tint32\tFastROMSpeed;\n\tbool8\tInDMA;\n\tbool8\tInHDMA;\n\tbool8\tInDMAorHDMA;\n\tbool8\tInWRAMDMAorHDMA;\n\tuint8\tHDMARanInDMA;\n\tint32\tCurrentDMAorHDMAChannel;\n\tuint8\tWhichEvent;\n\tint32\tNextEvent;\n\tbool8\tWaitingForInterrupt;\n\tuint32\tAutoSaveTimer;\n\tbool8\tSRAMModified;\n};\n\nenum\n{\n\tHC_HBLANK_START_EVENT = 1,\n\tHC_HDMA_START_EVENT = 2,\n\tHC_HCOUNTER_MAX_EVENT = 3,\n\tHC_HDMA_INIT_EVENT = 4,\n\tHC_RENDER_EVENT = 5,\n\tHC_WRAM_REFRESH_EVENT = 6\n};\n\nenum\n{\n\tIRQ_NONE = 0x0,\n\tIRQ_SET_FLAG = 0x1,\n\tIRQ_CLEAR_FLAG = 0x2,\n\tIRQ_TRIGGER_NMI = 0x4\n};\n\nstruct STimings\n{\n\tint32\tH_Max_Master;\n\tint32\tH_Max;\n\tint32\tV_Max_Master;\n\tint32\tV_Max;\n\tint32\tHBlankStart;\n\tint32\tHBlankEnd;\n\tint32\tHDMAInit;\n\tint32\tHDMAStart;\n\tint32\tNMITriggerPos;\n\tint32\tNextIRQTimer;\n\tinLine truncated
|
||||
},
|
||||
"profiles": {
|
||||
"anarch": {
|
||||
"core_version": null,
|
||||
"emulator": "Anarch",
|
||||
"type": "libretro",
|
||||
"version_basis": {
|
||||
"kind": "none"
|
||||
}
|
||||
},
|
||||
"beetle_psx": {
|
||||
"core_version": "v0.9.44.1",
|
||||
"emulator": "Beetle PSX",
|
||||
"type": "libretro",
|
||||
"version_basis": {
|
||||
"artifact": "mednafen_psx_libretro v0.9.44.1.zip",
|
||||
"kind": "package",
|
||||
"pattern": "libretro (v[0-9.]+)\\.zip"
|
||||
}
|
||||
},
|
||||
"dolphin": {
|
||||
"core_version": "2609-437-g3aac3f0",
|
||||
"emulator": "Dolphin",
|
||||
"source": "https://github.com/libretro/dolphin",
|
||||
"source_commit": "3aac3f052f733f8df2f1616843a4d2e14002ed11",
|
||||
"type": "standalone + libretro",
|
||||
"upstream": "https://github.com/dolphin-emu/dolphin",
|
||||
"version_basis": {
|
||||
"distance": 437,
|
||||
"kind": "tag",
|
||||
"tag": "2609"
|
||||
}
|
||||
},
|
||||
"fbneo": {
|
||||
"core_version": "v1.0.0.03",
|
||||
"emulator": "FinalBurn Neo",
|
||||
"source": "https://github.com/libretro/FBNeo",
|
||||
"source_commit": "32958a89abd645ed20387c8d31d21ede3bc48161",
|
||||
"type": "libretro",
|
||||
"version_basis": {
|
||||
"format": "v{1}.{2}.{3}.{4:02d}",
|
||||
"kind": "file",
|
||||
"path": "src/burn/version.h",
|
||||
"pattern": "VER_MAJOR\\s+(\\d+)\\s+#define\\s+VER_MINOR\\s+(\\d+)\\s+#define\\s+VER_BETA\\s+(\\d+)\\s+#define\\s+VER_ALPHA\\s+(\\d+)"
|
||||
}
|
||||
},
|
||||
"mednafen_psx": {
|
||||
"alias_of": "beetle_psx",
|
||||
"core_version": "v0.9.44.1",
|
||||
"emulator": "Beetle PSX",
|
||||
"type": "alias",
|
||||
"version_basis": {
|
||||
"kind": "alias",
|
||||
"of": "beetle_psx"
|
||||
}
|
||||
},
|
||||
"snes9x": {
|
||||
"core_version": "1.63",
|
||||
"emulator": "Snes9x",
|
||||
"source": "https://github.com/libretro/snes9x",
|
||||
"source_commit": "fae2fea08f74180759ef540ee94259213f503480",
|
||||
"type": "libretro",
|
||||
"upstream": "https://github.com/snes9xgit/snes9x",
|
||||
"version_basis": {
|
||||
"kind": "file",
|
||||
"path": "snes9x.h",
|
||||
"pattern": "#define\\s+VERSION\\s+\"([^\"]+)\""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
"""core_version is read from the code at the pin, where version_basis says.
|
||||
|
||||
Four bulk pin advances moved 295 pins and no version, and 54 profiles said
|
||||
"Git" or "SVN" where the code states a release. These tests read the
|
||||
recorded sources offline from a fixture of forge answers, hold every profile
|
||||
to a basis, and hold a pin advance to reading the version again.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import copy
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
import yaml
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "scripts"))
|
||||
|
||||
import profile_sync # noqa: E402
|
||||
import upstream # noqa: E402
|
||||
import version_basis # noqa: E402
|
||||
from common import load_emulator_profiles # noqa: E402
|
||||
from profile_sync import ProfileReport, bump_commit # noqa: E402
|
||||
|
||||
FIXTURE = ROOT / "tests" / "fixtures" / "version_basis.json"
|
||||
|
||||
|
||||
def _fixture_cache(directory: Path) -> dict:
|
||||
"""The fixture's forge answers, laid out as upstream caches them."""
|
||||
data = json.loads(FIXTURE.read_text(encoding="utf-8"))
|
||||
for url, payload in data["api"].items():
|
||||
target = directory / "_api" / f"{hashlib.sha256(url.encode()).hexdigest()}.json"
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_text(upstream.ABSENT if payload is None else json.dumps(payload),
|
||||
encoding="utf-8")
|
||||
for key, text in data["files"].items():
|
||||
host, owner, name, sha, path = key.split("/", 4)
|
||||
repo = upstream.make_repo(host, owner, name)
|
||||
target = upstream.cache_path(str(directory), repo, sha, path)
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_text(upstream.ABSENT if text is None else text, encoding="utf-8")
|
||||
return data["profiles"]
|
||||
|
||||
|
||||
class ReadsTheRecordedSourceOffline(unittest.TestCase):
|
||||
"""The check needs nothing the cache does not hold."""
|
||||
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.cache = Path(self.tmp.name) / "cache"
|
||||
self.profiles = _fixture_cache(self.cache)
|
||||
|
||||
def tearDown(self):
|
||||
self.tmp.cleanup()
|
||||
|
||||
def _check(self, name: str, profiles: dict | None = None) -> list:
|
||||
profiles = profiles or self.profiles
|
||||
return version_basis.check(profiles[name], str(self.cache), True, profiles)
|
||||
|
||||
def test_every_kind_reads_its_version(self):
|
||||
for name in self.profiles:
|
||||
with self.subTest(profile=name):
|
||||
readings = self._check(name)
|
||||
self.assertEqual([r.status for r in readings], [version_basis.MATCH],
|
||||
readings)
|
||||
|
||||
def test_a_file_basis_names_the_line_it_read(self):
|
||||
(reading,) = self._check("snes9x")
|
||||
self.assertEqual(reading.read, "1.63")
|
||||
self.assertTrue(reading.where.endswith("snes9x.h:11"), reading.where)
|
||||
|
||||
def test_groups_are_laid_out_by_the_format(self):
|
||||
(reading,) = self._check("fbneo")
|
||||
self.assertEqual(reading.read, "v1.0.0.03")
|
||||
|
||||
def test_a_version_the_code_does_not_state_is_drift(self):
|
||||
for name, stale in (("snes9x", "1.61"), ("fbneo", "v1.0.0.2"),
|
||||
("dolphin", "2609"), ("mednafen_psx", "v0.9.44"),
|
||||
("anarch", "Git")):
|
||||
with self.subTest(profile=name):
|
||||
profiles = copy.deepcopy(self.profiles)
|
||||
profiles[name]["core_version"] = stale
|
||||
(reading,) = self._check(name, profiles)
|
||||
self.assertEqual(reading.status, version_basis.DRIFT)
|
||||
|
||||
def test_a_tag_distance_the_forge_contradicts_is_drift(self):
|
||||
profiles = copy.deepcopy(self.profiles)
|
||||
profiles["dolphin"]["version_basis"]["distance"] = 400
|
||||
(reading,) = self._check("dolphin", profiles)
|
||||
self.assertEqual(reading.status, version_basis.DRIFT)
|
||||
self.assertEqual(dict(reading.updates)["distance"], 437)
|
||||
|
||||
def test_a_pattern_that_no_longer_matches_is_unreadable(self):
|
||||
profiles = copy.deepcopy(self.profiles)
|
||||
profiles["snes9x"]["version_basis"]["pattern"] = "#define\\s+NOPE\\s+(\\S+)"
|
||||
(reading,) = self._check("snes9x", profiles)
|
||||
self.assertEqual(reading.status, version_basis.UNREADABLE)
|
||||
|
||||
def test_a_profile_without_basis_is_reported(self):
|
||||
profiles = copy.deepcopy(self.profiles)
|
||||
del profiles["snes9x"]["version_basis"]
|
||||
(reading,) = self._check("snes9x", profiles)
|
||||
self.assertEqual(reading.status, version_basis.NO_BASIS)
|
||||
|
||||
def test_the_command_fails_on_drift_and_passes_clean(self):
|
||||
emulators = Path(self.tmp.name) / "emulators"
|
||||
emulators.mkdir()
|
||||
for name, profile in self.profiles.items():
|
||||
body = dict(profile, files=[] if profile["type"] != "alias" else None)
|
||||
body = {k: v for k, v in body.items() if v is not None or k == "core_version"}
|
||||
(emulators / f"{name}.yml").write_text(yaml.safe_dump(body), encoding="utf-8")
|
||||
argv = ["profile_sync.py", "--all", "--verify-version", "--offline",
|
||||
"--emulators-dir", str(emulators), "--cache-dir", str(self.cache)]
|
||||
|
||||
def run() -> int:
|
||||
out, err = io.StringIO(), io.StringIO()
|
||||
with mock.patch.object(sys, "argv", argv), contextlib.redirect_stdout(out), \
|
||||
contextlib.redirect_stderr(err):
|
||||
try:
|
||||
profile_sync.main()
|
||||
except SystemExit as exc:
|
||||
return int(exc.code or 0)
|
||||
return 0
|
||||
|
||||
import common # noqa: PLC0415
|
||||
|
||||
common._emulator_profiles_cache.clear()
|
||||
self.assertEqual(run(), 0)
|
||||
stale = emulators / "snes9x.yml"
|
||||
stale.write_text(stale.read_text(encoding="utf-8").replace("1.63", "1.61"),
|
||||
encoding="utf-8")
|
||||
common._emulator_profiles_cache.clear()
|
||||
self.assertEqual(run(), 1)
|
||||
|
||||
|
||||
class EveryProfileSaysWhereItsVersionIsRead(unittest.TestCase):
|
||||
"""What can be held without a forge is held here, for every profile."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.profiles = load_emulator_profiles(str(ROOT / "emulators"), skip_aliases=False)
|
||||
|
||||
def test_every_profile_has_a_basis(self):
|
||||
missing = sorted(name for name, p in self.profiles.items()
|
||||
if not version_basis.bases(p))
|
||||
self.assertEqual(missing, [], "profiles without version_basis")
|
||||
|
||||
def test_no_placeholder_is_a_version(self):
|
||||
named = sorted(
|
||||
name for name, p in self.profiles.items()
|
||||
for mode in (version_basis.bases(p) or {"": {}})
|
||||
if version_basis.is_placeholder(version_basis.declared(p, mode))
|
||||
)
|
||||
self.assertEqual(named, [])
|
||||
|
||||
def test_keyed_bases_follow_the_keyed_version(self):
|
||||
for name, profile in self.profiles.items():
|
||||
found = version_basis.bases(profile)
|
||||
if not found or "" in found:
|
||||
continue
|
||||
with self.subTest(profile=name):
|
||||
self.assertIsInstance(profile.get("core_version"), dict)
|
||||
self.assertEqual(set(found), set(profile["core_version"]))
|
||||
|
||||
def test_none_states_no_version(self):
|
||||
for name, profile in self.profiles.items():
|
||||
for mode, basis in version_basis.bases(profile).items():
|
||||
if basis.get("kind") == "none":
|
||||
with self.subTest(profile=name, mode=mode):
|
||||
self.assertIsNone(version_basis.declared(profile, mode))
|
||||
|
||||
def test_a_tag_version_is_the_describe_form_of_its_pin(self):
|
||||
for name, profile in self.profiles.items():
|
||||
for mode, basis in version_basis.bases(profile).items():
|
||||
if basis.get("kind") != "tag" or basis.get("url"):
|
||||
continue
|
||||
field = version_basis.PIN_FIELDS[str(basis.get("repo") or "source")]
|
||||
pin = version_basis._by_mode(profile, field, mode)
|
||||
if not pin:
|
||||
continue
|
||||
with self.subTest(profile=name, mode=mode):
|
||||
self.assertEqual(
|
||||
version_basis.declared(profile, mode),
|
||||
version_basis.describe_form(basis["tag"], basis["distance"], pin),
|
||||
)
|
||||
|
||||
def test_an_alias_carries_the_version_of_its_profile(self):
|
||||
for name, profile in self.profiles.items():
|
||||
for mode, basis in version_basis.bases(profile).items():
|
||||
if basis.get("kind") != "alias":
|
||||
continue
|
||||
with self.subTest(profile=name):
|
||||
(reading,) = [r for r in version_basis.check(
|
||||
profile, "", True, self.profiles) if r.mode == mode]
|
||||
self.assertEqual(reading.status, version_basis.MATCH, reading)
|
||||
|
||||
def test_a_package_named_after_its_version_states_it(self):
|
||||
for name, profile in self.profiles.items():
|
||||
for mode, basis in version_basis.bases(profile).items():
|
||||
if basis.get("kind") != "package" or not basis.get("pattern"):
|
||||
continue
|
||||
with self.subTest(profile=name):
|
||||
(reading,) = [r for r in version_basis.check(profile, "", True)
|
||||
if r.mode == mode]
|
||||
self.assertEqual(reading.status, version_basis.MATCH, reading)
|
||||
|
||||
|
||||
SAMPLE = """emulator: Sample
|
||||
type: libretro
|
||||
source: "https://github.com/o/n"
|
||||
profiled_date: "2026-01-01"
|
||||
source_commit: "aaaaaaa"
|
||||
core_version: "1.0"
|
||||
version_basis:
|
||||
kind: file
|
||||
path: "version.h"
|
||||
pattern: '#define\\s+VERSION\\s+"([^"]+)"'
|
||||
files: []
|
||||
"""
|
||||
|
||||
|
||||
class AnAdvanceReadsTheVersionAgain(unittest.TestCase):
|
||||
"""A pin never moves without its version: read again, or refused."""
|
||||
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.path = Path(self.tmp.name) / "p.yml"
|
||||
self.path.write_text(SAMPLE, encoding="utf-8")
|
||||
self.report = ProfileReport(
|
||||
name="p", repo="o/n", pin="aaaaaaa", head="bbbbbbb", entries=[], counts={},
|
||||
skipped=profile_sync.NO_SOURCE_REF,
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
self.tmp.cleanup()
|
||||
|
||||
def _reading(self, status: str, value: str | None):
|
||||
return version_basis.Reading("", "file", "1.0", value, "o/n@bbbbbbb:version.h:3", status)
|
||||
|
||||
def test_the_version_moves_with_the_pin(self):
|
||||
seen = []
|
||||
|
||||
def check(profile, cache_dir, offline, profiles=None, recompute=False):
|
||||
seen.append((profile["source_commit"], recompute))
|
||||
return [self._reading(version_basis.DRIFT, "2.0")]
|
||||
|
||||
with mock.patch.object(version_basis, "check", check):
|
||||
self.assertTrue(bump_commit(self.path, self.report))
|
||||
written = yaml.safe_load(self.path.read_text(encoding="utf-8"))
|
||||
self.assertEqual(written["source_commit"], "bbbbbbb")
|
||||
self.assertEqual(written["core_version"], "2.0")
|
||||
# Read at the new pin, the nearest tag asked again.
|
||||
self.assertEqual(seen, [("bbbbbbb", True)])
|
||||
|
||||
def test_an_unreadable_version_holds_the_pin(self):
|
||||
with mock.patch.object(version_basis, "check",
|
||||
return_value=[self._reading(version_basis.UNREADABLE, None)]):
|
||||
self.assertFalse(bump_commit(self.path, self.report))
|
||||
self.assertEqual(self.path.read_text(encoding="utf-8"), SAMPLE)
|
||||
|
||||
def test_a_profile_without_basis_is_not_advanced(self):
|
||||
self.path.write_text(SAMPLE.split("version_basis:")[0] + "files: []\n", encoding="utf-8")
|
||||
before = self.path.read_text(encoding="utf-8")
|
||||
self.assertFalse(bump_commit(self.path, self.report))
|
||||
self.assertEqual(self.path.read_text(encoding="utf-8"), before)
|
||||
|
||||
def test_a_package_version_is_advanced_by_hand(self):
|
||||
reading = version_basis.Reading("", "package", "1.0", None, "read from x.apk",
|
||||
version_basis.UNCHECKED)
|
||||
with mock.patch.object(version_basis, "check", return_value=[reading]):
|
||||
self.assertFalse(bump_commit(self.path, self.report))
|
||||
|
||||
|
||||
class TheWriterKeepsTheDocument(unittest.TestCase):
|
||||
def test_keyed_versions_and_bases_round_trip(self):
|
||||
text = ('emulator: X\nsource_commit: "abc"\ncore_version:\n standalone: "1"\n'
|
||||
' libretro: "2"\ndisplay_name: X\n')
|
||||
basis = {"standalone": {"kind": "tag", "tag": "2407", "distance": 0},
|
||||
"libretro": {"kind": "none"}}
|
||||
written = profile_sync.set_version_fields(
|
||||
text, {"standalone": "2407", "libretro": None}, basis)
|
||||
document = yaml.safe_load(written)
|
||||
self.assertEqual(document["core_version"], {"standalone": "2407", "libretro": None})
|
||||
self.assertEqual(document["version_basis"], basis)
|
||||
self.assertEqual(document["display_name"], "X")
|
||||
self.assertIn('tag: "2407"', written)
|
||||
|
||||
def test_a_missing_version_is_written_after_the_pins(self):
|
||||
text = 'emulator: X\nsource_commit: "abc"\nfiles: []\n'
|
||||
written = profile_sync.set_version_fields(text, None, {"kind": "none"})
|
||||
self.assertEqual(written.splitlines()[2:4], ["core_version: null", "version_basis:"])
|
||||
|
||||
|
||||
class TheSchemaRefusesPlaceholders(unittest.TestCase):
|
||||
def setUp(self):
|
||||
try:
|
||||
import jsonschema # noqa: PLC0415
|
||||
except ImportError:
|
||||
self.skipTest("jsonschema is not installed")
|
||||
schema = json.loads((ROOT / "schemas" / "emulator.schema.json").read_text())
|
||||
self.validator = jsonschema.Draft202012Validator(schema)
|
||||
|
||||
def _valid(self, **fields) -> bool:
|
||||
profile = {"emulator": "X", "type": "libretro", "files": [], **fields}
|
||||
return self.validator.is_valid(profile)
|
||||
|
||||
def test_placeholders(self):
|
||||
for value in ("Git", "GIT", "svn", "SVN (2015 snapshot)", "unknown", "master",
|
||||
"latest", "dev", "main", "HEAD", ""):
|
||||
with self.subTest(value=value):
|
||||
self.assertFalse(self._valid(core_version=value))
|
||||
|
||||
def test_versions(self):
|
||||
for value in ("1.63", "v1.0.0.03", "2609-437-g3aac3f0", "0.37b5", "r22",
|
||||
"2026.4.1git", "developer-1", "Dev-4.0.0"):
|
||||
with self.subTest(value=value):
|
||||
self.assertTrue(self._valid(core_version=value))
|
||||
|
||||
def test_none_means_null(self):
|
||||
self.assertTrue(self._valid(core_version=None, version_basis={"kind": "none"}))
|
||||
self.assertFalse(self._valid(core_version="1.0", version_basis={"kind": "none"}))
|
||||
|
||||
def test_each_kind_names_what_it_reads(self):
|
||||
self.assertFalse(self._valid(version_basis={"kind": "file", "path": "v.h"}))
|
||||
self.assertFalse(self._valid(version_basis={"kind": "tag", "tag": "v1"}))
|
||||
self.assertFalse(self._valid(version_basis={"kind": "package"}))
|
||||
self.assertTrue(self._valid(core_version="1", version_basis={"kind": "tag", "tag": "1",
|
||||
"distance": 0}))
|
||||
|
||||
|
||||
class VersionModesRefuseOtherFlags(unittest.TestCase):
|
||||
def test_refused(self):
|
||||
for extra in (["--bump-commit"], ["--check-version"], ["--strict"]):
|
||||
argv = ["profile_sync.py", "--all", "--verify-version", *extra]
|
||||
with self.subTest(flags=extra), mock.patch.object(sys, "argv", argv), \
|
||||
contextlib.redirect_stderr(io.StringIO()):
|
||||
with self.assertRaises(SystemExit) as ctx:
|
||||
profile_sync.main()
|
||||
self.assertEqual(ctx.exception.code, 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -61,7 +61,7 @@ Every module in `tests/`, by the area it guards.
|
||||
| Real-condition campaign | synthetic profiles, records and content folders, a fake host for load, disk, clock and commands, and whole passes against a libretro core built from C when a compiler and strace exist | `test_campaign` |
|
||||
| Automation runner, disk cleanup, asset sync and bit-flip repair | fake jobs, git repositories with a bare origin and session worktrees, a committed file with one bit flipped, canned CI runs in place of `gh` | `test_autorun` |
|
||||
| Truth and native export | synthetic + cached originals | `test_export_counts`, `test_export_native_layer`, `test_export_report`, `test_exporters`, `test_truth_data_dir_readers`, `test_truth_diff`, `test_truth_layout` |
|
||||
| Scrapers, upstream and provenance | inline sources, synthetic | `test_batocera_md5_list`, `test_check_freshness`, `test_contributing_text`, `test_coverage_cache`, `test_data_dir_refs`, `test_dataclass_defaults`, `test_dedup`, `test_doc_pages`, `test_doc_surfaces`, `test_elec_scrapers`, `test_fbneo_parser`, `test_find_pinned_revision`, `test_hash_merge`, `test_launchers`, `test_launchers_emudeck`, `test_launchers_recalbox`, `test_launchers_retrobat`, `test_launchers_retrodeck`, `test_launchers_retropie`, `test_launchers_rocknix`, `test_mame_devices`, `test_mame_parser`, `test_mame_ref_audit`, `test_optional_dependencies`, `test_profile_sync`, `test_prose_style`, `test_provenance`, `test_refresh_data_dirs`, `test_refresh_failures`, `test_refresh_stale`, `test_retrodeck_scraper_errors`, `test_romset_recipes`, `test_schema_semantic_gate`, `test_scraper_contract`, `test_site_availability`, `test_site_content_data`, `test_site_copy`, `test_site_cores`, `test_site_emulator_page`, `test_site_exports`, `test_site_funnel`, `test_site_images`, `test_site_picker`, `test_site_platform_hashes`, `test_site_r2_components`, `test_site_r2_css`, `test_site_r2_emulators`, `test_site_r2_entry`, `test_site_r2_gate`, `test_site_r2_nav`, `test_site_r2_p0`, `test_site_r2_reference`, `test_site_r2_systems`, `test_site_r2_wiki`, `test_site_r3_collection`, `test_site_r3_components`, `test_site_r3_css`, `test_site_r3_emulators`, `test_site_r3_entry`, `test_site_r3_nav`, `test_site_r3_p0`, `test_site_r3_reference`, `test_site_r3_systems`, `test_site_r3_wiki`, `test_site_r4_components`, `test_site_r4_css`, `test_site_r4_emulators`, `test_site_r4_entry`, `test_site_r4_merge`, `test_site_r4_p0`, `test_site_r4_reference`, `test_site_r4_search`, `test_site_r4_systems`, `test_site_r4_wiki`, `test_site_r5_components`, `test_site_r5_css`, `test_site_r5_emulators`, `test_site_r5_entry`, `test_site_r5_reference`, `test_site_r5_search`, `test_site_r5_systems`, `test_site_r5_wiki`, `test_site_render`, `test_site_system_links`, `test_site_validation`, `test_site_weight`, `test_site_write`, `test_source_ref_targets`, `test_upstream`, `test_upstream_index`, `test_validate_pr`, `test_validate_pr_flow`, `test_validate_pr_hashes`, `test_workflow_paths` |
|
||||
| Scrapers, upstream and provenance | inline sources, synthetic | `test_batocera_md5_list`, `test_check_freshness`, `test_contributing_text`, `test_coverage_cache`, `test_data_dir_refs`, `test_dataclass_defaults`, `test_dedup`, `test_doc_pages`, `test_doc_surfaces`, `test_elec_scrapers`, `test_fbneo_parser`, `test_find_pinned_revision`, `test_hash_merge`, `test_launchers`, `test_launchers_emudeck`, `test_launchers_recalbox`, `test_launchers_retrobat`, `test_launchers_retrodeck`, `test_launchers_retropie`, `test_launchers_rocknix`, `test_mame_devices`, `test_mame_parser`, `test_mame_ref_audit`, `test_optional_dependencies`, `test_profile_sync`, `test_prose_style`, `test_provenance`, `test_refresh_data_dirs`, `test_refresh_failures`, `test_refresh_stale`, `test_retrodeck_scraper_errors`, `test_romset_recipes`, `test_schema_semantic_gate`, `test_scraper_contract`, `test_site_availability`, `test_site_content_data`, `test_site_copy`, `test_site_cores`, `test_site_emulator_page`, `test_site_exports`, `test_site_funnel`, `test_site_images`, `test_site_picker`, `test_site_platform_hashes`, `test_site_r2_components`, `test_site_r2_css`, `test_site_r2_emulators`, `test_site_r2_entry`, `test_site_r2_gate`, `test_site_r2_nav`, `test_site_r2_p0`, `test_site_r2_reference`, `test_site_r2_systems`, `test_site_r2_wiki`, `test_site_r3_collection`, `test_site_r3_components`, `test_site_r3_css`, `test_site_r3_emulators`, `test_site_r3_entry`, `test_site_r3_nav`, `test_site_r3_p0`, `test_site_r3_reference`, `test_site_r3_systems`, `test_site_r3_wiki`, `test_site_r4_components`, `test_site_r4_css`, `test_site_r4_emulators`, `test_site_r4_entry`, `test_site_r4_merge`, `test_site_r4_p0`, `test_site_r4_reference`, `test_site_r4_search`, `test_site_r4_systems`, `test_site_r4_wiki`, `test_site_r5_components`, `test_site_r5_css`, `test_site_r5_emulators`, `test_site_r5_entry`, `test_site_r5_reference`, `test_site_r5_search`, `test_site_r5_systems`, `test_site_r5_wiki`, `test_site_render`, `test_site_system_links`, `test_site_validation`, `test_site_weight`, `test_site_write`, `test_source_ref_targets`, `test_upstream`, `test_upstream_index`, `test_validate_pr`, `test_version_basis`, `test_validate_pr_flow`, `test_validate_pr_hashes`, `test_workflow_paths` |
|
||||
|
||||
## Test architecture
|
||||
|
||||
|
||||
Reference in new issue
Block a user