mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-28 02:56:36 -04:00
Prepare the Pepsiman source-bound TAS candidate and Japanese BIOS profile
(cherry picked from commit 6802d20103)
This commit is contained in:
committed by
Alexandros Mandravillis
parent
0f3a34f403
commit
f6ca922016
@@ -0,0 +1,76 @@
|
||||
# Retail SCPH-5500 (Japan), SHA256 9c0421858e217805f4abe18698afea8d5aa36ff0727eb8484944e00eb5e7eadb.
|
||||
# ROM [0x80,0x18000) equals the qualified SCPH1001 kernel bytes.
|
||||
# The reset-vector area has 24 differing bytes; compile those target bytes.
|
||||
# The seed list retains matching 64-byte kernel entries plus the explicit
|
||||
# architectural reset vector. Shell code differs; PSX_SHELLWIN_INTERP=ON is
|
||||
# required until its own native admission is qualified. No firmware is shipped.
|
||||
|
||||
[program]
|
||||
name = "Sony SCPH-5500 BIOS (Japan, v3.0 09/09/96)"
|
||||
id = "SCPH-5500"
|
||||
rom = "bios/SCPH5500.BIN"
|
||||
load_address = "0xBFC00000"
|
||||
entry_pc = "0xBFC00000"
|
||||
text_size = "0x80000"
|
||||
|
||||
[recompiler]
|
||||
seeds = "recompiler/seeds/phase2_ghidra_seeds_SCPH5500.json"
|
||||
out_dir = "generated"
|
||||
out_stem = "SCPH5500"
|
||||
strict = true
|
||||
|
||||
# The BIOS's boot-time bulk copies of CODE out of ROM into RAM. Single source
|
||||
# of truth for every relocation window in the recompiler AND in the C it
|
||||
# emits (the generated dispatch's normalize(), the kernel-bless guard, the
|
||||
# game/shell-overlap tests). Each entry is a CLAIM that the copy is
|
||||
# byte-verbatim; the runtime's kernel-bless memcmp enforces it for the
|
||||
# bless window. All bounds are [lo, hi) with hi EXCLUSIVE.
|
||||
#
|
||||
# dispatch_key = "ram": functions in the window are keyed by RAM address
|
||||
# (normalize folds ROM -> RAM).
|
||||
# dispatch_key = "rom": functions keep their ROM key (normalize folds the
|
||||
# RAM alias back to ROM).
|
||||
[recompiler.address_model]
|
||||
normalize_mask = "0x1FFFFFFF"
|
||||
|
||||
[[recompiler.address_model.copy]]
|
||||
name = "Kernel Part 2"
|
||||
rom_lo = "0x1FC10000"
|
||||
rom_hi = "0x1FC18000"
|
||||
ram_lo = "0x00000500"
|
||||
runtime_base = "0x00000500" # KUSEG — the kernel executes uncached-region code cached at 0x500
|
||||
dispatch_key = "ram"
|
||||
kernel_bless = true
|
||||
|
||||
[[recompiler.address_model.copy]]
|
||||
name = "Shell"
|
||||
rom_lo = "0x1FC18000"
|
||||
rom_hi = "0x1FC43000"
|
||||
ram_lo = "0x00030000"
|
||||
runtime_base = "0x80030000" # KSEG0 — LoadRunShell jumps to 0x80030000
|
||||
dispatch_key = "rom"
|
||||
kernel_bless = false
|
||||
# NOTE (do not "fix" without evidence): runtime/src/bios_hle.c documents the
|
||||
# actual LoadRunShell copy as 0x67FF0 bytes (ROM 0xBFC18000 -> RAM 0x30000..
|
||||
# 0x97FF0). Every pre-profile window constant covered only 0x2B000 of that,
|
||||
# and dirty_ram_interp.c tracks live RAM in [0x5B000,0x8F000) as an open
|
||||
# class-B bug. Widening this window is a REAL BEHAVIOR CHANGE requiring its
|
||||
# own evidence-backed commit — it is not a parameterization cleanup.
|
||||
|
||||
# Kernel-RAM PCs the BIOS overwrites with 4-instruction dispatch stubs at
|
||||
# runtime (CLAUDE.md rule 18; docs/dynamic_handler_install.md). The emitter
|
||||
# plants a dirty-check hook at these PCs so the installed stub executes.
|
||||
[[recompiler.install_slots]]
|
||||
ram_addr = "0x00000CF0" # SIO data-byte handler dispatch slot
|
||||
|
||||
# Per-image anchors couriered into the generated C (psx_bios_image) for the
|
||||
# runtime HLE tier. Values are FACTS about this image, not tunables: the
|
||||
# shell entry is where LoadRunShell (ROM 0xBFC06FF0) jumps after its copy;
|
||||
# deliver_event_ret is the instruction after the kernel DeliverEvent loop's
|
||||
# `jalr v0` (kernel RAM 0x1718), the $ra a delivered event callback returns
|
||||
# through. Omit a key on a BIOS that has no such anchor — the runtime then
|
||||
# treats that HLE feature as structurally unavailable instead of firing on a
|
||||
# wrong address.
|
||||
[recompiler.runtime_exports]
|
||||
shell_entry_phys = "0x00030000"
|
||||
deliver_event_ret = "0x80001720"
|
||||
@@ -839,6 +839,9 @@ if(BUILD_TESTING)
|
||||
add_test(NAME tas_replay_recipe
|
||||
COMMAND ${Python3_EXECUTABLE}
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/../tools/tasreplays/test_tekken3.py")
|
||||
add_test(NAME tas_pepsiman_admission
|
||||
COMMAND ${Python3_EXECUTABLE}
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/../tools/tasreplays/test_pepsiman.py")
|
||||
# The Python driver compiles test_source_ram_page_probe.c at O0/O2.
|
||||
add_test(NAME tas_ram_long_route
|
||||
COMMAND ${Python3_EXECUTABLE}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,16 +2,19 @@ cmake_minimum_required(VERSION 3.24)
|
||||
project(Tekken3Tas LANGUAGES C CXX)
|
||||
get_filename_component(PSXRECOMP_ROOT "${CMAKE_CURRENT_LIST_DIR}/../.." ABSOLUTE)
|
||||
set(TAS_PROJECT_DIR "${PSXRECOMP_ROOT}/build/tekken3" CACHE PATH "Prepared title directory")
|
||||
set(TAS_GAME_STEM "SLUS_004.02" CACHE STRING "Prepared boot-program filename stem")
|
||||
set(TAS_EXE_NAME "Tekken3-TAS" CACHE STRING "Native player filename")
|
||||
set(TAS_WINDOW_TITLE "Tekken 3 - Spikestuff TAS" CACHE STRING "Native player title")
|
||||
set(PSXRECOMP_BIOS_STEMS "SCPH1001" CACHE STRING "BIOS")
|
||||
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DPSX_ENABLE_BLOCK_CYCLES=1")
|
||||
include("${PSXRECOMP_ROOT}/runtime/runtime.cmake")
|
||||
file(GLOB TAS_GAME_SHARDS CONFIGURE_DEPENDS "${TAS_PROJECT_DIR}/generated/SLUS_004.02_full_*.c")
|
||||
file(GLOB TAS_GAME_SHARDS CONFIGURE_DEPENDS "${TAS_PROJECT_DIR}/generated/${TAS_GAME_STEM}_full_*.c")
|
||||
if(NOT TAS_GAME_SHARDS)
|
||||
message(FATAL_ERROR "Run tools/tasreplays/tekken3.py setup first")
|
||||
message(FATAL_ERROR "Generate the selected title before configuring its player")
|
||||
endif()
|
||||
psxrecomp_add_runtime_target(psx-runtime
|
||||
EXE_NAME Tekken3-TAS
|
||||
WINDOW_TITLE "Tekken 3 - Spikestuff TAS"
|
||||
EXE_NAME "${TAS_EXE_NAME}"
|
||||
WINDOW_TITLE "${TAS_WINDOW_TITLE}"
|
||||
DEFAULT_GAME_CONFIG_PATH "${TAS_PROJECT_DIR}/game.toml"
|
||||
GAME_GENERATED_FULL_C ${TAS_GAME_SHARDS}
|
||||
GAME_GENERATED_DISPATCH_C "${TAS_PROJECT_DIR}/generated/SLUS_004.02_dispatch.c")
|
||||
GAME_GENERATED_DISPATCH_C "${TAS_PROJECT_DIR}/generated/${TAS_GAME_STEM}_dispatch.c")
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
"""Prepare and compare the original Pepsiman 6044 movie with owned inputs.
|
||||
|
||||
The native profile remains experimental until its complete source comparison
|
||||
passes. Stock-source completion must precede setup. No reference is generated
|
||||
from a candidate, and no movie input is changed to correct a divergence.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import bk2_intake
|
||||
from bk2_to_psxrti import convert
|
||||
import tekken3
|
||||
from compare_ram_pages import read_pages
|
||||
|
||||
HERE=Path(__file__).resolve().parent
|
||||
ROOT=HERE.parent.parent
|
||||
FRAMES=71806
|
||||
MOVIE_SHA='de3fe8065a8adf068adf2cdbc2732b754df8e26aa08dc3a6f332c7dca7e5d578'
|
||||
WORDS_SHA='025f465a3ff008c4bf70f9cef46ebae04c4f0c726b38a79b88bbd90e064da68d'
|
||||
BIOS_SHA='9c0421858e217805f4abe18698afea8d5aa36ff0727eb8484944e00eb5e7eadb'
|
||||
EXE_SHA='96960e14406b58dd71c6717bb374537313ff7b64215330671c989ba2926f3718'
|
||||
TRACKS=[
|
||||
(124164432,'6452e78fa5d65abec2a5084afa28878bf478c9c63d6ba739b6b542cad366eded'),
|
||||
(10936800,'6cd94daf5173b9f84469683fd0270621a8ca2fbd614a6d8af1e75375c36f2531'),
|
||||
(10936800,'2255e58929497be30bded16a81e3548613b89ff7b57501219eca15e9bf557600'),
|
||||
(10936800,'8bfcd406e12e3cea8cd83f232272ef2a81581bafc0c98ffa2ed55608c6d2f8b9'),
|
||||
(10936800,'2beda20a27697e13bf84921cbca5d7363c0455ce95400ffaacd8d799ad3f7121'),
|
||||
(10936800,'bb784ddc940277585042209a07368995cc32470b9fa42e3c5245b199c5243e7d'),
|
||||
(5630688,'fcc2bbb43914bac5b3dfa79e92baaa0a94d21fc57a5d7c4b5b144643cd5a75c4d'),
|
||||
(32149488,'19c7d810996b3b56ce08ea46adcebcb6a57695b140d2d037fca409c2e409d196')]
|
||||
|
||||
digest=tekken3.digest
|
||||
write=tekken3.write_json
|
||||
command=tekken3.command
|
||||
|
||||
def cue_files(text):
|
||||
lines=[line.strip() for line in text.splitlines() if line.strip()]
|
||||
files=[]
|
||||
for track in range(1,9):
|
||||
if not lines: raise ValueError('missing CUE track')
|
||||
match=re.fullmatch(r'FILE "([^"\r\n]+)" BINARY',lines.pop(0))
|
||||
if not match: raise ValueError('invalid CUE file line')
|
||||
name=match[1]
|
||||
if Path(name).is_absolute() or '..' in Path(name).parts:
|
||||
raise ValueError('CUE companion must remain below the CUE directory')
|
||||
expected=[f'TRACK {track:02d} '+('MODE2/2352' if track==1 else 'AUDIO')]
|
||||
expected+=['INDEX 01 00:00:00'] if track==1 else ['INDEX 00 00:00:00','INDEX 01 00:02:00']
|
||||
if lines[:len(expected)]!=expected: raise ValueError('unqualified track order, mode or pregap')
|
||||
del lines[:len(expected)]
|
||||
files.append(name)
|
||||
if lines or len(set(files))!=8: raise ValueError('extra or repeated disc companions')
|
||||
return files
|
||||
|
||||
def media(cue,bios):
|
||||
tekken3.require_hash(bios,BIOS_SHA)
|
||||
tracks=[(cue.parent/name).resolve(strict=True) for name in cue_files(cue.read_text())]
|
||||
for path,(length,sha) in zip(tracks,TRACKS):
|
||||
if not path.is_relative_to(cue.parent): raise ValueError('disc companion escapes root')
|
||||
if path.stat().st_size!=length: raise ValueError('wrong track size: '+str(path))
|
||||
tekken3.require_hash(path,sha)
|
||||
return tracks
|
||||
|
||||
def boot_program(track):
|
||||
sys.path.insert(0,str(ROOT/'tools'))
|
||||
from prepare_disc import parse_root_entries
|
||||
with track.open('rb') as f:
|
||||
def user(lba):
|
||||
f.seek(lba*2352)
|
||||
sector=f.read(2352)
|
||||
if len(sector)!=2352 or sector[:12]!=b'\0'+b'\xff'*10+b'\0':
|
||||
raise ValueError('invalid raw data sector')
|
||||
return sector[24:2072]
|
||||
def span(lba,size):
|
||||
return b''.join(user(lba+i) for i in range((size+2047)//2048))[:size]
|
||||
pvd=user(16)
|
||||
if pvd[1:6]!=b'CD001': raise ValueError('missing ISO9660')
|
||||
tree=parse_root_entries(span(struct.unpack_from('<I',pvd,158)[0],struct.unpack_from('<I',pvd,166)[0]))
|
||||
cnf=span(*tree['SYSTEM.CNF'])
|
||||
if b'BOOT = cdrom:SLPS_017.62;1' not in cnf: raise ValueError('wrong boot program')
|
||||
data=span(*tree['SLPS_017.62'])
|
||||
if hashlib.sha256(data).hexdigest()!=EXE_SHA: raise ValueError('wrong boot executable')
|
||||
return data
|
||||
|
||||
def admit_source_control(path):
|
||||
complete=json.loads((path/'complete.json').read_text())
|
||||
end=json.loads((path/'exit.json').read_text())
|
||||
semantic=json.loads((path/'semantic-review.json').read_text())
|
||||
manifest=json.loads((path/'manifest.json').read_text())
|
||||
if (complete.get('frame'),complete.get('original_inputs'),complete.get('full_movie'))!=(FRAMES,FRAMES,True):
|
||||
raise ValueError('full original source control required')
|
||||
if end.get('exit_code')!=0 or end.get('stop_reason') is not None:
|
||||
raise ValueError('source did not complete cleanly')
|
||||
if semantic.get('completion_observed') is not True or semantic.get('frame')!=FRAMES:
|
||||
raise ValueError('source completion needs semantic review')
|
||||
evidence=path/semantic['image']
|
||||
if not evidence.resolve().is_relative_to(path) or digest(evidence)!=semantic['image_sha256']:
|
||||
raise ValueError('source completion image identity changed')
|
||||
if not any(x['sha256']==MOVIE_SHA for x in manifest['bindings']):
|
||||
raise ValueError('source used a different movie')
|
||||
with (path/'ram-frames.tsv').open() as rows:
|
||||
if rows.readline().strip()!='frame\tlag_count\tpc\tram_sha256':
|
||||
raise ValueError('unrecognized stock RAM capture')
|
||||
count=0
|
||||
for expected,row in enumerate(rows):
|
||||
columns=row.rstrip('\r\n').split('\t')
|
||||
if len(columns)!=4 or int(columns[0])!=expected or not re.fullmatch('[0-9A-Fa-f]{64}',columns[3]):
|
||||
raise ValueError('incomplete or discontinuous stock RAM capture')
|
||||
count+=1
|
||||
if count!=FRAMES+1: raise ValueError('stock control misses original input boundaries')
|
||||
return {name:{'path':str(path/name),'sha256':digest(path/name)} for name in
|
||||
['complete.json','exit.json','semantic-review.json','manifest.json','ram-frames.tsv']}
|
||||
|
||||
def setup(args):
|
||||
if os.name!='nt': raise ValueError('Windows UCRT build required for this candidate')
|
||||
if subprocess.check_output(['git','-C',str(ROOT),'status','--porcelain'],text=True).strip():
|
||||
raise ValueError('commit source changes before building')
|
||||
for tool in ['gcc','g++','cmake','ninja']:
|
||||
if not shutil.which(tool): raise ValueError('missing tool: '+tool)
|
||||
macros=subprocess.check_output(['gcc','-dM','-E','-include','_mingw.h','-'],input='',text=True)
|
||||
if not re.search(r'^#define _UCRT\b',macros,re.M): raise ValueError('UCRT compiler required')
|
||||
control=admit_source_control(args.source_control.resolve(strict=True))
|
||||
disc,bios,movie=[p.resolve(strict=True) for p in (args.disc,args.bios,args.movie)]
|
||||
tracks=media(disc,bios)
|
||||
route,intake=bk2_intake.inspect(movie.read_bytes())
|
||||
if (intake['movie_sha256'],intake['frame_count'],intake['pad_words_le_sha256'])!=(MOVIE_SHA,FRAMES,WORDS_SHA):
|
||||
raise ValueError('wrong Pepsiman movie')
|
||||
project=args.project.resolve()
|
||||
if project==ROOT or project.is_relative_to(ROOT): raise ValueError('put generated title/build output outside source')
|
||||
project.mkdir(parents=True,exist_ok=False)
|
||||
data=boot_program(tracks[0])
|
||||
cache=args.cache.resolve()/EXE_SHA
|
||||
cache.mkdir(parents=True,exist_ok=True)
|
||||
exe=cache/'SLPS_017.62'
|
||||
if exe.exists(): tekken3.require_hash(exe,EXE_SHA)
|
||||
else: exe.write_bytes(data)
|
||||
tape=project/'octoshock-cold-random.psxrng'
|
||||
command([sys.executable,HERE/'external/source_random_tape.py',tape],project/'random-tape.log')
|
||||
tekken3.require_hash(tape,tekken3.TAPE_SHA)
|
||||
payload,receipt=convert(movie.read_bytes())
|
||||
(project/'input.psxrti').write_bytes(payload)
|
||||
write(project/'input.json',receipt)
|
||||
tools=project/'tools'
|
||||
common=['-G','Ninja','-DCMAKE_BUILD_TYPE=Release','-DCMAKE_C_COMPILER=gcc','-DCMAKE_CXX_COMPILER=g++']
|
||||
command(['cmake','-S',ROOT/'recompiler','-B',tools,*common,'-DBUILD_TESTING=ON',
|
||||
'-DPSXRECOMP_ENABLE_CHD=ON','-DPython3_EXECUTABLE='+sys.executable],project/'configure-tools.log')
|
||||
command(['cmake','--build',tools,'--parallel',str(args.jobs)],project/'build-tools.log')
|
||||
command(['ctest','--test-dir',tools,'--output-on-failure','-j',str(args.jobs)],project/'test-tools.log')
|
||||
command([tools/'psxrecomp-toml.exe',exe,'--output',project/'census.toml','--seeds',project/'seeds.txt'],project/'census.log')
|
||||
q=lambda p:json.dumps(p.as_posix())
|
||||
game=project/'game.toml'
|
||||
game.write_text(f'''[game]
|
||||
name = "Pepsiman TAS"
|
||||
id = "SLPS-01762"
|
||||
exe = {q(exe)}
|
||||
load_address = "0x80010000"
|
||||
entry_pc = "0x80042C58"
|
||||
text_size = "0x85800"
|
||||
stack_base = "0x801FFFF0"
|
||||
[recompiler]
|
||||
seeds = {q(project/'seeds.txt')}
|
||||
bios_config = {q(ROOT/'bios/SCPH5500.toml')}
|
||||
strict = true
|
||||
out_dir = {q(project/'generated')}
|
||||
[runtime]
|
||||
window_title = "Pepsiman - original TAS"
|
||||
bios_hle = false
|
||||
[video]
|
||||
renderer = "software"
|
||||
''',encoding='utf8')
|
||||
command([tools/'psxrecomp-bios.exe','--config',ROOT/'bios/SCPH5500.toml','--rom',bios,
|
||||
'--out-dir',ROOT/'generated'],project/'generate-bios.log')
|
||||
command([tools/'psxrecomp-game.exe','--config',game],project/'generate-game.log')
|
||||
native=project/'native'
|
||||
command(['cmake','-S',HERE,'-B',native,*common,'-DTAS_PROJECT_DIR='+str(project),
|
||||
'-DTAS_GAME_STEM=SLPS_017.62','-DTAS_EXE_NAME=Pepsiman-TAS','-DTAS_WINDOW_TITLE=Pepsiman TAS',
|
||||
'-DPSXRECOMP_BIOS_STEMS=SCPH5500','-DPSX_SHELLWIN_INTERP=ON',
|
||||
'-DPSX_RECOMP_UI=OFF','-DPSX_NETPLAY=OFF','-DPSX_REWIND=OFF','-DPSX_SETUP_WIZARD=OFF',
|
||||
'-DPSX_DEBUG_TOOLS=ON','-DPSX_ENABLE_VULKAN=OFF','-DPSXRECOMP_SKIP_BIOS_STALE_CHECK=ON',
|
||||
'-DCMAKE_DISABLE_FIND_PACKAGE_SDL3=TRUE','-DCMAKE_DISABLE_FIND_PACKAGE_ZLIB=TRUE'],project/'configure-native.log')
|
||||
command(['cmake','--build',native,'--parallel',str(args.jobs)],project/'build-native.log')
|
||||
files=[disc,bios,movie,exe,game,tape,project/'input.psxrti',project/'seeds.txt',*tracks]
|
||||
build=native/'Pepsiman-TAS.exe'
|
||||
generated={str(f.relative_to(ROOT)):digest(f) for f in (ROOT/'generated').glob('SCPH5500*') if f.is_file()}
|
||||
generated.update({str(f):digest(f) for f in (project/'generated').glob('*') if f.is_file()})
|
||||
info={'schema':'pepsiman-tas-candidate-v1','source_control':control,
|
||||
'source_head':subprocess.check_output(['git','-C',str(ROOT),'rev-parse','HEAD'],text=True).strip(),
|
||||
'source_tree':subprocess.check_output(['git','-C',str(ROOT),'rev-parse','HEAD^{tree}'],text=True).strip(),
|
||||
'bindings':[{'path':str(f),'sha256':digest(f)} for f in files],
|
||||
'generated':generated,'executable':str(build),'executable_sha256':digest(build),
|
||||
'disc':str(disc),'bios':str(bios),'game':str(game),'route':str(project/'input.psxrti'),'tape':str(tape),
|
||||
'compiler':subprocess.check_output(['gcc','--version'],text=True).splitlines()[0],
|
||||
'qualification':'candidate only; source/native comparison pending'}
|
||||
write(project/'setup.json',info)
|
||||
print(json.dumps({'candidate':str(build),'sha256':digest(build)}))
|
||||
|
||||
def run(args):
|
||||
info=json.loads((args.project/'setup.json').read_text())
|
||||
for binding in info['bindings']: tekken3.require_hash(Path(binding['path']),binding['sha256'])
|
||||
tekken3.require_hash(Path(info['executable']),info['executable_sha256'])
|
||||
argv=[sys.executable,HERE/'run_native.py',args.output,'--exe',info['executable'],
|
||||
'--game',info['game'],'--disc',info['disc'],'--bios',info['bios'],'--route',info['route'],
|
||||
'--cd-source-clock-tape',info['tape'],'--neutral-tail','1','--timeout',str(args.timeout),
|
||||
'--checkpoint-every','1200','--renderer','software',*tekken3.PROFILE]
|
||||
# The same native implementations are an explicit candidate: all psx/
|
||||
# source bytes match 2.2.2 -> 2.3, but this title still needs its own gates.
|
||||
if args.show: argv.append('--show')
|
||||
command(argv,args.output.parent/(args.output.name+'-launch.log'))
|
||||
print('Native input run ended; compare against the admitted independent source before claiming a pass.')
|
||||
|
||||
def main():
|
||||
p=argparse.ArgumentParser(description=__doc__)
|
||||
sub=p.add_subparsers(dest='action',required=True)
|
||||
s=sub.add_parser('setup')
|
||||
for field in ['disc','bios','movie','project','cache','source-control']:
|
||||
s.add_argument('--'+field,type=Path,required=True)
|
||||
s.add_argument('--jobs',type=int,default=4)
|
||||
r=sub.add_parser('run')
|
||||
r.add_argument('--project',type=Path,required=True)
|
||||
r.add_argument('--output',type=Path,required=True)
|
||||
r.add_argument('--timeout',type=int,default=43200)
|
||||
r.add_argument('--show',action='store_true')
|
||||
args=p.parse_args()
|
||||
(setup if args.action=='setup' else run)(args)
|
||||
|
||||
if __name__=='__main__': main()
|
||||
@@ -0,0 +1,28 @@
|
||||
"""Asset-free disc-topology admission tests for the second real TAS consumer."""
|
||||
import unittest
|
||||
import pepsiman
|
||||
|
||||
def cue():
|
||||
parts=[]
|
||||
for i in range(1,9):
|
||||
parts += [f'FILE "track{i}.bin" BINARY',f' TRACK {i:02d} '+('MODE2/2352' if i==1 else 'AUDIO')]
|
||||
parts += [' INDEX 01 00:00:00'] if i==1 else [' INDEX 00 00:00:00',' INDEX 01 00:02:00']
|
||||
return '\n'.join(parts)+'\n'
|
||||
|
||||
class PepsimanAdmission(unittest.TestCase):
|
||||
def test_exact_topology(self):
|
||||
self.assertEqual(pepsiman.cue_files(cue()),[f'track{i}.bin' for i in range(1,9)])
|
||||
def test_companion_and_topology_negatives(self):
|
||||
original=cue()
|
||||
bad=[original.replace('TRACK 02 AUDIO','TRACK 02 MODE2/2352'),
|
||||
original.replace('INDEX 01 00:02:00','INDEX 01 00:01:00',1),
|
||||
original.replace('INDEX 00 00:00:00\n','',1),
|
||||
original.replace('track8.bin','track7.bin'),
|
||||
original.replace('track1.bin','../outside.bin'),
|
||||
original.replace('track1.bin','/outside.bin'),
|
||||
original+'REM undeclared\n',original[:original.index('FILE "track8.bin"')]]
|
||||
for text in bad:
|
||||
with self.subTest(text=text):
|
||||
with self.assertRaises(ValueError): pepsiman.cue_files(text)
|
||||
|
||||
if __name__=='__main__': unittest.main()
|
||||
Reference in New Issue
Block a user