mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-29 07:07:20 -05:00
chore: merge main into the launchers
This commit is contained in:
commit
0369c75dfe
317 files changed
+22514
-2652
No files matched your search
Vendored
+1
-1
@@ -37,4 +37,4 @@
|
||||
|
||||
|
||||
|
||||
A/Enter Open B/Esc Back ? Help
|
||||
A/Enter Use this folder B/Esc Back ? Help
|
||||
Vendored
+1
-1
@@ -9,4 +9,4 @@
|
||||
system/
|
||||
|
||||
|
||||
A/Enter Open B/Esc Back ? Help
|
||||
A Use this folder B Back ? Help
|
||||
Vendored
+1
-1
@@ -12,4 +12,4 @@
|
||||
|
||||
|
||||
|
||||
A/Enter Open B/Esc Back ? Help
|
||||
A Use this folder B Back ? Help
|
||||
Vendored
+1
-1
@@ -21,4 +21,4 @@
|
||||
|
||||
|
||||
|
||||
A/Enter Open B/Esc Back ? Help
|
||||
A/Enter Use this folder B/Esc Back ? Help
|
||||
Vendored
+1
-1
@@ -37,4 +37,4 @@
|
||||
|
||||
|
||||
|
||||
A/Enter Done B/Esc Back X/Space Tick ? Help
|
||||
A/Enter Filter B/Esc Back X/Space Tick ? Help
|
||||
Vendored
+1
-1
@@ -9,4 +9,4 @@
|
||||
|
||||
|
||||
|
||||
A/Enter Done B/Esc Back X/Space Tick
|
||||
A Filter B Back X Tick ? Help
|
||||
Vendored
+1
-1
@@ -12,4 +12,4 @@
|
||||
|
||||
|
||||
|
||||
A/Enter Done B/Esc Back X/Space Tick
|
||||
A Filter B Back X Tick ? Help
|
||||
Vendored
+1
-1
@@ -21,4 +21,4 @@
|
||||
|
||||
|
||||
|
||||
A/Enter Done B/Esc Back X/Space Tick ? Help
|
||||
A/Enter Filter B/Esc Back X/Space Tick ? Help
|
||||
Vendored
+1
-1
@@ -37,4 +37,4 @@
|
||||
|
||||
|
||||
|
||||
A/Enter Choose B/Esc Back
|
||||
A/Enter Create B/Esc Back
|
||||
Vendored
+1
-1
@@ -9,4 +9,4 @@
|
||||
|
||||
|
||||
|
||||
A/Enter Choose B/Esc Back
|
||||
A/Enter Create B/Esc Back
|
||||
Vendored
+1
-1
@@ -12,4 +12,4 @@
|
||||
|
||||
|
||||
|
||||
A/Enter Choose B/Esc Back
|
||||
A/Enter Create B/Esc Back
|
||||
Vendored
+1
-1
@@ -21,4 +21,4 @@
|
||||
|
||||
|
||||
|
||||
A/Enter Choose B/Esc Back
|
||||
A/Enter Create B/Esc Back
|
||||
Vendored
+3
-3
@@ -4,8 +4,7 @@
|
||||
known
|
||||
Address: https://raw.githubusercontent.com/Abdess/retrobios/main/install/retroarch.json
|
||||
|
||||
> Try again
|
||||
Choose another setup or folder
|
||||
> Choose another setup or folder
|
||||
Quit
|
||||
|
||||
|
||||
@@ -37,4 +36,5 @@
|
||||
|
||||
|
||||
|
||||
A/Enter Choose B/Esc Back
|
||||
|
||||
A/Enter Back B/Esc Back
|
||||
Vendored
+6
-6
@@ -3,10 +3,10 @@
|
||||
Could not fetch the file list for
|
||||
retroarch after 3 attempts: [Errno
|
||||
-2] Name or service not known
|
||||
|
||||
> Try again
|
||||
Choose another setup or folder
|
||||
Address:
|
||||
https://raw.githubusercontent.com/
|
||||
Abdess/retrobios/main/install/
|
||||
retroarch.json
|
||||
> Choose another setup or folder
|
||||
Quit
|
||||
|
||||
|
||||
A/Enter Choose B/Esc Back
|
||||
A/Enter Back B/Esc Back
|
||||
Vendored
+3
-3
@@ -8,8 +8,8 @@
|
||||
Abdess/retrobios/main/install/
|
||||
retroarch.json
|
||||
|
||||
> Try again
|
||||
Choose another setup or folder
|
||||
> Choose another setup or folder
|
||||
Quit
|
||||
|
||||
A/Enter Choose B/Esc Back
|
||||
|
||||
A/Enter Back B/Esc Back
|
||||
Vendored
+3
-3
@@ -5,8 +5,7 @@
|
||||
Address: https://raw.githubusercontent.com/Abdess/retrobios/main/install/
|
||||
retroarch.json
|
||||
|
||||
> Try again
|
||||
Choose another setup or folder
|
||||
> Choose another setup or folder
|
||||
Quit
|
||||
|
||||
|
||||
@@ -21,4 +20,5 @@
|
||||
|
||||
|
||||
|
||||
A/Enter Choose B/Esc Back
|
||||
|
||||
A/Enter Back B/Esc Back
|
||||
Vendored
+1
-1
@@ -37,4 +37,4 @@
|
||||
|
||||
|
||||
|
||||
A/Enter Choose B/Esc Back ? Help
|
||||
A/Enter Use this folder B/Esc Back ? Help
|
||||
Vendored
+1
-1
@@ -9,4 +9,4 @@
|
||||
|
||||
|
||||
|
||||
A/Enter Choose B/Esc Back ? Help
|
||||
A Use this folder B Back ? Help
|
||||
Vendored
+1
-1
@@ -12,4 +12,4 @@
|
||||
|
||||
|
||||
|
||||
A/Enter Choose B/Esc Back ? Help
|
||||
A Use this folder B Back ? Help
|
||||
Vendored
+1
-1
@@ -21,4 +21,4 @@
|
||||
|
||||
|
||||
|
||||
A/Enter Choose B/Esc Back ? Help
|
||||
A/Enter Use this folder B/Esc Back ? Help
|
||||
+1
-1
@@ -37,4 +37,4 @@
|
||||
|
||||
|
||||
|
||||
A/Enter Choose B/Esc Back ? Help
|
||||
A/Enter Browse B/Esc Back ? Help
|
||||
+1
-1
@@ -9,4 +9,4 @@
|
||||
|
||||
|
||||
|
||||
A/Enter Choose B/Esc Back ? Help
|
||||
A/Enter Browse B/Esc Back ? Help
|
||||
+1
-1
@@ -12,4 +12,4 @@
|
||||
|
||||
|
||||
|
||||
A/Enter Choose B/Esc Back ? Help
|
||||
A/Enter Browse B/Esc Back ? Help
|
||||
+1
-1
@@ -21,4 +21,4 @@
|
||||
|
||||
|
||||
|
||||
A/Enter Choose B/Esc Back ? Help
|
||||
A/Enter Browse B/Esc Back ? Help
|
||||
Vendored
+2
-2
@@ -1,8 +1,6 @@
|
||||
RetroBIOS: Keys
|
||||
|
||||
Up/Down, k/j Move
|
||||
PgUp/PgDn, L1/R1 Page
|
||||
Home/End, g/G First, last
|
||||
A/Enter, Start Install
|
||||
Y, o, Tab Options
|
||||
B/Esc, Backspace Back
|
||||
@@ -34,6 +32,8 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Vendored
+2
-2
@@ -1,12 +1,12 @@
|
||||
RetroBIOS: Keys
|
||||
|
||||
Up/Down, k/j Move
|
||||
PgUp/PgDn, L1/R1 Page
|
||||
Home/End, g/G First, last
|
||||
A/Enter, Start Install
|
||||
Y, o, Tab Options
|
||||
B/Esc, Backspace Back
|
||||
q, Ctrl-C Quit
|
||||
?, F1 This help
|
||||
|
||||
BIOS folder:
|
||||
/home/user/.config/retroarch/system
|
||||
Any key closes this help
|
||||
Vendored
+2
-2
@@ -1,8 +1,6 @@
|
||||
RetroBIOS: Keys
|
||||
|
||||
Up/Down, k/j Move
|
||||
PgUp/PgDn, L1/R1 Page
|
||||
Home/End, g/G First, last
|
||||
A/Enter, Start Install
|
||||
Y, o, Tab Options
|
||||
B/Esc, Backspace Back
|
||||
@@ -12,4 +10,6 @@
|
||||
BIOS folder:
|
||||
/home/user/.config/retroarch/system
|
||||
|
||||
|
||||
|
||||
Any key closes this help
|
||||
Vendored
+2
-2
@@ -1,8 +1,6 @@
|
||||
RetroBIOS: Keys
|
||||
|
||||
Up/Down, k/j Move
|
||||
PgUp/PgDn, L1/R1 Page
|
||||
Home/End, g/G First, last
|
||||
A/Enter, Start Install
|
||||
Y, o, Tab Options
|
||||
B/Esc, Backspace Back
|
||||
@@ -21,4 +19,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Any key closes this help
|
||||
+2
-2
@@ -4,7 +4,7 @@
|
||||
528.0 KB of 1.0 MB
|
||||
128.9 KB/s, 0:04 left
|
||||
2 of 4 files
|
||||
What is done stays done if you stop.
|
||||
B, Ctrl-C or Start+Select stops. What is done stays done.
|
||||
|
||||
FAILED scph5500.bin (HTTP 404)
|
||||
ok gba_bios.bin
|
||||
@@ -37,4 +37,4 @@
|
||||
|
||||
|
||||
|
||||
Ctrl-C / Start+Select stops
|
||||
B/Esc Stop ? Help
|
||||
+3
-3
@@ -4,9 +4,9 @@
|
||||
528.0 KB of 1.0 MB
|
||||
128.9 KB/s, 0:04 left
|
||||
2 of 4 files
|
||||
What is done stays done if you stop.
|
||||
B, Ctrl-C or Start+Select stops. What
|
||||
is done stays done.
|
||||
|
||||
FAILED scph5500.bin (HTTP 404)
|
||||
ok gba_bios.bin
|
||||
|
||||
Ctrl-C / Start+Select stops
|
||||
B/Esc Stop ? Help
|
||||
+3
-3
@@ -4,12 +4,12 @@
|
||||
528.0 KB of 1.0 MB
|
||||
128.9 KB/s, 0:04 left
|
||||
2 of 4 files
|
||||
What is done stays done if you stop.
|
||||
B, Ctrl-C or Start+Select stops. What
|
||||
is done stays done.
|
||||
|
||||
FAILED scph5500.bin (HTTP 404)
|
||||
ok gba_bios.bin
|
||||
|
||||
|
||||
|
||||
|
||||
Ctrl-C / Start+Select stops
|
||||
B/Esc Stop ? Help
|
||||
+2
-2
@@ -4,7 +4,7 @@
|
||||
528.0 KB of 1.0 MB
|
||||
128.9 KB/s, 0:04 left
|
||||
2 of 4 files
|
||||
What is done stays done if you stop.
|
||||
B, Ctrl-C or Start+Select stops. What is done stays done.
|
||||
|
||||
FAILED scph5500.bin (HTTP 404)
|
||||
ok gba_bios.bin
|
||||
@@ -21,4 +21,4 @@
|
||||
|
||||
|
||||
|
||||
Ctrl-C / Start+Select stops
|
||||
B/Esc Stop ? Help
|
||||
+2
-2
@@ -2,7 +2,7 @@
|
||||
|
||||
Each row is an option of the command line, named on the right.
|
||||
|
||||
> Preferred regions: north-america, japan --region
|
||||
> Preferred regions: North America, Japan --region
|
||||
Leave out game data: no --no-game-data
|
||||
Installed cores only: no (1 found) --installed-cores
|
||||
Cores: all --core
|
||||
@@ -37,4 +37,4 @@
|
||||
|
||||
|
||||
|
||||
A/Enter Change B/Esc Back Left/Right Set ? Help
|
||||
A/Enter Open B/Esc Back Left/Right Set ? Help
|
||||
Vendored
+3
-3
@@ -1,12 +1,12 @@
|
||||
RetroBIOS: Options
|
||||
|
||||
> Preferred regions: north-america, ja
|
||||
> Preferred regions: North America, Ja
|
||||
Leave out game data: no
|
||||
Installed cores only: no (1 found)
|
||||
Cores: all
|
||||
Parallel downloads: 8
|
||||
BIOS folder: ~/.config/retroarch/sys
|
||||
BIOS folder: ~/.config...ch/system
|
||||
Preview the file list
|
||||
Show the command line
|
||||
|
||||
A/Enter Change B/Esc Back
|
||||
A Open B Back <> Set ? Help
|
||||
Vendored
+3
-3
@@ -1,15 +1,15 @@
|
||||
RetroBIOS: Options
|
||||
|
||||
> Preferred regions: north-america, ja
|
||||
> Preferred regions: North America, Ja
|
||||
Leave out game data: no
|
||||
Installed cores only: no (1 found)
|
||||
Cores: all
|
||||
Parallel downloads: 8
|
||||
BIOS folder: ~/.config/retroarch/sys
|
||||
BIOS folder: ~/.config...ch/system
|
||||
Preview the file list
|
||||
Show the command line
|
||||
|
||||
Sets --region
|
||||
|
||||
|
||||
A/Enter Change B/Esc Back
|
||||
A Open B Back <> Set ? Help
|
||||
Vendored
+2
-2
@@ -2,7 +2,7 @@
|
||||
|
||||
Each row is an option of the command line, named on the right.
|
||||
|
||||
> Preferred regions: north-america, japan --region
|
||||
> Preferred regions: North America, Japan --region
|
||||
Leave out game data: no --no-game-data
|
||||
Installed cores only: no (1 found) --installed-cores
|
||||
Cores: all --core
|
||||
@@ -21,4 +21,4 @@
|
||||
|
||||
|
||||
|
||||
A/Enter Change B/Esc Back Left/Right Set ? Help
|
||||
A/Enter Open B/Esc Back Left/Right Set ? Help
|
||||
Vendored
+2
-2
@@ -3,7 +3,7 @@
|
||||
The folder for RetroArch:
|
||||
> ~/bios_
|
||||
|
||||
Quotes are removed, ~ and variables are expanded.
|
||||
Ctrl-U clears. Quotes are removed, ~ and variables are expanded.
|
||||
|
||||
|
||||
|
||||
@@ -37,4 +37,4 @@
|
||||
|
||||
|
||||
|
||||
Enter Use this folder Esc Back
|
||||
Enter Use this folder Esc Back Ctrl-U Clear
|
||||
Vendored
+2
-2
@@ -3,8 +3,8 @@
|
||||
The folder for RetroArch:
|
||||
> ~/bios_
|
||||
|
||||
Quotes are removed, ~ and variables
|
||||
are expanded.
|
||||
Ctrl-U clears. Quotes are removed, ~
|
||||
and variables are expanded.
|
||||
|
||||
|
||||
|
||||
|
||||
Vendored
+2
-2
@@ -3,8 +3,8 @@
|
||||
The folder for RetroArch:
|
||||
> ~/bios_
|
||||
|
||||
Quotes are removed, ~ and variables
|
||||
are expanded.
|
||||
Ctrl-U clears. Quotes are removed, ~
|
||||
and variables are expanded.
|
||||
|
||||
|
||||
|
||||
|
||||
Vendored
+2
-2
@@ -3,7 +3,7 @@
|
||||
The folder for RetroArch:
|
||||
> ~/bios_
|
||||
|
||||
Quotes are removed, ~ and variables are expanded.
|
||||
Ctrl-U clears. Quotes are removed, ~ and variables are expanded.
|
||||
|
||||
|
||||
|
||||
@@ -21,4 +21,4 @@
|
||||
|
||||
|
||||
|
||||
Enter Use this folder Esc Back
|
||||
Enter Use this folder Esc Back Ctrl-U Clear
|
||||
Vendored
+1
-1
@@ -9,4 +9,4 @@
|
||||
Choose systems
|
||||
Options
|
||||
Quit
|
||||
A/Enter Install B/Esc Back
|
||||
A Install B Back Y Options ? Help
|
||||
Vendored
+1
-1
@@ -12,4 +12,4 @@
|
||||
Choose systems
|
||||
Options
|
||||
Quit
|
||||
A/Enter Install B/Esc Back
|
||||
A Install B Back Y Options ? Help
|
||||
+1
-1
@@ -9,4 +9,4 @@
|
||||
> Close
|
||||
Options
|
||||
|
||||
A/Enter Close B/Esc Back Y/o Options
|
||||
A Close B Back Y Options ? Help
|
||||
+1
-1
@@ -12,4 +12,4 @@
|
||||
|
||||
|
||||
|
||||
A/Enter Close B/Esc Back Y/o Options
|
||||
A Close B Back Y Options ? Help
|
||||
+2
-2
@@ -3,7 +3,7 @@
|
||||
Choose the setup that will use the BIOS files.
|
||||
|
||||
Filter (/ to type)
|
||||
> RetroArch
|
||||
> RetroArch muOS, JELOS, KOLOS, Uno...Anbernic stock firmware
|
||||
Batocera
|
||||
Recalbox
|
||||
RetroBat
|
||||
@@ -15,7 +15,7 @@
|
||||
BizHawk
|
||||
MiSTer FPGA
|
||||
RetroPie
|
||||
AmberELEC
|
||||
AmberELEC PAN4ELEC
|
||||
EmuELEC
|
||||
KNULLI
|
||||
MinUI
|
||||
|
||||
+3
-3
@@ -1,12 +1,12 @@
|
||||
RetroBIOS: Which setup?
|
||||
|
||||
Filter (/ to type)
|
||||
> RetroArch
|
||||
> RetroArch muOS, JE...firmware
|
||||
Batocera
|
||||
Recalbox
|
||||
RetroBat
|
||||
EmuDeck
|
||||
Lakka
|
||||
RetroDECK
|
||||
1-8 of 19
|
||||
A/Enter Choose B/Esc Back / Filter
|
||||
1-7 of 18
|
||||
A Choose B Back / Filter ? Help
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
RetroBIOS: Which setup?
|
||||
|
||||
Filter (/ to type)
|
||||
> RetroArch
|
||||
> RetroArch muOS, JE...firmware
|
||||
Batocera
|
||||
Recalbox
|
||||
RetroBat
|
||||
@@ -11,5 +11,5 @@
|
||||
ROCKNIX
|
||||
RomM
|
||||
BizHawk
|
||||
1-11 of 19
|
||||
A/Enter Choose B/Esc Back / Filter
|
||||
1-10 of 18
|
||||
A Choose B Back / Filter ? Help
|
||||
+2
-2
@@ -3,7 +3,7 @@
|
||||
Choose the setup that will use the BIOS files.
|
||||
|
||||
Filter (/ to type)
|
||||
> RetroArch
|
||||
> RetroArch muOS, JELOS, KOLOS...nic stock firmware
|
||||
Batocera
|
||||
Recalbox
|
||||
RetroBat
|
||||
@@ -15,7 +15,7 @@
|
||||
BizHawk
|
||||
MiSTer FPGA
|
||||
RetroPie
|
||||
AmberELEC
|
||||
AmberELEC PAN4ELEC
|
||||
EmuELEC
|
||||
KNULLI
|
||||
MinUI
|
||||
|
||||
Vendored
+1
-1
@@ -37,4 +37,4 @@
|
||||
|
||||
|
||||
|
||||
A/Enter Choose B/Esc Stay
|
||||
A/Enter Quit B/Esc Stay
|
||||
Vendored
+1
-1
@@ -9,4 +9,4 @@
|
||||
|
||||
|
||||
|
||||
A/Enter Choose B/Esc Stay
|
||||
A/Enter Quit B/Esc Stay
|
||||
Vendored
+1
-1
@@ -12,4 +12,4 @@
|
||||
|
||||
|
||||
|
||||
A/Enter Choose B/Esc Stay
|
||||
A/Enter Quit B/Esc Stay
|
||||
Vendored
+1
-1
@@ -21,4 +21,4 @@
|
||||
|
||||
|
||||
|
||||
A/Enter Choose B/Esc Stay
|
||||
A/Enter Quit B/Esc Stay
|
||||
+2
-2
@@ -3,8 +3,8 @@
|
||||
Tick the regions to keep, best first. Where a system has one BIOS per region, the best one found
|
||||
is installed.
|
||||
|
||||
> [1] north-america
|
||||
[ ] japan
|
||||
> [1] North America
|
||||
[ ] Japan
|
||||
|
||||
|
||||
|
||||
|
||||
Vendored
+3
-3
@@ -5,8 +5,8 @@
|
||||
region, the best one found is
|
||||
installed.
|
||||
|
||||
> [1] north-america
|
||||
[ ] japan
|
||||
> [1] North America
|
||||
[ ] Japan
|
||||
|
||||
|
||||
A/Enter Tick B/Esc Done
|
||||
A Tick B Done <> Order ? Help
|
||||
Vendored
+3
-3
@@ -5,11 +5,11 @@
|
||||
region, the best one found is
|
||||
installed.
|
||||
|
||||
> [1] north-america
|
||||
[ ] japan
|
||||
> [1] North America
|
||||
[ ] Japan
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
A/Enter Tick B/Esc Done
|
||||
A Tick B Done <> Order ? Help
|
||||
Vendored
+2
-2
@@ -3,8 +3,8 @@
|
||||
Tick the regions to keep, best first. Where a system has one BIOS per region,
|
||||
the best one found is installed.
|
||||
|
||||
> [1] north-america
|
||||
[ ] japan
|
||||
> [1] North America
|
||||
[ ] Japan
|
||||
|
||||
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -8,5 +8,5 @@
|
||||
/home/user/.config/retroarch/
|
||||
retrobios-backup/2026-10-23_1200
|
||||
To put them back: python3 install.py
|
||||
1-8 of 20
|
||||
1-4 of 9
|
||||
A/Enter Exit Up/Down Scroll
|
||||
Vendored
+1
-1
@@ -11,5 +11,5 @@
|
||||
--restore
|
||||
/home/user/.config/retroarch/
|
||||
retrobios-backup/2026-10-23_1200
|
||||
1-11 of 20
|
||||
1-5 of 9
|
||||
A/Enter Exit Up/Down Scroll
|
||||
Vendored
+2
-2
@@ -2,8 +2,8 @@
|
||||
|
||||
Found on this machine:
|
||||
|
||||
> RetroArch ~/.config/retroarch/system
|
||||
KNULLI (Batocera files) /userdata/bios
|
||||
> RetroArch
|
||||
KNULLI (Batocera files)
|
||||
Another setup or folder
|
||||
|
||||
BIOS folder: /home/user/.config/retroarch/system
|
||||
|
||||
Vendored
+2
-2
@@ -2,8 +2,8 @@
|
||||
|
||||
Found on this machine:
|
||||
|
||||
> RetroArch ~/.config/retroarch/system
|
||||
KNULLI (Batocera files) /userdata/bios
|
||||
> RetroArch
|
||||
KNULLI (Batocera files)
|
||||
Another setup or folder
|
||||
|
||||
BIOS folder: /home/user/.config/retroarch/system
|
||||
|
||||
Vendored
+1
-1
@@ -9,4 +9,4 @@
|
||||
|
||||
|
||||
2 of 3 systems: 4 files, 1.0 MB
|
||||
A/Enter Continue B/Esc Back
|
||||
A Continue B Back X Tick ? Help
|
||||
Vendored
+1
-1
@@ -12,4 +12,4 @@
|
||||
|
||||
|
||||
2 of 3 systems: 4 files, 1.0 MB
|
||||
A/Enter Continue B/Esc Back
|
||||
A Continue B Back X Tick ? Help
|
||||
@@ -5049,6 +5049,9 @@ class EventsDescribeTheRun(unittest.TestCase, _SyntheticTree):
|
||||
{"event": "detect", "os": "linux", "os_name": install._os_name(),
|
||||
"platforms": [{"platform": "retroarch", "name": "RetroArch",
|
||||
"bios_dir": str(self.bios)}]},
|
||||
# a.bin and b.bin are in place: the first read and the last.
|
||||
{"event": "checking", "files_done": 1, "files_total": 2},
|
||||
{"event": "checking", "files_done": 2, "files_total": 2},
|
||||
{"event": "file", "platform": "retroarch", "dest": "a.bin",
|
||||
"systems": ["sys-a"], "size": 100, "state": "present"},
|
||||
{"event": "file", "platform": "retroarch", "dest": "b.bin",
|
||||
|
||||
@@ -0,0 +1,491 @@
|
||||
"""BIOS files the user already has, found by their contents.
|
||||
|
||||
A user who wiped the card a handheld came with asks where its BIOS files went,
|
||||
and one who copied the folder from a Mac, unzipped a pack into bios/bios/ or
|
||||
renamed scph1001.bin in capitals read "missing" for a file sitting in the
|
||||
folder. --from FOLDER copies what an old card, a backup or a Downloads folder
|
||||
holds, and an install puts the files it finds misplaced in the BIOS folder
|
||||
where the cores open them. Every file is recognised by its hash, copied and
|
||||
never moved, and checked again once written.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import copy
|
||||
import io
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO_ROOT))
|
||||
sys.path.insert(0, str(REPO_ROOT / "tests"))
|
||||
|
||||
import install # noqa: E402
|
||||
from test_install_offline import ( # noqa: E402
|
||||
EXTRA, GBA, JP, PAYLOAD, US, Scratch, _entry, _offline_env,
|
||||
)
|
||||
from test_install_selection import FakeRepository # noqa: E402
|
||||
|
||||
|
||||
def _zip(path: Path, members: dict[str, bytes]) -> Path:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as archive:
|
||||
for name, data in members.items():
|
||||
archive.writestr(name, data)
|
||||
return path
|
||||
|
||||
|
||||
def _snapshot(folder: Path) -> dict[str, tuple[bytes, int]]:
|
||||
"""Every file below folder, with its bytes and modification time."""
|
||||
return {p.relative_to(folder).as_posix(): (p.read_bytes(), p.stat().st_mtime_ns)
|
||||
for p in folder.rglob("*") if p.is_file() and not p.is_symlink()}
|
||||
|
||||
|
||||
def _events(proc: subprocess.CompletedProcess) -> list[dict]:
|
||||
return [json.loads(line) for line in proc.stdout.splitlines() if line.startswith("{")]
|
||||
|
||||
|
||||
class FromFolder(Scratch):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.old = self.root / "old card"
|
||||
self.old.mkdir()
|
||||
|
||||
def keep(self, relative: str, data: bytes) -> Path:
|
||||
path = self.old / relative
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_bytes(data)
|
||||
return path
|
||||
|
||||
def install(self, *options: str) -> subprocess.CompletedProcess:
|
||||
return self.run_installer("--manifest", str(self.list), "--from", str(self.old),
|
||||
"--no-input", *options)
|
||||
|
||||
def installed(self) -> dict[str, bytes]:
|
||||
if not self.dest.exists():
|
||||
return {}
|
||||
return {p.relative_to(self.dest).as_posix(): p.read_bytes()
|
||||
for p in self.dest.rglob("*") if p.is_file()}
|
||||
|
||||
|
||||
class CopyFromAnOldCard(FromFolder):
|
||||
def test_files_in_nested_folders_and_under_other_names_are_copied(self):
|
||||
self.keep("BIOS/psx/SCPH5501.BIN", US)
|
||||
self.keep("Roms/psx/japan.bin", JP)
|
||||
self.keep("a/b/c/gba.rom", GBA)
|
||||
self.keep("pcsx/extra.bin", EXTRA)
|
||||
before = _snapshot(self.old)
|
||||
proc = self.install()
|
||||
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
|
||||
self.assertEqual(self.installed(), PAYLOAD)
|
||||
self.assertIn(f"4 files copied from {self.old}.", proc.stdout)
|
||||
self.assertEqual(_snapshot(self.old), before, "the old card is only read")
|
||||
|
||||
def test_members_of_a_zip_are_copied(self):
|
||||
_zip(self.old / "Downloads" / "psx bios.zip",
|
||||
{"PSX/scph5501.bin": US, "PSX/scph5500.bin": JP, "readme.txt": b"hi"})
|
||||
self.keep("gba_bios.bin", GBA)
|
||||
self.keep("pcsx/extra.bin", EXTRA)
|
||||
proc = self.install()
|
||||
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
|
||||
self.assertEqual(self.installed(), PAYLOAD)
|
||||
|
||||
def test_a_zip_given_alone_is_read_as_a_folder(self):
|
||||
archive = _zip(self.root / "backup.zip", {f"system/{k}": v for k, v in PAYLOAD.items()})
|
||||
proc = self.run_installer("--manifest", str(self.list), "--from", str(archive),
|
||||
"--no-input")
|
||||
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
|
||||
self.assertEqual(self.installed(), PAYLOAD)
|
||||
self.assertIn(f"4 files copied from {archive}.", proc.stdout)
|
||||
|
||||
def test_an_archive_the_list_wants_whole_is_copied_whole(self):
|
||||
inner = io.BytesIO()
|
||||
with zipfile.ZipFile(inner, "w") as archive:
|
||||
archive.writestr("sp-s2.sp1", b"neo geo bios" * 10)
|
||||
neogeo = inner.getvalue()
|
||||
self.list.write_text(json.dumps({
|
||||
"manifest_version": 2, "platform": "retroarch",
|
||||
"files": [_entry("neogeo.zip", neogeo, ["snk-neogeo"]),
|
||||
_entry("fbneo/neogeo.zip", neogeo, ["snk-neogeo"])],
|
||||
}))
|
||||
self.keep("arcade/neogeo (1).zip", neogeo)
|
||||
proc = self.install()
|
||||
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
|
||||
self.assertEqual(self.installed(), {"neogeo.zip": neogeo, "fbneo/neogeo.zip": neogeo})
|
||||
|
||||
def test_an_archive_inside_an_archive_is_copied(self):
|
||||
inner = io.BytesIO()
|
||||
with zipfile.ZipFile(inner, "w") as archive:
|
||||
archive.writestr("sp-s2.sp1", b"neo geo bios" * 10)
|
||||
neogeo = inner.getvalue()
|
||||
self.list.write_text(json.dumps({
|
||||
"manifest_version": 2, "platform": "retroarch",
|
||||
"files": [_entry("neogeo.zip", neogeo, ["snk-neogeo"])],
|
||||
}))
|
||||
_zip(self.old / "RetroArch_BIOS_Pack.zip", {"neogeo.zip": neogeo})
|
||||
proc = self.install()
|
||||
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
|
||||
self.assertEqual(self.installed(), {"neogeo.zip": neogeo})
|
||||
|
||||
def test_an_empty_file_and_a_same_size_file_are_not_taken(self):
|
||||
""""bios folder is 0kb" after flashing Garlic: a name is no proof."""
|
||||
self.keep("scph5501.bin", b"")
|
||||
self.keep("gba_bios.bin", b"x" * len(GBA))
|
||||
self.keep("scph5500.bin", JP)
|
||||
self.keep("pcsx/extra.bin", EXTRA)
|
||||
proc = self.install()
|
||||
self.assertEqual(proc.returncode, 1, proc.stdout + proc.stderr)
|
||||
self.assertEqual(self.installed(), {"scph5500.bin": JP, "pcsx/extra.bin": EXTRA})
|
||||
self.assertIn(f"2 files copied from {self.old}, 2 still missing.", proc.stdout)
|
||||
|
||||
def test_a_link_out_of_the_folder_is_not_followed(self):
|
||||
outside = self.root / "elsewhere"
|
||||
outside.mkdir()
|
||||
(outside / "scph5501.bin").write_bytes(US)
|
||||
(outside / "gba").mkdir()
|
||||
(outside / "gba" / "gba_bios.bin").write_bytes(GBA)
|
||||
try:
|
||||
(self.old / "scph5501.bin").symlink_to(outside / "scph5501.bin")
|
||||
(self.old / "gba").symlink_to(outside / "gba", target_is_directory=True)
|
||||
except (OSError, NotImplementedError):
|
||||
self.skipTest("this system makes no symbolic link")
|
||||
self.keep("scph5500.bin", JP)
|
||||
proc = self.install()
|
||||
self.assertEqual(proc.returncode, 1, proc.stdout + proc.stderr)
|
||||
self.assertEqual(self.installed(), {"scph5500.bin": JP})
|
||||
|
||||
def test_a_link_inside_the_folder_is_read(self):
|
||||
self.keep("real/scph5501.bin", US)
|
||||
try:
|
||||
(self.old / "scph5501.bin").symlink_to(self.old / "real" / "scph5501.bin")
|
||||
except (OSError, NotImplementedError):
|
||||
self.skipTest("this system makes no symbolic link")
|
||||
index = install.FolderIndex(self.old)
|
||||
self.assertEqual(len(index.by_size[len(US)]), 2)
|
||||
|
||||
def test_mac_and_system_files_are_never_read(self):
|
||||
"""A Mac writes ._name beside every file it copies to a FAT card,
|
||||
and Spotlight keeps an index there: neither is a file a person put."""
|
||||
self.keep("._scph5501.bin", US)
|
||||
self.keep(".Spotlight-V100/Store-V2/scph5500.bin", JP)
|
||||
self.keep("System Volume Information/gba_bios.bin", GBA)
|
||||
self.keep("pcsx/extra.bin", EXTRA)
|
||||
proc = self.install()
|
||||
self.assertEqual(proc.returncode, 1, proc.stdout + proc.stderr)
|
||||
self.assertEqual(self.installed(), {"pcsx/extra.bin": EXTRA})
|
||||
|
||||
def test_the_rest_is_downloaded_and_the_summary_says_where_each_came_from(self):
|
||||
with FakeRepository() as repo:
|
||||
old = repo.root / "old"
|
||||
payloads = FakeRepository.PAYLOADS
|
||||
(old / "BIOS").mkdir(parents=True)
|
||||
(old / "BIOS" / "SCPH5500.BIN").write_bytes(payloads["scph5500.bin"][0])
|
||||
(old / "gba.bin").write_bytes(payloads["gba_bios.bin"][0])
|
||||
(repo.root / "bios" / "mgba_extra.bin").unlink()
|
||||
proc = repo.run("--from", str(old), "--no-input")
|
||||
self.assertEqual(proc.returncode, 1, proc.stdout + proc.stderr)
|
||||
self.assertIn(f"2 files copied from {old}, 2 downloaded, 1 still missing.",
|
||||
proc.stdout)
|
||||
self.assertEqual(sorted(repo.installed()), sorted(
|
||||
name for name in payloads if name != "mgba/extra.bin"))
|
||||
|
||||
def test_two_folders_are_read_in_turn(self):
|
||||
second = self.root / "backup"
|
||||
(second / "x").mkdir(parents=True)
|
||||
(second / "x" / "gba_bios.bin").write_bytes(GBA)
|
||||
(second / "scph5501.bin").write_bytes(US)
|
||||
self.keep("scph5501.bin", US)
|
||||
self.keep("scph5500.bin", JP)
|
||||
self.keep("pcsx/extra.bin", EXTRA)
|
||||
proc = self.install("--from", str(second))
|
||||
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
|
||||
self.assertEqual(self.installed(), PAYLOAD)
|
||||
self.assertIn(f"3 files copied from {self.old}, 1 copied from {second}.", proc.stdout)
|
||||
|
||||
def test_check_names_what_the_folder_would_give_and_writes_nothing(self):
|
||||
self.keep("BIOS/SCPH5501.BIN", US)
|
||||
self.keep("gba_bios.bin", GBA)
|
||||
proc = self.run_installer("--check", "--manifest", str(self.list), "--from",
|
||||
str(self.old))
|
||||
self.assertEqual(proc.returncode, 1, proc.stdout + proc.stderr)
|
||||
self.assertIn(f"{self.old} holds 2 of these files: a run without --check "
|
||||
"copies them.", proc.stdout)
|
||||
self.assertIn(f"in {self.old}: BIOS/SCPH5501.BIN", proc.stdout)
|
||||
self.assertFalse(self.dest.exists())
|
||||
|
||||
def test_dry_run_counts_the_copies_and_writes_nothing(self):
|
||||
self.keep("BIOS/SCPH5501.BIN", US)
|
||||
self.keep("gba_bios.bin", GBA)
|
||||
proc = self.install("--dry-run")
|
||||
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
|
||||
self.assertIn(f"Would copy 2 files ({install.format_size(len(US) + len(GBA))}) "
|
||||
f"from {self.old}", proc.stdout)
|
||||
self.assertIn("Would download 2 files", proc.stdout)
|
||||
self.assertFalse(self.dest.exists())
|
||||
self.assertFalse((self.root / install.BACKUP_DIR).exists())
|
||||
|
||||
def test_a_folder_that_does_not_exist_is_refused(self):
|
||||
proc = self.run_installer("--manifest", str(self.list), "--from",
|
||||
str(self.root / "nowhere"))
|
||||
self.assertEqual(proc.returncode, 1)
|
||||
self.assertIn("does not exist", proc.stderr)
|
||||
self.assertFalse(self.dest.exists())
|
||||
|
||||
def test_a_listing_refuses_the_option(self):
|
||||
proc = self.run_installer("--manifest", str(self.list), "--from", str(self.old),
|
||||
"--list-systems")
|
||||
self.assertEqual(proc.returncode, 2)
|
||||
self.assertIn("--from", proc.stderr)
|
||||
|
||||
def test_restore_refuses_the_option(self):
|
||||
proc = subprocess.run(
|
||||
[sys.executable, str(REPO_ROOT / "install.py"), "--restore", str(self.old),
|
||||
"--from", str(self.old)],
|
||||
env=_offline_env(), capture_output=True, text=True, timeout=60, check=False,
|
||||
stdin=subprocess.DEVNULL)
|
||||
self.assertEqual(proc.returncode, 2)
|
||||
self.assertIn("takes no --from", proc.stderr)
|
||||
|
||||
|
||||
class CaseTwinsOnAFolderThatFoldsCase(FromFolder):
|
||||
"""An ext4 card holds font.bin and FONT.BIN, two files. On an exFAT
|
||||
card or a Windows folder they are one name: the copy kept is the one
|
||||
the list installs, found by its hash, never the other by its name."""
|
||||
|
||||
def test_the_hash_decides_which_twin_lands(self):
|
||||
first, second = b"first font" * 9, b"second font" * 9
|
||||
self.list.write_text(json.dumps({
|
||||
"manifest_version": 2, "platform": "retroarch",
|
||||
"files": [_entry("sys/font.bin", first, ["x"]),
|
||||
_entry("SYS/FONT.BIN", second, ["x"])],
|
||||
}))
|
||||
self.keep("sys/FONT.BIN", first)
|
||||
self.keep("sys/font.bin", second)
|
||||
out = io.StringIO()
|
||||
argv = ["install.py", "--platform", "retroarch", "--dest", str(self.dest),
|
||||
"--manifest", str(self.list), "--from", str(self.old), "--no-input"]
|
||||
with mock.patch.object(install, "keeps_case", return_value=False), \
|
||||
mock.patch.object(install, "detect_os", return_value="linux"), \
|
||||
mock.patch("sys.argv", argv), contextlib.redirect_stdout(out), \
|
||||
contextlib.redirect_stderr(io.StringIO()):
|
||||
try:
|
||||
install.main()
|
||||
except SystemExit as exc:
|
||||
self.assertIn(exc.code, (0, None), out.getvalue())
|
||||
self.addCleanup(install.LOCAL.reset)
|
||||
self.assertEqual(self.installed(), {"sys/font.bin": first})
|
||||
self.assertIn("1 file copied from", out.getvalue())
|
||||
|
||||
|
||||
class EventsForAFolder(FromFolder):
|
||||
def test_each_file_found_is_an_event_before_it_is_copied(self):
|
||||
self.keep("BIOS/SCPH5501.BIN", US)
|
||||
_zip(self.old / "gba.zip", {"gba_bios.bin": GBA})
|
||||
proc = self.install("--events")
|
||||
events = _events(proc)
|
||||
self.assertEqual(proc.returncode, 1, proc.stderr)
|
||||
for event in events:
|
||||
self.assertEqual(list(event), ["event", *install.EVENT_KEYS[event["event"]]])
|
||||
found = sorted((e for e in events if e["event"] == "found"), key=lambda e: e["dest"])
|
||||
self.assertEqual(found, [
|
||||
{"event": "found", "platform": "retroarch", "dest": "gba_bios.bin",
|
||||
"folder": str(self.old), "path": str((self.old / "gba.zip").resolve()),
|
||||
"member": "gba_bios.bin", "reason": ""},
|
||||
{"event": "found", "platform": "retroarch", "dest": "scph5501.bin",
|
||||
"folder": str(self.old),
|
||||
"path": str((self.old / "BIOS" / "SCPH5501.BIN").resolve()),
|
||||
"member": "", "reason": ""},
|
||||
])
|
||||
kinds = [e["event"] for e in events]
|
||||
self.assertLess(kinds.index("found"), kinds.index("download"))
|
||||
copied = {e["dest"] for e in events if e["event"] == "downloaded" and e["ok"]}
|
||||
self.assertEqual(copied, {"gba_bios.bin", "scph5501.bin"})
|
||||
summary = next(e for e in events if e["event"] == "summary")
|
||||
self.assertEqual(summary["installed"], 2)
|
||||
|
||||
def test_a_check_writes_the_same_events_and_nothing_else(self):
|
||||
self.keep("BIOS/SCPH5501.BIN", US)
|
||||
proc = self.run_installer("--check", "--events", "--manifest", str(self.list),
|
||||
"--from", str(self.old))
|
||||
events = _events(proc)
|
||||
self.assertEqual([e["dest"] for e in events if e["event"] == "found"],
|
||||
["scph5501.bin"])
|
||||
self.assertNotIn("download", [e["event"] for e in events])
|
||||
self.assertFalse(self.dest.exists())
|
||||
|
||||
def test_a_folder_that_does_not_exist_is_an_error_event(self):
|
||||
proc = self.run_installer("--events", "--manifest", str(self.list), "--from",
|
||||
str(self.root / "nowhere"))
|
||||
events = _events(proc)
|
||||
error = next(e for e in events if e["event"] == "error")
|
||||
self.assertEqual((error["stage"], error["reason"]), ("from", "not_found"))
|
||||
self.assertEqual(events[-1], {"event": "end", "code": 1})
|
||||
|
||||
|
||||
class TheInterfaceReadsAFoundEvent(unittest.TestCase):
|
||||
"""The full-screen interface drives the engine through --events. A
|
||||
found event, which an install writes for a misplaced file even without
|
||||
--from, leaves its screens as they were."""
|
||||
|
||||
FOUND = {"event": "found", "platform": "retroarch", "dest": "scph5501.bin",
|
||||
"folder": "/bios", "path": "/bios/bios/scph5501.bin", "member": "",
|
||||
"reason": "nested folder: it is at bios/bios/scph5501.bin"}
|
||||
|
||||
def test_every_run_ignores_it(self):
|
||||
for run in ("check", "confirm", "preview", "install"):
|
||||
with self.subTest(run=run):
|
||||
state = install.TuiState()
|
||||
state.run, state.run_id, state.running = run, 3, True
|
||||
before = copy.deepcopy(vars(state))
|
||||
self.assertEqual(install.tui_receive(state, ("event", 3, dict(self.FOUND))),
|
||||
[])
|
||||
self.assertEqual(vars(state), before)
|
||||
|
||||
|
||||
class MisplacedInTheBiosFolder(Scratch):
|
||||
"""F12: the files are in the BIOS folder, at a place no core opens."""
|
||||
|
||||
def install(self, *options: str) -> subprocess.CompletedProcess:
|
||||
return self.run_installer("--manifest", str(self.list), "--no-input", *options)
|
||||
|
||||
def test_a_nested_bios_folder_is_fixed_and_left_in_place(self):
|
||||
for name, data in PAYLOAD.items():
|
||||
self.place(f"system/{name}", data)
|
||||
proc = self.install()
|
||||
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
|
||||
for name, data in PAYLOAD.items():
|
||||
self.assertEqual((self.dest / name).read_bytes(), data)
|
||||
self.assertEqual((self.dest / "system" / name).read_bytes(), data)
|
||||
self.assertIn("4 files fixed from copies already in the BIOS folder.", proc.stdout)
|
||||
self.assertIn("nested folder: it is at system/system/scph5501.bin", proc.stdout)
|
||||
|
||||
def test_a_pack_extracted_into_a_folder_of_its_own_is_fixed(self):
|
||||
for name, data in PAYLOAD.items():
|
||||
self.place(f"RetroArch_BIOS_Pack/{name}", data)
|
||||
proc = self.install()
|
||||
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
|
||||
self.assertIn("one folder too deep: it is at RetroArch_BIOS_Pack/scph5501.bin",
|
||||
proc.stdout)
|
||||
self.assertEqual((self.dest / "pcsx" / "extra.bin").read_bytes(), EXTRA)
|
||||
|
||||
def test_a_name_in_capitals_is_fixed_where_case_counts(self):
|
||||
probe = self.root / "CaseProbe"
|
||||
probe.write_text("x")
|
||||
if (self.root / "caseprobe").exists():
|
||||
self.skipTest("this filesystem does not keep case")
|
||||
self.complete("gba_bios.bin")
|
||||
self.place("GBA_BIOS.BIN", GBA)
|
||||
check = self.check()
|
||||
self.assertIn("case of the name: the folder holds GBA_BIOS.BIN, the cores open "
|
||||
"gba_bios.bin", check)
|
||||
proc = self.install()
|
||||
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
|
||||
self.assertEqual((self.dest / "gba_bios.bin").read_bytes(), GBA)
|
||||
self.assertEqual((self.dest / "GBA_BIOS.BIN").read_bytes(), GBA)
|
||||
self.assertIn("1 file fixed from copies already in the BIOS folder.", proc.stdout)
|
||||
|
||||
def test_a_file_left_in_its_zip_is_extracted(self):
|
||||
self.complete("gba_bios.bin")
|
||||
_zip(self.dest / "GBA BIOS.zip", {"bios/gba_bios.bin": GBA})
|
||||
proc = self.install()
|
||||
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
|
||||
self.assertEqual((self.dest / "gba_bios.bin").read_bytes(), GBA)
|
||||
self.assertTrue((self.dest / "GBA BIOS.zip").is_file())
|
||||
|
||||
def test_a_file_above_the_folder_is_fixed(self):
|
||||
self.complete("gba_bios.bin")
|
||||
(self.root / "gba_bios.bin").write_bytes(GBA)
|
||||
proc = self.install()
|
||||
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
|
||||
self.assertEqual((self.dest / "gba_bios.bin").read_bytes(), GBA)
|
||||
self.assertTrue((self.root / "gba_bios.bin").is_file())
|
||||
|
||||
def test_a_wrong_file_in_place_is_kept_before_the_right_bytes_take_its_place(self):
|
||||
self.complete("gba_bios.bin")
|
||||
wrong = b"y" * len(GBA)
|
||||
self.place("gba_bios.bin", wrong)
|
||||
self.place("old/gba_bios.bin", GBA)
|
||||
proc = self.install()
|
||||
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
|
||||
self.assertEqual((self.dest / "gba_bios.bin").read_bytes(), GBA)
|
||||
kept = list((self.root / install.BACKUP_DIR).rglob("gba_bios.bin"))
|
||||
self.assertEqual([p.read_bytes() for p in kept], [wrong])
|
||||
|
||||
def test_check_says_a_run_puts_it_in_place(self):
|
||||
for name, data in PAYLOAD.items():
|
||||
self.place(f"system/{name}", data)
|
||||
out = self.check()
|
||||
self.assertIn("4 of these files are in the BIOS folder at the wrong place: a run "
|
||||
"without --check copies them where the cores open them, and leaves "
|
||||
"your copies as they are.", out)
|
||||
self.assertFalse((self.dest / "scph5501.bin").exists())
|
||||
|
||||
def test_dry_run_counts_them_and_writes_nothing(self):
|
||||
for name, data in PAYLOAD.items():
|
||||
self.place(f"system/{name}", data)
|
||||
before = _snapshot(self.dest)
|
||||
proc = self.install("--dry-run")
|
||||
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
|
||||
self.assertIn("Would fix 4 files", proc.stdout)
|
||||
self.assertEqual(_snapshot(self.dest), before)
|
||||
|
||||
def test_a_mac_file_alone_is_named(self):
|
||||
self.complete("gba_bios.bin")
|
||||
self.place("._gba_bios.bin", b"\x00\x05\x16\x07" + b"\x00" * 60)
|
||||
self.assertIn("only a Mac's ._ file: ._gba_bios.bin holds Finder data, not the "
|
||||
"file itself", self.check())
|
||||
|
||||
def test_a_copy_at_another_place_of_the_list_is_no_mistake(self):
|
||||
"""np2kai/2608_bd.wav and np2/2608_bd.wav hold the same bytes: the
|
||||
second is copied from the first, and neither is called misplaced."""
|
||||
probe = self.root / "CaseProbe"
|
||||
probe.write_text("x")
|
||||
if (self.root / "caseprobe").exists():
|
||||
self.skipTest("this filesystem does not keep case")
|
||||
data = json.loads(self.list.read_text())
|
||||
twin = next(f for f in data["files"] if f["dest"] == "gba_bios.bin")
|
||||
data["case_variants"] = [{**twin, "dest": "twin/gba_bios.bin"}]
|
||||
self.list.write_text(json.dumps(data))
|
||||
self.complete()
|
||||
check = self.check()
|
||||
self.assertIn("a run without --check copies it from gba_bios.bin", check)
|
||||
self.assertNotIn("wrong place", check)
|
||||
proc = self.install()
|
||||
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
|
||||
self.assertEqual((self.dest / "twin" / "gba_bios.bin").read_bytes(), GBA)
|
||||
self.assertIn("1 file copied from another place of the BIOS folder.", proc.stdout)
|
||||
self.assertNotIn("fixed", proc.stdout)
|
||||
|
||||
def test_a_folder_with_nothing_misplaced_says_nothing(self):
|
||||
with FakeRepository() as repo:
|
||||
proc = repo.run("--no-input")
|
||||
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
|
||||
self.assertNotIn("fixed", proc.stdout)
|
||||
self.assertNotIn("copied from", proc.stdout)
|
||||
|
||||
|
||||
class TheSummaryLine(unittest.TestCase):
|
||||
def test_the_parts_in_words(self):
|
||||
line = install.sources_line([("E:\\", 38)], fixed=0, downloaded=4, missing=2)
|
||||
self.assertEqual(line, "38 files copied from E:\\, 4 downloaded, 2 still missing.")
|
||||
|
||||
def test_one_file_and_fixes(self):
|
||||
self.assertEqual(install.sources_line([("/old", 1)], fixed=3, downloaded=0, missing=0),
|
||||
"1 file copied from /old, 3 fixed from copies already in the "
|
||||
"BIOS folder.")
|
||||
self.assertEqual(install.sources_line([], fixed=2, downloaded=1, missing=0),
|
||||
"2 files fixed from copies already in the BIOS folder, "
|
||||
"1 downloaded.")
|
||||
|
||||
def test_nothing_copied_is_no_line(self):
|
||||
self.assertEqual(install.sources_line([("/old", 0)], 0, 5, 0), "")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1093,6 +1093,19 @@ class ThePlatformsOwnSystemNamesAreAccepted(unittest.TestCase):
|
||||
self.assertIn('"system_names": platform_system_names(config, list(pack_systems))',
|
||||
source)
|
||||
|
||||
def test_every_listed_system_gets_the_title_the_site_prints(self):
|
||||
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||
from generate_pack import system_titles # noqa: PLC0415
|
||||
|
||||
files = [{"dest": "a.bin", "systems": ["sony-playstation", "nintendo-gba"]}]
|
||||
omitted = [{"dest": "b.bin", "systems": ["3do"]}]
|
||||
self.assertEqual(system_titles(files, omitted), {
|
||||
"3do": "3DO",
|
||||
"nintendo-gba": "Nintendo Game Boy Advance",
|
||||
"sony-playstation": "Sony PlayStation",
|
||||
})
|
||||
self.assertEqual(system_titles([{"dest": "c.bin", "systems": ["sys-a"]}]), {})
|
||||
|
||||
def test_a_native_name_narrows_and_lists(self):
|
||||
with FakeRepository() as repo:
|
||||
path = repo.root / "install" / "retroarch.json"
|
||||
|
||||
+410
-17
@@ -484,7 +484,8 @@ class Flow(unittest.TestCase):
|
||||
self.assertEqual(state.screen, "cores")
|
||||
self.assertEqual([row.value for row in install.tui_rows(state)][1:],
|
||||
["mesen", "pcsx_rearmed"])
|
||||
press(state, "down", " ")
|
||||
self.assertEqual(install.tui_rows(state)[state.focus["cores"]].value, "mesen")
|
||||
press(state, " ")
|
||||
self.assertEqual(state.cores, ["mesen"])
|
||||
press(state, "left")
|
||||
self.assertEqual(state.cores, [])
|
||||
@@ -644,13 +645,14 @@ class Flow(unittest.TestCase):
|
||||
state = fresh_state()
|
||||
with mock.patch.object(install.Path, "exists", return_value=True):
|
||||
press(state, "enter")
|
||||
install.tui_receive(state, ("manifest_failed", "retroarch",
|
||||
install.tui_receive(state, ("manifest_failed", "retroarch", "No connection",
|
||||
[" Could not fetch the file list for retroarch", ""], 1))
|
||||
self.assertIn("RetroBIOS: No connection", install.tui_view(state, 80, 24)[0])
|
||||
self.assertEqual(state.screen, "error")
|
||||
self.assertEqual([row.action for row in install.tui_rows(state)],
|
||||
["retry", "back", "quit"])
|
||||
self.assertEqual(press(state, "enter"), [("fetch", "retroarch")])
|
||||
install.tui_receive(state, ("manifest_failed", "retroarch", ["no"], 1))
|
||||
install.tui_receive(state, ("manifest_failed", "retroarch", "No connection", ["no"], 1))
|
||||
press(state, "down", "down", "enter")
|
||||
self.assertEqual(state.exit_code, 1)
|
||||
|
||||
@@ -697,7 +699,13 @@ class PlanWarnings(unittest.TestCase):
|
||||
state = checked_state({"free": 1024, "writable": False, "cores": []})
|
||||
text = " ".join(install.tui_view(state, 120, 40))
|
||||
self.assertIn("Not enough free space for this download", text)
|
||||
self.assertIn("This folder cannot be written", text)
|
||||
self.assertIn("You cannot write to this folder: choose another one, or run the "
|
||||
"installer with sudo.", text)
|
||||
rows = install.tui_rows(state)
|
||||
self.assertEqual([row.action for row in rows[:2]], ["folder", "install"])
|
||||
self.assertEqual(state.focus.get("plan", 0), 0)
|
||||
self.assertEqual(press(state, "enter"), [])
|
||||
self.assertEqual(state.screen, "folder")
|
||||
self.assertNotIn("--installed-cores", [row.right for row in install._option_rows(state)])
|
||||
|
||||
|
||||
@@ -716,7 +724,8 @@ class Widgets(unittest.TestCase):
|
||||
press(state, "end")
|
||||
lines = install.tui_view(state, 40, 15)
|
||||
self.assertIn("> [x] system-111", "\n".join(lines))
|
||||
self.assertTrue(any(re.fullmatch(r" \d+-115 of 115", line) for line in lines), lines)
|
||||
# The All, None and Filter rows are not counted among the systems.
|
||||
self.assertTrue(any(re.fullmatch(r" \d+-112 of 112", line) for line in lines), lines)
|
||||
press(state, "pgup")
|
||||
self.assertLess(state.focus["systems"], 114)
|
||||
press(state, "home")
|
||||
@@ -755,7 +764,10 @@ class Widgets(unittest.TestCase):
|
||||
press(state, "a")
|
||||
self.assertEqual(len(state.ticked), 5)
|
||||
state.focus["systems"] = 1
|
||||
# X on None does nothing: only A, an explicit choice, unticks all.
|
||||
press(state, " ")
|
||||
self.assertEqual(len(state.ticked), 5)
|
||||
press(state, "enter")
|
||||
self.assertEqual(state.ticked, set())
|
||||
|
||||
def test_running_total_counts_each_file_once(self):
|
||||
@@ -909,8 +921,8 @@ class Views(unittest.TestCase):
|
||||
for name, state in golden_states().items():
|
||||
with self.subTest(screen=name):
|
||||
hint = install.tui_view(state, 40, 15)[-1].strip()
|
||||
self.assertTrue(hint.startswith(("A/Enter", "Enter", "Ctrl-C", "Any key")),
|
||||
hint)
|
||||
self.assertTrue(hint.startswith(("A/Enter", "A ", "Enter", "Ctrl-C", "Any key",
|
||||
"B/Esc Stop")), hint)
|
||||
|
||||
def test_focus_is_marked_without_colour(self):
|
||||
lines = install.tui_view(fresh_state(), 80, 24)
|
||||
@@ -1092,6 +1104,19 @@ class SmallFixes(unittest.TestCase):
|
||||
self.assertTrue(state.installed_only)
|
||||
self.assertNotIn("--installed-cores", [f for f, _v in install.tui_flags(state)])
|
||||
|
||||
def test_the_end_of_a_wrapped_pager_is_reachable(self):
|
||||
state = checked_state()
|
||||
state.stack.append("result")
|
||||
state.code, state.summary = 0, {"installed": 3}
|
||||
state.dest = Path("/media/" + "long-folder-name/" * 8)
|
||||
state.cols, state.rows = 40, 12
|
||||
press(state, "end")
|
||||
lines = install.tui_view(state, 40, 12)
|
||||
self.assertIn("python3 install.py", "\n".join(lines))
|
||||
top = state.focus["result"]
|
||||
press(state, "up")
|
||||
self.assertEqual(state.focus["result"], top - 1)
|
||||
|
||||
def test_end_then_up_in_a_pager(self):
|
||||
state = checked_state()
|
||||
state.stack += ["options", "preview"]
|
||||
@@ -1127,7 +1152,7 @@ class SmallFixes(unittest.TestCase):
|
||||
"event": "downloaded", "dest": "a.bin", "ok": True, "reason": "", "size": 1,
|
||||
"files_done": 1, "files_total": 4, "bytes_done": 1, "bytes_total": 100}))
|
||||
press(state, "ctrl-c")
|
||||
self.assertIn("Press Ctrl-C again to quit now.",
|
||||
self.assertIn("Press B or Ctrl-C again to stop now.",
|
||||
" ".join(install.tui_view(state, 120, 40)))
|
||||
self.assertEqual(press(state, "ctrl-c"), [("kill",)])
|
||||
install.tui_receive(state, ("ended", state.run_id, -9, []))
|
||||
@@ -1192,7 +1217,10 @@ class CleanupSurvivesABrokenTerminal(unittest.TestCase):
|
||||
CleanupSurvivesABrokenTerminal.Tty.broken = True
|
||||
|
||||
self.addCleanup(setattr, CleanupSurvivesABrokenTerminal.Tty, "broken", False)
|
||||
work = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp"))
|
||||
self.addCleanup(shutil.rmtree, work, True)
|
||||
with mock.patch.object(install._TuiTerminal, "draw", breaking_draw), \
|
||||
mock.patch.object(install.tempfile, "mkdtemp", return_value=str(work)), \
|
||||
mock.patch.object(install, "tui_start", return_value=(fresh_state(), [])), \
|
||||
mock.patch.object(install, "_open_local_sources"), \
|
||||
mock.patch.object(install, "detect_os", return_value="linux"), \
|
||||
@@ -1201,11 +1229,353 @@ class CleanupSurvivesABrokenTerminal(unittest.TestCase):
|
||||
code = install.run_tui(_args())
|
||||
self.assertIs(sys.stdout, tty)
|
||||
self.assertEqual(code, 1)
|
||||
# The crash keeps its log, and nothing else of the run.
|
||||
self.assertEqual(sorted(path.name for path in work.iterdir()), ["error.log"])
|
||||
self.assertEqual(termios.tcgetattr(slave), before)
|
||||
for number, handler in handlers.items():
|
||||
self.assertEqual(signal.getsignal(number), handler)
|
||||
|
||||
|
||||
class AuditRoundTwo(unittest.TestCase):
|
||||
"""tui_audit_1.md, each finding held where it was found."""
|
||||
|
||||
def systems(self) -> install.TuiState:
|
||||
state = checked_state()
|
||||
press(state, "down", "enter")
|
||||
self.assertEqual(state.screen, "systems")
|
||||
return state
|
||||
|
||||
def focused(self, state: install.TuiState) -> install.TuiRow:
|
||||
rows = install.tui_rows(state)
|
||||
return rows[state.focus[state.screen]]
|
||||
|
||||
def test_p1_3_the_filter_never_changes_the_ticks(self):
|
||||
state = self.systems()
|
||||
press(state, "n")
|
||||
state.focus["systems"] = next(i for i, row in enumerate(install.tui_rows(state))
|
||||
if row.value == "nintendo-gba")
|
||||
press(state, "right")
|
||||
self.assertEqual(state.ticked, {"nintendo-gba"})
|
||||
press(state, "/", "p", "l", "a", "y")
|
||||
self.assertEqual(self.focused(state).action, "filter")
|
||||
press(state, "enter")
|
||||
# Enter lands on the first match, never on All or None.
|
||||
self.assertEqual(self.focused(state).value, "sony-playstation")
|
||||
press(state, " ")
|
||||
self.assertEqual(state.ticked, {"nintendo-gba", "sony-playstation"})
|
||||
press(state, " ")
|
||||
press(state, "esc")
|
||||
# The filter is cleared, the row in focus stays in focus, and A there
|
||||
# continues instead of ticking everything.
|
||||
self.assertEqual(state.filters["systems"], "")
|
||||
self.assertEqual(self.focused(state).value, "sony-playstation")
|
||||
self.assertEqual(state.ticked, {"nintendo-gba"})
|
||||
self.assertIn("A/Enter Continue", install.tui_view(state, 80, 24)[-1])
|
||||
state.focus["systems"] = 2
|
||||
press(state, " ")
|
||||
self.assertEqual(state.editing, "")
|
||||
self.assertEqual(state.ticked, {"nintendo-gba"})
|
||||
|
||||
def test_p2_2_the_bar_names_what_a_does_on_the_focused_row(self):
|
||||
state = checked_state()
|
||||
self.assertTrue(install.tui_view(state, 80, 24)[-1].strip().startswith("A/Enter Install"))
|
||||
press(state, "down")
|
||||
self.assertTrue(install.tui_view(state, 80, 24)[-1].strip().startswith("A/Enter Open"))
|
||||
state = self.systems()
|
||||
state.focus["systems"] = 0
|
||||
self.assertIn("A/Enter Tick all", install.tui_view(state, 80, 24)[-1])
|
||||
|
||||
def test_p2_2_b_esc_and_q_stop_an_install(self):
|
||||
for key in ("esc", "backspace", "q"):
|
||||
with self.subTest(key=key):
|
||||
state = checked_state()
|
||||
press(state, "enter")
|
||||
self.assertEqual(press(state, key), [("stop",)])
|
||||
self.assertTrue(state.stopping)
|
||||
self.assertEqual(press(state, key), [("kill",)])
|
||||
|
||||
def test_p2_3_options_read_the_plan_again(self):
|
||||
state = self.systems()
|
||||
press(state, "n")
|
||||
state.focus["systems"] = 3
|
||||
press(state, "right")
|
||||
ticked = set(state.ticked)
|
||||
press(state, "esc", "o")
|
||||
self.assertEqual(state.screen, "options")
|
||||
state.focus["options"] = 1
|
||||
press(state, "enter")
|
||||
self.assertTrue(state.no_game_data)
|
||||
effects = press(state, "esc")
|
||||
self.assertEqual(effects, [("run", "check")])
|
||||
self.assertIn("--no-game-data", [f for f, _v in state.checked_flags])
|
||||
self.assertNotIn("--system", [f for f, _v in state.checked_flags])
|
||||
for event in [*FILES, *SYSTEMS, {**CHECK, "missing": 2, "bytes_needed": 600}]:
|
||||
install.tui_receive(state, ("event", state.run_id, event))
|
||||
with mock.patch.object(install, "_tui_folder_facts", return_value={}):
|
||||
install.tui_receive(state, ("ended", state.run_id, 1, []))
|
||||
self.assertEqual(state.screen, "plan")
|
||||
self.assertEqual(state.ticked, ticked)
|
||||
for cols, rows in ((80, 24), (40, 15)):
|
||||
text = "\n".join(install.tui_view(state, cols, rows))
|
||||
self.assertIn("3 files to download, 600 B", text)
|
||||
self.assertIn("For your 1 of 3 systems", text)
|
||||
|
||||
def test_p2_4_no_network_is_said_at_once(self):
|
||||
import socket
|
||||
|
||||
with socket.socket() as probe:
|
||||
probe.bind(("127.0.0.1", 0))
|
||||
port = probe.getsockname()[1]
|
||||
inbox = __import__("queue").Queue()
|
||||
started = time.monotonic()
|
||||
with mock.patch.object(install, "MANIFEST_URL",
|
||||
f"http://127.0.0.1:{port}/install/{{platform}}.json"):
|
||||
install._tui_fetch("retroarch", inbox, io.StringIO())
|
||||
self.assertLess(time.monotonic() - started, 5)
|
||||
kind, plat, title, lines, code = inbox.get_nowait()
|
||||
self.assertEqual((kind, title, code), ("manifest_failed", "No connection", 1))
|
||||
self.assertIn("could not be downloaded: the server refused the connection", lines[0])
|
||||
self.assertNotIn("--", " ".join(lines))
|
||||
state = fresh_state()
|
||||
with mock.patch.object(install.Path, "exists", return_value=True):
|
||||
press(state, "enter")
|
||||
install.tui_receive(state, (kind, plat, title, lines, code))
|
||||
self.assertEqual([row.action for row in install.tui_rows(state)],
|
||||
["retry", "back", "quit"])
|
||||
|
||||
def test_p2_4_the_clock_says_so(self):
|
||||
import ssl
|
||||
|
||||
inbox = __import__("queue").Queue()
|
||||
error = urllib_error(ssl.SSLCertVerificationError("certificate is not yet valid"))
|
||||
words = install.network_words(error, today=install.datetime.date(1980, 1, 1))
|
||||
with mock.patch.object(install, "_fetch_json", side_effect=error), \
|
||||
mock.patch.object(install, "network_words", return_value=words):
|
||||
install._tui_fetch("retroarch", inbox, io.StringIO())
|
||||
_kind, _plat, title, lines, _code = inbox.get_nowait()
|
||||
self.assertEqual(title, "The clock is wrong")
|
||||
self.assertIn("The clock reads 1980-01-01", lines[0])
|
||||
|
||||
def test_p2_5_one_line_per_cause(self):
|
||||
state = checked_state()
|
||||
state.stack.append("result")
|
||||
state.code = 2
|
||||
state.summary = {"installed": 0, "up_to_date": 0, "failed": [
|
||||
{"dest": f"f{n}.bin", "reason": "folder not writable"} for n in range(50)]}
|
||||
_title, lines = install.tui_outcome(state)
|
||||
self.assertEqual([line for line in lines if "failed" in line],
|
||||
["Nothing was installed: 50 files failed.",
|
||||
" 50 files failed (folder not writable), the first f0.bin"])
|
||||
|
||||
def test_p2_6_a_folder_inside_a_card_offers_its_root(self):
|
||||
card = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp"))
|
||||
self.addCleanup(shutil.rmtree, card, True)
|
||||
(card / ".system").mkdir()
|
||||
(card / ".system" / "version.txt").write_text("MinUI-20250101\n")
|
||||
(card / "Bios").mkdir()
|
||||
state = fresh_state()
|
||||
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))
|
||||
self.assertIn(f"under the card's root: {card}", text)
|
||||
self.assertEqual(press(state, "enter"), [("fetch", "minui")])
|
||||
self.assertEqual(state.dest, card)
|
||||
|
||||
def test_p2_6_the_banner_is_not_an_error_line(self):
|
||||
state = fresh_state()
|
||||
state.platform, state.dest = "retroarch", SYSTEM_DIR
|
||||
state.manifest, state.manifest_for = MANIFEST, "retroarch"
|
||||
install._start_check(state)
|
||||
install.tui_receive(state, ("ended", state.run_id, 1,
|
||||
["RetroBIOS 532f0b8d96e0", "Error: refused."]))
|
||||
self.assertEqual(state.error, ["Error: refused."])
|
||||
self.assertNotIn("retry", [row.action for row in install.tui_rows(state)])
|
||||
|
||||
def test_p2_7_a_typed_path_replaces_the_one_offered(self):
|
||||
state = fresh_state()
|
||||
state.platform, state.stack = "retroarch", ["folder"]
|
||||
state.folder_default = SYSTEM_DIR
|
||||
state.focus["folder"] = 2
|
||||
press(state, "enter")
|
||||
self.assertEqual((state.screen, state.path_selected), ("path", True))
|
||||
install.tui_update(state, Key("paste", '"/media/sd card/bios"'))
|
||||
self.assertEqual(state.path_text, '"/media/sd card/bios"')
|
||||
install.tui_update(state, Key("ctrl-u"))
|
||||
self.assertEqual(state.path_text, "")
|
||||
install.tui_update(state, Key("paste", '/media/x"'))
|
||||
self.assertEqual(install.typed_path(state.path_text), Path("/media/x"))
|
||||
|
||||
def test_p2_7_a_path_from_the_root_is_not_created_on_a(self):
|
||||
state = fresh_state()
|
||||
state.stack = ["folder"]
|
||||
install._choose(state, "retroarch", Path("/no/such/deep/folder"))
|
||||
self.assertEqual(state.screen, "create")
|
||||
self.assertEqual(self.focused(state).action, "other")
|
||||
self.assertIn("Every folder of this path would be made",
|
||||
" ".join(install.tui_view(state, 120, 40)))
|
||||
near = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp"))
|
||||
self.addCleanup(shutil.rmtree, near, True)
|
||||
install._choose(state, "retroarch", near / "bios")
|
||||
self.assertEqual(self.focused(state).action, "create")
|
||||
|
||||
def test_p2_9_two_setups_of_one_name(self):
|
||||
flatpak = HOME / ".var/app/org.libretro.RetroArch/config/retroarch/system"
|
||||
setups = install.tell_apart([("retroarch", flatpak, "RetroArch"),
|
||||
("retroarch", SYSTEM_DIR, "RetroArch"),
|
||||
("emudeck", HOME / "Emulation/bios", "EmuDeck")])
|
||||
self.assertEqual([label for _p, _f, label in setups],
|
||||
["RetroArch (Flatpak)", "RetroArch (retroarch/system)", "EmuDeck"])
|
||||
state = fresh_state()
|
||||
state.setups = setups
|
||||
self.assertIn("RetroArch (Flatpak)", "\n".join(install.tui_view(state, 40, 15)))
|
||||
# A Flatpak whose config names a system folder outside its sandbox.
|
||||
elsewhere = HOME / "bios"
|
||||
config = HOME / ".var/app/org.libretro.RetroArch/config/retroarch/retroarch.cfg"
|
||||
with mock.patch.dict(install._RETROARCH_CONFIGS, {elsewhere: (config, config.parent)}):
|
||||
setups = install.tell_apart([("retroarch", elsewhere, "RetroArch"),
|
||||
("retroarch", SYSTEM_DIR, "RetroArch")])
|
||||
self.assertEqual(setups[0][2], "RetroArch (Flatpak)")
|
||||
|
||||
def test_p2_10_the_check_counts_while_it_reads(self):
|
||||
state = fresh_state()
|
||||
state.platform, state.dest = "retroarch", SYSTEM_DIR
|
||||
state.manifest, state.manifest_for = MANIFEST, "retroarch"
|
||||
install._start_check(state)
|
||||
install.tui_receive(state, ("event", state.run_id,
|
||||
{"event": "checking", "files_done": 120, "files_total": 400}))
|
||||
self.assertIn("Reading the files already in the folder: 120 of 400",
|
||||
"\n".join(install.tui_view(state, 80, 24)))
|
||||
|
||||
def test_p2_11_a_narrow_bar_keeps_the_keys_to_finish(self):
|
||||
state = self.systems()
|
||||
self.assertEqual(install.tui_view(state, 40, 15)[-1].strip(),
|
||||
"A Continue B Back X Tick ? Help")
|
||||
|
||||
def test_p2_12_a_device_name_finds_its_setup(self):
|
||||
state = fresh_state()
|
||||
state.stack = ["platforms"]
|
||||
press(state, "/", "m", "u", "o", "s")
|
||||
self.assertEqual([row.value for row in install.tui_rows(state)][1:], ["retroarch"])
|
||||
|
||||
def test_p2_13_a_long_preview_wraps_only_what_is_in_view(self):
|
||||
state = checked_state()
|
||||
state.stack += ["options", "preview"]
|
||||
state.dry = dict(CHECK)
|
||||
state.dry_files = [{**FILES[0], "dest": f"file{n:05d}.bin"} for n in range(8000)]
|
||||
state.cols, state.rows = 40, 15
|
||||
real = install._text_lines
|
||||
calls = []
|
||||
|
||||
def counted(items, width):
|
||||
calls.append(len(items))
|
||||
return real(items, width)
|
||||
|
||||
with mock.patch.object(install, "_text_lines", counted):
|
||||
press(state, "end")
|
||||
lines = install.tui_view(state, 40, 15)
|
||||
self.assertLess(sum(calls), 40)
|
||||
self.assertIn("file07999.bin", "\n".join(lines))
|
||||
|
||||
def test_p2_1_titles_and_regions(self):
|
||||
state = checked_state()
|
||||
state.manifest = {**MANIFEST, "system_titles": {"nintendo-gba": "Game Boy Advance"}}
|
||||
state.stack.append("systems")
|
||||
text = "\n".join(install.tui_view(state, 40, 15))
|
||||
self.assertIn("[x] Game Boy Advance", text)
|
||||
self.assertEqual(install.region_title("asia-ntsc"), "Asia (NTSC)")
|
||||
self.assertEqual(install.region_title("north-america"), "North America")
|
||||
|
||||
def test_p3_jobs_stop_at_the_ends_and_q_keeps_the_error_code(self):
|
||||
state = checked_state()
|
||||
state.jobs = 1
|
||||
install._option(state, "jobs", -1)
|
||||
self.assertEqual(state.jobs, 1)
|
||||
state.jobs = 32
|
||||
install._option(state, "jobs", 1)
|
||||
self.assertEqual(state.jobs, 32)
|
||||
state = fresh_state()
|
||||
state.stack, state.error_code = ["setup", "error"], 2
|
||||
press(state, "q")
|
||||
self.assertEqual(state.exit_code, 2)
|
||||
|
||||
def test_p3_ascii_keeps_the_columns(self):
|
||||
self.assertEqual(install.ascii_text("Édition ファミ"), "Edition ??????")
|
||||
|
||||
def test_p3_a_refused_tui_says_why_without_the_usage(self):
|
||||
proc = install_run("--tui")
|
||||
self.assertEqual(proc.returncode, 2)
|
||||
self.assertEqual(proc.stderr.strip().splitlines(),
|
||||
["install.py: error: --tui cannot open the full-screen interface "
|
||||
"here: the input is not a terminal"])
|
||||
|
||||
|
||||
def urllib_error(reason: BaseException):
|
||||
import urllib.error
|
||||
|
||||
return urllib.error.URLError(reason)
|
||||
|
||||
|
||||
class WorkFolders(unittest.TestCase):
|
||||
"""Every run's folder of logs goes, but the one a failure names."""
|
||||
|
||||
class Terminal:
|
||||
state_to = None
|
||||
|
||||
def __init__(self, *_streams) -> None:
|
||||
self.frame = []
|
||||
|
||||
def enter(self) -> None:
|
||||
pass
|
||||
|
||||
def leave(self) -> None:
|
||||
pass
|
||||
|
||||
def size(self) -> tuple:
|
||||
return 80, 24
|
||||
|
||||
def draw(self, lines, cols) -> None:
|
||||
pass
|
||||
|
||||
def keys(self, timeout):
|
||||
WorkFolders.Terminal.state_to(self)
|
||||
return [Key("enter")]
|
||||
|
||||
def run_to(self, code: int) -> tuple:
|
||||
work = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp"))
|
||||
self.addCleanup(shutil.rmtree, work, True)
|
||||
state = checked_state()
|
||||
state.stack.append("result")
|
||||
state.code = code
|
||||
state.log = str(work / "install.log")
|
||||
state.summary = {"installed": 1, "failed": [] if code == 0 else [
|
||||
{"dest": "a.bin", "reason": "HTTP 404"}]}
|
||||
WorkFolders.Terminal.state_to = lambda _t: None
|
||||
out = io.StringIO()
|
||||
with mock.patch.object(install, "_TuiTerminal", self.Terminal), \
|
||||
mock.patch.object(install, "tui_start", return_value=(state, [])), \
|
||||
mock.patch.object(install, "_open_local_sources"), \
|
||||
mock.patch.object(install, "detect_os", return_value="linux"), \
|
||||
mock.patch.object(install.tempfile, "mkdtemp", return_value=str(work)), \
|
||||
mock.patch.object(sys, "stdout", out), mock.patch.object(sys, "stderr", out):
|
||||
self.assertEqual(install.run_tui(_args()), code)
|
||||
return work, out.getvalue()
|
||||
|
||||
def test_a_success_leaves_nothing(self):
|
||||
work, out = self.run_to(0)
|
||||
self.assertFalse(work.exists())
|
||||
self.assertNotIn("Details:", out)
|
||||
|
||||
def test_a_failure_keeps_the_log_it_names(self):
|
||||
work, out = self.run_to(1)
|
||||
self.assertTrue(work.exists())
|
||||
self.assertIn(f"Details: {work / 'install.log'}", out)
|
||||
|
||||
def test_a_stop_leaves_nothing(self):
|
||||
work, out = self.run_to(130)
|
||||
self.assertFalse(work.exists())
|
||||
|
||||
|
||||
class Crash(unittest.TestCase):
|
||||
"""An unexpected error ends on one sentence and a log, never a traceback."""
|
||||
|
||||
@@ -1441,13 +1811,11 @@ class EndToEnd(unittest.TestCase):
|
||||
term.send("\r")
|
||||
term.wait("Preferred regions")
|
||||
term.send(DOWN, "\r", "\x1b")
|
||||
screen = term.wait("Preferred regions: north-america")
|
||||
screen = term.wait("Preferred regions: North America")
|
||||
term.send(DOWN, RIGHT, "\x1b")
|
||||
screen = term.wait("> Install")
|
||||
self.assertIn("Your choices: --region north-america --no-game-data", screen)
|
||||
term.send("\r")
|
||||
# Back on the plan, the check runs again under the new options.
|
||||
screen = term.wait("4 files to download")
|
||||
self.assertIn("RetroBIOS: Confirm", screen)
|
||||
self.assertIn("> Install", screen)
|
||||
term.send("\r")
|
||||
screen = term.wait("installed.")
|
||||
self.assertIn("Left out as game data: 1 file.", screen)
|
||||
@@ -1477,11 +1845,36 @@ class EndToEnd(unittest.TestCase):
|
||||
term.wait("1 of 6 files")
|
||||
term.send("\x03")
|
||||
screen = term.wait("Stopped:")
|
||||
self.assertRegex(screen, r"Stopped: \d of 6 installed, run again to finish\.")
|
||||
term.send("\r")
|
||||
self.assertEqual(term.close(), 130)
|
||||
self.assertLess(len(repo.installed()), 6)
|
||||
self.assertIn("RetroBIOS: Stopped", term.after())
|
||||
# The count said is the count on disk: "6 of 6" after a stop at 1
|
||||
# passed a regex and was false.
|
||||
on_disk = len(repo.installed())
|
||||
self.assertLess(on_disk, 6)
|
||||
self.assertIn(f"Stopped: {on_disk} of 6 installed, run again to finish.", screen)
|
||||
self.assertIn(f"Stopped: {on_disk} of 6 installed", term.after())
|
||||
|
||||
def test_sigterm_during_the_download_is_a_stop(self):
|
||||
"""Start+Select on a handheld is gptokeyb's pkill: SIGTERM, SIGKILL 3 s on."""
|
||||
import signal
|
||||
|
||||
repo = self.repo(GameDataRepository, slow=True)
|
||||
term = self.start("--platform", "retroarch", "--dest", str(repo.dest), "--jobs", "1")
|
||||
term.wait("> Install")
|
||||
term.send("\r")
|
||||
term.wait("1 of 6 files")
|
||||
started = time.monotonic()
|
||||
os.kill(term.pid, signal.SIGTERM)
|
||||
code = term.close()
|
||||
self.assertLess(time.monotonic() - started, 3)
|
||||
self.assertEqual(code, 130)
|
||||
on_disk = len(repo.installed())
|
||||
self.assertGreaterEqual(on_disk, 1)
|
||||
after = term.after()
|
||||
self.assertIn(f"Stopped: {on_disk} of 6 installed, run again to finish.", after)
|
||||
self.assertNotIn("Nothing was installed", after)
|
||||
time.sleep(1)
|
||||
self.assertEqual(len(repo.installed()), on_disk)
|
||||
|
||||
def test_ctrl_c_on_the_first_screen(self):
|
||||
self.repo()
|
||||
@@ -1524,7 +1917,7 @@ class EndToEnd(unittest.TestCase):
|
||||
term.resize(100, 30)
|
||||
term.send(DOWN)
|
||||
screen = term.wait("> Another setup or folder")
|
||||
self.assertIn("A/Enter Continue", screen.splitlines()[29])
|
||||
self.assertIn("B/Esc Quit", screen.splitlines()[29])
|
||||
term.resize(30, 10)
|
||||
term.wait("Make the window larger")
|
||||
term.resize(40, 12)
|
||||
|
||||
+377
-36
@@ -387,6 +387,14 @@ class TestAnchorBlock(unittest.TestCase):
|
||||
self.assertEqual(result.status, "CHANGED")
|
||||
self.assertEqual(result.start, line)
|
||||
|
||||
def test_a_file_rewritten_line_for_line_is_changed_not_gone(self):
|
||||
# fheroes2 version.txt:1 went from 1.0.11 to 1.1.17. With no
|
||||
# unchanged line left the file read GONE and held the whole recale.
|
||||
result = anchor_block(["1.0.11"], ["1.1.17"], 1, 1)
|
||||
self.assertEqual((result.status, result.start, result.end), ("CHANGED", 1, 1))
|
||||
result = anchor_block(["a = 1", "b = 2"], ["a = 3", "b = 4"], 2, 2)
|
||||
self.assertEqual((result.status, result.start), ("CHANGED", 2))
|
||||
|
||||
def test_gone_when_content_vanished(self):
|
||||
result = anchor_block(["a", "unique marker", "b"], ["x"] * 200, 2, 2)
|
||||
self.assertEqual(result.status, "GONE")
|
||||
@@ -1173,6 +1181,12 @@ class TestBuildReport(unittest.TestCase):
|
||||
profile_sync.upstream._http_text,
|
||||
profile_sync.upstream.list_gitlinks,
|
||||
profile_sync.upstream.current_slug,
|
||||
profile_sync.upstream.fork_base,
|
||||
)
|
||||
self.fork_bases: dict[str, str] = {}
|
||||
profile_sync.upstream.fork_base = (
|
||||
lambda repo, sha, fork, fork_sha, cache_dir, offline=False:
|
||||
self.fork_bases.get(fork_sha)
|
||||
)
|
||||
self.current_slugs: dict[str, str] = {}
|
||||
profile_sync.upstream.current_slug = (
|
||||
@@ -1235,6 +1249,7 @@ class TestBuildReport(unittest.TestCase):
|
||||
profile_sync.upstream._http_text,
|
||||
profile_sync.upstream.list_gitlinks,
|
||||
profile_sync.upstream.current_slug,
|
||||
profile_sync.upstream.fork_base,
|
||||
) = self._orig
|
||||
self.tmp.cleanup()
|
||||
|
||||
@@ -2723,6 +2738,21 @@ class TestWriteDryRun(unittest.TestCase):
|
||||
self.assertNotIn("source_commit", output)
|
||||
self.assertEqual(self.path.read_text(), before)
|
||||
|
||||
def test_a_bump_that_leaves_stale_prose_is_rolled_back(self):
|
||||
"""dolphin-mmjr2, hatari, xenios: the bump moved upstream_commit only,
|
||||
and prose the report had judged in the fork fell under the new
|
||||
upstream pin. The guard had read it under the old pins."""
|
||||
from unittest import mock
|
||||
|
||||
before = self.path.read_text()
|
||||
answers = iter([[], ["notes: a.c:10 (a.c:10 changed)"]])
|
||||
with mock.patch.object(
|
||||
profile_sync, "realign_prose", side_effect=lambda *a, **k: next(answers)
|
||||
), contextlib.redirect_stderr(io.StringIO()) as err:
|
||||
self._run(self._args(rebase_refs=True, dry_run=False), self._shifted())
|
||||
self.assertEqual(self.path.read_text(), before)
|
||||
self.assertIn("nothing was written", err.getvalue())
|
||||
|
||||
def test_a_planned_bump_reads_the_prose_the_rebase_would_have_left(self):
|
||||
"""The pin is blocked by prose until the rebase moves it.
|
||||
|
||||
@@ -2812,17 +2842,30 @@ class TestDeliberatelyFrozenPin(TestBuildReport):
|
||||
source = profile.get("source")
|
||||
if isinstance(source, str):
|
||||
by_source.setdefault(source.rstrip("/").lower(), []).append(name)
|
||||
# A version a platform builds (built_by: mame0258, mame2014) is not
|
||||
# the same program as the repository's other cores: mame2015-libretro
|
||||
# builds mame2014, mess2015 and cdi2015, three system sets. And a
|
||||
# live build can add a system its frozen revision lacked
|
||||
# (advancemame's coh1002v, after advmame-1.4), so the frozen one's
|
||||
# systems are a subset, not an equal set.
|
||||
pairs = 0
|
||||
for names in by_source.values():
|
||||
frozen = [n for n in names if str(profiles[n].get("pin_frozen") or "").strip()]
|
||||
live = [n for n in names if n not in frozen]
|
||||
frozen = [
|
||||
n for n in names
|
||||
if str(profiles[n].get("pin_frozen") or "").strip()
|
||||
and not profiles[n].get("built_by")
|
||||
]
|
||||
live = [
|
||||
n for n in names
|
||||
if n not in frozen and not str(profiles[n].get("pin_frozen") or "").strip()
|
||||
]
|
||||
for old in frozen:
|
||||
for new in live:
|
||||
pairs += 1
|
||||
with self.subTest(frozen=old, live=new):
|
||||
self.assertEqual(
|
||||
sorted(profiles[old].get("systems") or []),
|
||||
sorted(profiles[new].get("systems") or []),
|
||||
self.assertLessEqual(
|
||||
set(profiles[old].get("systems") or []),
|
||||
set(profiles[new].get("systems") or []),
|
||||
)
|
||||
if (old, new) not in builds_differ:
|
||||
self.assertEqual(profiles[old].get("type"), profiles[new].get("type"))
|
||||
@@ -2961,6 +3004,34 @@ class TestRebaseWaitsForThePin(unittest.TestCase):
|
||||
self.assertFalse(profile_sync.pending_recale(report))
|
||||
self.assertEqual(rebase_refs(self.path, report), ["a.c:10-12 -> a.c:30-32"])
|
||||
|
||||
def test_chained_shifts_are_judged_at_their_own_entry(self):
|
||||
# tr1x: cut2 508->531, cut3 531->552, cut4 552->575. Once written,
|
||||
# cut3 and cut4 read as pending on the values cut2 and cut3 took.
|
||||
text = (
|
||||
'emulator: Test\nsource: "https://github.com/o/n"\n'
|
||||
'source_commit: "pin"\nfiles:\n'
|
||||
+ "".join(
|
||||
f' - name: "cut{n}.mp3"\n source_ref: "g.json5:{line}"\n'
|
||||
for n, line in ((2, 508), (3, 531), (4, 552))
|
||||
)
|
||||
)
|
||||
self.path.write_text(text, encoding="utf-8")
|
||||
entries = [
|
||||
EntryReport(
|
||||
f"cut{n}.mp3", f"g.json5:{old}", "SHIFTED",
|
||||
[PartResult(RefPart("g.json5", old, old), "SHIFTED", None, new, new, [])],
|
||||
"ref", f"files[cut{n}.mp3].source_ref",
|
||||
)
|
||||
for n, old, new in ((2, 508, 531), (3, 531, 552), (4, 552, 575))
|
||||
]
|
||||
report = self._report(entries, {"SHIFTED": 3})
|
||||
self.assertEqual(len(rebase_refs(self.path, report)), 3)
|
||||
written = self.path.read_text()
|
||||
self.assertIn('source_ref: "g.json5:575"', written)
|
||||
self.assertEqual(
|
||||
profile_sync.pending_recale(report, text=written), 0
|
||||
)
|
||||
|
||||
|
||||
class TestBumpCommit(unittest.TestCase):
|
||||
def setUp(self):
|
||||
@@ -2999,6 +3070,24 @@ class TestBumpCommit(unittest.TestCase):
|
||||
self.assertEqual(profile_sync.pending_recale(report), 0)
|
||||
self.assertTrue(bump_commit(self.path, report))
|
||||
|
||||
def test_refused_on_a_build_a_platform_compiles(self):
|
||||
# mame0258 documents the MAME Recalbox builds: libretro/mame HEAD
|
||||
# is a later release, and every ref still anchors there.
|
||||
self.path.write_text(SAMPLE + "built_by: [recalbox]\n", encoding="utf-8")
|
||||
part = PartResult(
|
||||
RefPart("a.c", 10, 12, "a.c:10-12"), "ANCHORED", None, 10, 12, []
|
||||
)
|
||||
report = ProfileReport(
|
||||
name="p", repo="o/n", pin="pin", head="newhead",
|
||||
entries=[
|
||||
EntryReport("a.bin", "a.c:10-12, (just a note)", "ANCHORED", [part])
|
||||
],
|
||||
counts={"ANCHORED": 1},
|
||||
)
|
||||
with contextlib.redirect_stderr(io.StringIO()):
|
||||
self.assertFalse(bump_commit(self.path, report))
|
||||
self.assertNotIn("newhead", self.path.read_text())
|
||||
|
||||
def test_refused_when_no_ref_reads_the_pinned_repository(self):
|
||||
"""gam4980: every bare libretro.c ref anchored in upstream, and the
|
||||
bump wrote the fork's HEAD over refs that never described it."""
|
||||
@@ -3134,6 +3223,7 @@ class TestCheckVersion(unittest.TestCase):
|
||||
"tagsha" if tag == "v3.0.0" else None
|
||||
)
|
||||
)
|
||||
profile_sync.upstream.tag_commit = profile_sync.upstream.resolve_tag_commit
|
||||
|
||||
def tearDown(self):
|
||||
(
|
||||
@@ -3268,6 +3358,44 @@ class TestDetectNewFiles(unittest.TestCase):
|
||||
found, _ = detect_new_files(['#include "driver.cpp"'], set())
|
||||
self.assertEqual(found, [])
|
||||
|
||||
def test_a_printf_conversion_is_not_part_of_the_name(self):
|
||||
# sdl2trs src/trs_options.c:864-866 builds "%slevel2.rom" and the
|
||||
# report named slevel2.rom.
|
||||
lines = [
|
||||
'snprintf(romfile1, sizeof(romfile1), "%slevel2.rom", ROM_PATH);',
|
||||
'snprintf(name, 64, "%02dcart.bin", slot);',
|
||||
]
|
||||
found, _ = detect_new_files(lines, set(), path="src/trs_options.c")
|
||||
self.assertEqual(found, [(1, "level2.rom"), (2, "cart.bin")])
|
||||
|
||||
def test_a_quoted_name_with_spaces_is_read_whole(self):
|
||||
# virtualjaguar libretro.c:5815-5816 opens "Jaguar CD BIOS.rom",
|
||||
# reported as BIOS.rom. A sentence still yields the name it holds.
|
||||
lines = [
|
||||
'snprintf(path, n, "%s%c%s", dir, slash, "Jaguar CD BIOS.rom");',
|
||||
'log("Please insert bios.bin first");',
|
||||
'message(STATUS "Packing patches.zip")',
|
||||
'#define ROM ".bin.hex.iii.rom"',
|
||||
]
|
||||
found, _ = detect_new_files(lines, set(), path="libretro.c")
|
||||
self.assertEqual(
|
||||
found,
|
||||
[(1, "Jaguar CD BIOS.rom"), (2, "bios.bin"), (3, "patches.zip")],
|
||||
)
|
||||
|
||||
def test_an_identifier_in_code_is_not_a_file(self):
|
||||
# pcee2 reads config keys EE.bios and IOP.bios and a field psm_s.pal.
|
||||
lines = [
|
||||
"cfg.EE.bios = load(path);",
|
||||
"if (psm_s.pal) {",
|
||||
"// loads boot.rom from the system directory",
|
||||
'open("ipl.rom");',
|
||||
]
|
||||
found, _ = detect_new_files(lines, set(), path="src/core/config.cpp")
|
||||
self.assertEqual(found, [(3, "boot.rom"), (4, "ipl.rom")])
|
||||
found, _ = detect_new_files(["Copy EE.bios here"], set(), path="README.md")
|
||||
self.assertEqual(found, [(1, "EE.bios")])
|
||||
|
||||
|
||||
class TestWatchHashes(unittest.TestCase):
|
||||
def test_finds_new_sha1(self):
|
||||
@@ -3565,6 +3693,37 @@ class TestCollectCitations(unittest.TestCase):
|
||||
citation = collect_citations(document)[0]
|
||||
self.assertEqual(citation.ref, "x.c:100")
|
||||
|
||||
def test_spaced_lone_number_closing_the_citation_joins_the_run(self):
|
||||
# vkquake wrote `(Quake/bgmusic.c:29, 321).` and cemu a list of ten
|
||||
# lines of .gitlab-ci.yml: only the first number was ever compared.
|
||||
cases = {
|
||||
"music (Quake/bgmusic.c:29, 321).\n": [(29, 29), (321, 321)],
|
||||
"(ref: a/b.cpp:1586-1633, 2646).\n": [(1586, 1633), (2646, 2646)],
|
||||
"jobs in .gitlab-ci.yml:110, 116, 139-140\n": [
|
||||
(110, 110), (116, 116), (139, 140)
|
||||
],
|
||||
"read at x.c:5, 6; then y\n": [(5, 5), (6, 6)],
|
||||
}
|
||||
for text, spans in cases.items():
|
||||
citation = collect_citations({"notes": text})[0]
|
||||
self.assertEqual(
|
||||
[(p.start, p.end) for p in citation.parts], spans, text
|
||||
)
|
||||
line = text.splitlines()[0]
|
||||
self.assertEqual(
|
||||
[line[lo:hi] for lo, hi in citation.spans],
|
||||
[p.raw for p in citation.parts],
|
||||
)
|
||||
|
||||
def test_spaced_number_list_followed_by_a_word_stays_prose(self):
|
||||
for text in (
|
||||
"It reads x.c:5, 6, 7 players at once.\n",
|
||||
"It reads x.c:5, 6 and 7 at once.\n",
|
||||
"Lines 3, 4 and 5, version 2, 3 players.\n",
|
||||
):
|
||||
refs = [c.ref for c in collect_citations({"notes": text})]
|
||||
self.assertEqual(refs, ["x.c:5"] if "x.c" in text else [], text)
|
||||
|
||||
def test_declared_repository_name_is_not_external(self):
|
||||
document = {
|
||||
"upstream": "https://github.com/o/mednafen",
|
||||
@@ -3611,6 +3770,13 @@ files:
|
||||
source_ref: "a.c:10-12"
|
||||
'''
|
||||
|
||||
# One run written twice in a folded scalar: no physical line carries the
|
||||
# folded sentence, and the run alone is not unique.
|
||||
FOLDED_TWICE = PROSE_SAMPLE.replace(
|
||||
"files:",
|
||||
"twice: >\n Read at (q.c:7) first\n and again at (q.c:7) later.\nfiles:",
|
||||
)
|
||||
|
||||
|
||||
class TestRebaseProse(unittest.TestCase):
|
||||
def setUp(self):
|
||||
@@ -3678,6 +3844,18 @@ class TestRebaseProse(unittest.TestCase):
|
||||
self.assertIn("(a.c:10)", self.path.read_text())
|
||||
|
||||
def test_ambiguous_sentence_location_is_left_alone(self):
|
||||
self.path.write_text(FOLDED_TWICE, encoding="utf-8")
|
||||
part = PartResult(RefPart("q.c", 7, 7, "7"), "SHIFTED", None, 9, 9, [])
|
||||
applied = rebase_refs(
|
||||
self.path, self._report([self._prose_entry("q.c:7", [part], "twice")])
|
||||
)
|
||||
self.assertEqual(applied, [])
|
||||
self.assertEqual(self.path.read_text(), FOLDED_TWICE)
|
||||
|
||||
def test_one_sentence_in_two_fields_moves_in_both(self):
|
||||
# emucorex cites CMakeLists.txt:706 in notes and in exclusion_note on
|
||||
# identical lines: neither was unique in the file, one was left, and
|
||||
# the pin refused to follow.
|
||||
text = PROSE_SAMPLE.replace(
|
||||
"files:",
|
||||
'quirks: |\n The loader appends dc/ to the system directory '
|
||||
@@ -3685,11 +3863,37 @@ class TestRebaseProse(unittest.TestCase):
|
||||
)
|
||||
self.path.write_text(text, encoding="utf-8")
|
||||
part = PartResult(RefPart("a.c", 10, 10, "10"), "SHIFTED", None, 14, 14, [])
|
||||
applied = rebase_refs(
|
||||
self.path, self._report([self._prose_entry("a.c:10", [part])])
|
||||
report = self._report([
|
||||
self._prose_entry("a.c:10", [part]),
|
||||
self._prose_entry("a.c:10", [part], "quirks"),
|
||||
])
|
||||
self.assertEqual(
|
||||
rebase_refs(self.path, report),
|
||||
["notes: a.c:10 -> a.c:14", "quirks: a.c:10 -> a.c:14"],
|
||||
)
|
||||
self.assertEqual(applied, [])
|
||||
self.assertEqual(self.path.read_text(), text)
|
||||
written = self.path.read_text()
|
||||
self.assertEqual(written, text.replace("(a.c:10)", "(a.c:14)"))
|
||||
self.assertEqual(profile_sync.pending_recale(report, text=written), 0)
|
||||
self.assertTrue(bump_commit(self.path, report))
|
||||
|
||||
def test_a_refused_recale_names_the_citation_and_writes_nothing(self):
|
||||
# The structured ref could move, the prose could not: the run
|
||||
# printed nothing, wrote nothing, and the pin stayed.
|
||||
self.path.write_text(FOLDED_TWICE, encoding="utf-8")
|
||||
prose = PartResult(RefPart("q.c", 7, 7, "7"), "SHIFTED", None, 9, 9, [])
|
||||
structured = PartResult(RefPart("a.c", 10, 12), "SHIFTED", None, 14, 16, [])
|
||||
report = self._report([
|
||||
self._prose_entry("q.c:7", [prose], "twice"),
|
||||
EntryReport("a.bin", "a.c:10-12", "SHIFTED", [structured]),
|
||||
])
|
||||
err = io.StringIO()
|
||||
with contextlib.redirect_stderr(err):
|
||||
self.assertEqual(rebase_refs(self.path, report), [])
|
||||
self.assertIn(
|
||||
"p: not recaled, cannot move twice: q.c:7 (0 lines carry its sentence)",
|
||||
err.getvalue(),
|
||||
)
|
||||
self.assertEqual(self.path.read_text(), FOLDED_TWICE)
|
||||
|
||||
def test_folded_scalar_moves_through_its_run_token(self):
|
||||
text = PROSE_SAMPLE.replace(
|
||||
@@ -3776,14 +3980,12 @@ class TestPendingProse(unittest.TestCase):
|
||||
self.assertTrue(bump_commit(self.path, report))
|
||||
|
||||
def test_unwritable_prose_blocks_the_pin(self):
|
||||
text = PROSE_SAMPLE.replace(
|
||||
"files:",
|
||||
'quirks: |\n The loader appends dc/ to the system directory '
|
||||
'(a.c:10) and reads\n nothing else.\nfiles:',
|
||||
)
|
||||
self.path.write_text(text, encoding="utf-8")
|
||||
part = PartResult(RefPart("a.c", 10, 10, "10"), "SHIFTED", None, 14, 14, [])
|
||||
self.path.write_text(FOLDED_TWICE, encoding="utf-8")
|
||||
part = PartResult(RefPart("q.c", 7, 7, "7"), "SHIFTED", None, 9, 9, [])
|
||||
report = self._report([part])
|
||||
report.entries[0] = EntryReport(
|
||||
"twice", "q.c:7", "SHIFTED", [part], "prose", "twice"
|
||||
)
|
||||
self.assertEqual(rebase_refs(self.path, report), [])
|
||||
self.assertFalse(bump_commit(self.path, report))
|
||||
|
||||
@@ -3832,24 +4034,46 @@ class TestWrappedProse(unittest.TestCase):
|
||||
}
|
||||
self.assertEqual(runs, {"src/config.rs:76-78,95-99": True})
|
||||
|
||||
def test_a_wrapped_run_that_moves_holds_the_pin(self):
|
||||
def _report(self, ref, parts):
|
||||
return ProfileReport(
|
||||
name="p", repo="o/n", pin="pin", head="newhead",
|
||||
entries=[EntryReport("adam", ref, "SHIFTED", parts, "prose", "adam")],
|
||||
counts={"SHIFTED": 1},
|
||||
)
|
||||
|
||||
def test_a_wrapped_run_moves_on_both_lines_and_the_pin_follows(self):
|
||||
# gearcoleco put its four ranges on the line after `libretro.cpp:`,
|
||||
# and the run held the pin with nothing written and nothing said.
|
||||
parts = [
|
||||
PartResult(RefPart("libretro.cpp", 2372, 2373, "2372-2373"),
|
||||
"SHIFTED", None, 2381, 2382, []),
|
||||
PartResult(RefPart("libretro.cpp", 2429, 2433, "2429-2433"),
|
||||
"SHIFTED", None, 2438, 2442, []),
|
||||
]
|
||||
entry = EntryReport(
|
||||
"adam", "libretro.cpp:2372-2373, 2429-2433", "SHIFTED", parts,
|
||||
"prose", "adam",
|
||||
report = self._report("libretro.cpp:2372-2373, 2429-2433", parts)
|
||||
self.assertEqual(len(rebase_refs(self.path, report)), 1)
|
||||
text = self.path.read_text()
|
||||
self.assertIn("(libretro.cpp:\n 2381-2382, 2438-2442) and by", text)
|
||||
self.assertTrue(bump_commit(self.path, report))
|
||||
|
||||
def test_a_break_before_the_colon_moves_the_next_line(self):
|
||||
# playdiaemu wrote `.../src/content.rs` and `:111-199` one line apart.
|
||||
text = PROSE_SAMPLE.replace(
|
||||
"files:",
|
||||
"adam: |\n ZIP archives from the game path (src/content.rs\n"
|
||||
" :111-199). No other file is opened.\nfiles:",
|
||||
)
|
||||
report = ProfileReport(
|
||||
name="p", repo="o/n", pin="pin", head="newhead",
|
||||
entries=[entry], counts={"SHIFTED": 1},
|
||||
self.path.write_text(text, encoding="utf-8")
|
||||
part = PartResult(RefPart("src/content.rs", 111, 199, "111-199"),
|
||||
"CHANGED", None, 111, 204, [])
|
||||
report = self._report("src/content.rs:111-199", [part])
|
||||
report.counts = {"CHANGED": 1}
|
||||
self.assertEqual(
|
||||
rebase_refs(self.path, report, accept_changed=True),
|
||||
["adam: src/content.rs:111-199 -> src/content.rs:111-204"],
|
||||
)
|
||||
self.assertEqual(rebase_refs(self.path, report), [])
|
||||
self.assertFalse(bump_commit(self.path, report))
|
||||
self.assertEqual(self.path.read_text(), self.TEXT)
|
||||
self.assertIn("(src/content.rs\n :111-204). No", self.path.read_text())
|
||||
self.assertTrue(bump_commit(self.path, report, accept_changed=True))
|
||||
|
||||
|
||||
class TestOpcodeCache(unittest.TestCase):
|
||||
@@ -3902,19 +4126,41 @@ class TestCommentCitations(unittest.TestCase):
|
||||
[c.ref for c in profile_sync.collect_citations(document, self.TEXT)],
|
||||
)
|
||||
|
||||
def test_a_comment_run_that_moves_holds_the_pin(self):
|
||||
field = profile_sync.comment_citations(self.TEXT)[0].field
|
||||
def _report(self, index, part):
|
||||
field = profile_sync.comment_citations(self.TEXT)[index].field
|
||||
return ProfileReport(
|
||||
name="p", repo="o/n", pin="pin", head="newhead",
|
||||
entries=[EntryReport("comment", f"{part.part.path}:{part.part.raw}",
|
||||
part.status, [part], "prose", field)],
|
||||
counts={part.status: 1},
|
||||
)
|
||||
|
||||
def test_a_comment_run_moves_and_the_pin_follows(self):
|
||||
# amiarcadia `# da.h:3-5` and mame2003_plus `core_options.c:223-236`
|
||||
# held the pin, and --rebase-refs wrote nothing and said nothing.
|
||||
part = PartResult(RefPart("src/cpu.c", 3212, 3451, "3212-3451"),
|
||||
"SHIFTED", None, 3268, 3507, [])
|
||||
report = ProfileReport(
|
||||
name="p", repo="o/n", pin="pin", head="newhead",
|
||||
entries=[EntryReport("comment", "src/cpu.c:3212-3451", "SHIFTED",
|
||||
[part], "prose", field)],
|
||||
counts={"SHIFTED": 1},
|
||||
report = self._report(0, part)
|
||||
self.assertEqual(
|
||||
rebase_refs(self.path, report),
|
||||
["comment line 10: src/cpu.c:3212-3451 -> src/cpu.c:3268-3507"],
|
||||
)
|
||||
self.assertEqual(rebase_refs(self.path, report), [])
|
||||
self.assertFalse(bump_commit(self.path, report))
|
||||
self.assertEqual(self.path.read_text(), self.TEXT)
|
||||
self.assertEqual(
|
||||
self.path.read_text(),
|
||||
self.TEXT.replace("src/cpu.c:3212-3451", "src/cpu.c:3268-3507"),
|
||||
)
|
||||
self.assertTrue(bump_commit(self.path, report))
|
||||
|
||||
def test_a_renamed_comment_path_is_rewritten(self):
|
||||
# bennugd `libretro.c:887-910` in a comment: RENAMED to
|
||||
# libretro/libretro.c, same lines, and nothing moved.
|
||||
part = PartResult(RefPart("z.c", 9, 9, "9"), "RENAMED", "src/z.c", 9, 9, [])
|
||||
report = self._report(1, part)
|
||||
self.assertEqual(
|
||||
rebase_refs(self.path, report), ["comment line 16: z.c:9 -> src/z.c:9"]
|
||||
)
|
||||
self.assertIn('"a.c:10-12" # see src/z.c:9', self.path.read_text())
|
||||
self.assertTrue(bump_commit(self.path, report))
|
||||
|
||||
|
||||
class TestRealignProse(unittest.TestCase):
|
||||
@@ -4512,6 +4758,19 @@ class TestEmulatorsDirStatus(unittest.TestCase):
|
||||
Path(root, "a.yml").write_text("emulator: a\n")
|
||||
self.assertTrue(profile_sync.emulators_dir_is_dirty(root))
|
||||
|
||||
def test_only_the_selected_profiles_count(self):
|
||||
# A loop of --emulator X --rebase-refs refused every profile after
|
||||
# the first one it wrote.
|
||||
with tempfile.TemporaryDirectory() as root:
|
||||
self._git("init", "-q", cwd=root)
|
||||
Path(root, "a.yml").write_text("emulator: a\n")
|
||||
Path(root, "b.yml").write_text("emulator: b\n")
|
||||
self._git("add", "a.yml", "b.yml", cwd=root)
|
||||
self._git("-c", "user.name=t", "-c", "user.email=t@t", "commit", "-qm", "p", cwd=root)
|
||||
Path(root, "a.yml").write_text("emulator: a2\n")
|
||||
self.assertTrue(profile_sync.emulators_dir_is_dirty(root, ["a"]))
|
||||
self.assertFalse(profile_sync.emulators_dir_is_dirty(root, ["b"]))
|
||||
|
||||
def test_a_tree_outside_any_repository_is_not_clean(self):
|
||||
with tempfile.TemporaryDirectory() as root, mock.patch.dict(
|
||||
os.environ, {"GIT_CEILING_DIRECTORIES": str(Path(root).parent)}
|
||||
@@ -4550,6 +4809,88 @@ class UpstreamVendoredAsASubmodule(unittest.TestCase):
|
||||
self.assertEqual(views[1].head, "nesbox-tip")
|
||||
|
||||
|
||||
class RealignReadsThePathAsWritten(unittest.TestCase):
|
||||
"""hatarib cites hatari/src/tos.c, a path of hatariB. When only
|
||||
upstream_commit moved, realign stripped the hatari/ prefix, read
|
||||
hatari/hatari's src/tos.c and proposed moving a correct citation."""
|
||||
|
||||
def test_an_unmoved_repository_carrying_the_path_keeps_it(self):
|
||||
from unittest import mock # noqa: PLC0415
|
||||
|
||||
fork = profile_sync.upstream.make_repo("github.com", "b", "hatariB")
|
||||
up = profile_sync.upstream.make_repo("github.com", "hatari", "hatari")
|
||||
trees = {(fork, "fork-pin"): {"hatari/src/tos.c"},
|
||||
(up, "old"): {"src/tos.c"}, (up, "new"): {"src/tos.c"}}
|
||||
|
||||
def fetch(repo, sha, path, *a, **k):
|
||||
return ["line"] * 40 if path in trees.get((repo, sha), set()) else None
|
||||
|
||||
part = profile_sync.RefPart("hatari/src/tos.c", 10, 10, "10")
|
||||
with mock.patch.object(profile_sync.upstream, "fetch_file", side_effect=fetch), \
|
||||
mock.patch.object(profile_sync, "_resolve_at",
|
||||
return_value=("found", ("src/tos.c", ["x"] * 40))):
|
||||
result = profile_sync._realign_part(
|
||||
part, [(up, "old", "new")], "cache", True,
|
||||
pinned=[(fork, "fork-pin"), (up, "new")],
|
||||
)
|
||||
self.assertIsNone(result)
|
||||
|
||||
|
||||
class RealignGivesASharedPathToTheFirstRepository(unittest.TestCase):
|
||||
"""freej2me-plus and hex007/freej2me both hold src/libretro/: with only
|
||||
upstream_commit moved, the fork's citations were read in upstream."""
|
||||
|
||||
def test_the_source_declared_first_keeps_it(self):
|
||||
from unittest import mock # noqa: PLC0415
|
||||
|
||||
fork = profile_sync.upstream.make_repo("github.com", "t", "freej2me-plus")
|
||||
up = profile_sync.upstream.make_repo("github.com", "hex007", "freej2me")
|
||||
part = profile_sync.RefPart("src/libretro/freej2me_libretro.c", 39, 46, "39-46")
|
||||
with mock.patch.object(
|
||||
profile_sync.upstream, "fetch_file", return_value=["line"] * 99
|
||||
), mock.patch.object(
|
||||
profile_sync, "_resolve_at", return_value=("found", (part.path, ["x"] * 99))
|
||||
):
|
||||
result = profile_sync._realign_part(
|
||||
part, [(up, "eb4ac87", "8b9bc8a")], "cache", True,
|
||||
pinned=[(fork, "3933e75"), (up, "8b9bc8a")],
|
||||
)
|
||||
self.assertIsNone(result)
|
||||
|
||||
|
||||
class UpstreamForkedByThePort(unittest.TestCase):
|
||||
"""snes9x, kronos and np2kai pinned upstream_commit on the merge base of
|
||||
upstream with the fork. Compared to upstream's own HEAD, a bump moved it
|
||||
past anything the port carries."""
|
||||
|
||||
def _views(self, base):
|
||||
from unittest import mock # noqa: PLC0415
|
||||
|
||||
fork = profile_sync.upstream.make_repo("github.com", "f", "port")
|
||||
self.up = profile_sync.upstream.make_repo("github.com", "o", "up")
|
||||
self.fork = fork
|
||||
views = [
|
||||
profile_sync.RepoView(fork, "src-pin", "src-head", "source_commit", "source"),
|
||||
profile_sync.RepoView(self.up, "old-base", "up-tip", "upstream_commit", "upstream"),
|
||||
]
|
||||
with mock.patch.object(
|
||||
profile_sync.upstream, "fork_base", return_value=base
|
||||
) as asked:
|
||||
followed = profile_sync._follow_fork_base(views, "cache", True)
|
||||
return followed, asked
|
||||
|
||||
def test_the_upstream_head_is_the_fork_base(self):
|
||||
views, asked = self._views("new-base")
|
||||
self.assertEqual(views[1].head, "new-base")
|
||||
self.assertEqual(
|
||||
asked.call_args.args[:4], (self.up, "up-tip", self.fork, "src-head")
|
||||
)
|
||||
|
||||
def test_a_port_outside_the_network_keeps_the_tip(self):
|
||||
views, _ = self._views(None)
|
||||
self.assertEqual(views[1].head, "up-tip")
|
||||
|
||||
|
||||
class OfflineCacheMissIsNotGone(TestBuildReport):
|
||||
"""Offline, a file the cache never stored came back as None and the
|
||||
report read "GONE (absent at HEAD, no rename found)" for files that
|
||||
|
||||
@@ -910,5 +910,79 @@ class TestCurrentSlug(unittest.TestCase):
|
||||
)
|
||||
|
||||
|
||||
|
||||
class TestNewestTag(unittest.TestCase):
|
||||
"""GitHub's REST tag list is sorted by name: nesoid lists 2.5 before
|
||||
2.5-4, so the first entry was reported as the latest tag."""
|
||||
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.repo = make_repo("github.com", "o", "n")
|
||||
self.saved = (upstream._github_graphql, upstream.list_tags)
|
||||
upstream.list_tags = lambda repo, cache, offline=False: ["2.5", "2.5-4"]
|
||||
|
||||
def tearDown(self):
|
||||
upstream._github_graphql, upstream.list_tags = self.saved
|
||||
self.tmp.cleanup()
|
||||
|
||||
def test_commit_date_order_wins(self):
|
||||
upstream._github_graphql = lambda *a: {
|
||||
"data": {"repository": {"refs": {"nodes": [{"name": "2.5-4"}]}}}
|
||||
}
|
||||
self.assertEqual(upstream.newest_tag(self.repo, self.tmp.name), "2.5-4")
|
||||
|
||||
def test_without_a_token_the_forge_order_stays(self):
|
||||
upstream._github_graphql = lambda *a: None
|
||||
self.assertEqual(upstream.newest_tag(self.repo, self.tmp.name), "2.5")
|
||||
|
||||
|
||||
class TestNamedTagCommit(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.saved = upstream._http_json
|
||||
|
||||
def tearDown(self):
|
||||
upstream._http_json = self.saved
|
||||
self.tmp.cleanup()
|
||||
|
||||
def test_gitlab_tag_is_asked_by_name(self):
|
||||
asked = []
|
||||
|
||||
def answer(url):
|
||||
asked.append(url)
|
||||
return {"name": "1.2.35", "commit": {"id": "c74fc36"}}
|
||||
|
||||
upstream._http_json = answer
|
||||
repo = parse_repo("https://gitlab.com/jengun/sdltrs")
|
||||
self.assertEqual(upstream.tag_commit(repo, "1.2.35", self.tmp.name), "c74fc36")
|
||||
self.assertTrue(asked[0].endswith("/repository/tags/1.2.35"))
|
||||
|
||||
|
||||
class TestForkBase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.saved = upstream._http_json
|
||||
|
||||
def tearDown(self):
|
||||
upstream._http_json = self.saved
|
||||
self.tmp.cleanup()
|
||||
|
||||
def test_the_merge_base_github_names(self):
|
||||
asked = []
|
||||
|
||||
def answer(url):
|
||||
asked.append(url)
|
||||
return {"merge_base_commit": {"sha": "base"}, "status": "diverged"}
|
||||
|
||||
upstream._http_json = answer
|
||||
up, fork = make_repo("github.com", "o", "up"), make_repo("github.com", "f", "port")
|
||||
self.assertEqual(upstream.fork_base(up, "tip", fork, "head", self.tmp.name), "base")
|
||||
self.assertTrue(asked[0].endswith("/repos/o/up/compare/tip...f:port:head"))
|
||||
|
||||
def test_no_network_is_none(self):
|
||||
upstream._http_json = lambda url: None
|
||||
up, fork = make_repo("github.com", "o", "up"), make_repo("github.com", "f", "port")
|
||||
self.assertIsNone(upstream.fork_base(up, "tip", fork, "head", self.tmp.name))
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user