Files
gen1recomp/tests/android_importer_copy_test.py
T
HimioneGranger 0937e3f24b Add native Black/White battle-sprite importer with complete animations (#2598)
* Add native Black and White battle sprite importer and shared provider

* Reduce Gen 5 import scheduling and duplicate atlas work

* Play long Gen 5 idle animations from independent part tracks

* Skip part tracks when a late frame cannot be composed

* Fix importer picker lifecycle, Xbox formats, and CI coverage

* Fix Linux Gen 5 test discovery and picker regression checks

* Preserve original Gen 5 sprite resolution in provider API 2

* Trim importer comments and documentation
2026-10-07 12:10:37 -04:00

200 lines
13 KiB
Python

"""Run production Android picker copy helpers with javac (no Android SDK required).
The worker's stream/file implementation is extracted unchanged; only Log is stubbed.
Fixtures verify streaming publication, failure cleanup and destination allowlisting.
"""
from pathlib import Path
import argparse
import subprocess
import tempfile
ROOT = Path(__file__).resolve().parents[1]
SOURCE = ROOT / 'mobile/android/love/src/main/java/org/love2d/android/GameActivity.java'
def method(text, anchor):
start = text.index(anchor)
brace = text.index('{', start)
depth = 1
end = brace + 1
while depth:
depth += (text[end] == '{') - (text[end] == '}')
end += 1
return text[start:end]
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--source', type=Path, default=SOURCE)
parser.add_argument('--javac', default='javac')
parser.add_argument('--java', default='java')
parser.add_argument('--work-dir', default=None)
args = parser.parse_args()
src = args.source.read_text()
assert 'if (directRequired || isImporterDestination(destName)) {' in src
worker = src[src.index('if (directRequired ||'):src.index('if (!copyAssetFile(pickedSource')]
assert 'new Thread(new Runnable()' in worker
assert 'if (directRequired) {' in worker
assert 'writeFlagFile(pickedRoot, PICK_ERROR_FILENAME, destName)' in worker
assert 'getCanonicalFile()' in src and '!destFile.getPath().startsWith(rootPrefix)' in src
picker = method(src, 'public static boolean showFilePicker(String destFilename, String saveDir)')
assert picker.index('pickerTransfer.beginPicker()') < picker.index('self.pendingPickSaveDir =')
assert 'if (!opened) pickerTransfer.finish();' in picker
assert 'if (!pickerTransfer.claimResult())' in src
assert 'if (!handedOff) pickerTransfer.finish();' in src
assert 'STATE_PENDING_PICK_ACTIVE, pickerTransfer.isPicking()' in src
assert 'pickerTransfer.restorePicker();' in src
assert 'finally {' in worker and 'pickerTransfer.finish();' in worker
result_source = src[src.index('if (requestCode != FILE_PICKER_REQUEST_CODE)'):]
cancelled = method(result_source, 'if (uri == null)')
assert 'PICK_CANCELLED_PREFIX + destName' in cancelled and 'isTelevision' not in cancelled
helpers = '\n'.join(method(src, anchor) for anchor in (
'private static boolean isImporterDestination',
'private static final class PickerTransferGate',
'private static final class PickCopyResult',
'private static String hex(',
'private static void writeFlagFile(',
'private static PickCopyResult copyRequiredImport(',
))
helpers += '\nstatic final PickerTransferGate pickerTransfer = new PickerTransferGate();\n'
helpers += 'static final String PICK_ERROR_FILENAME="pick_error.flag", PICK_COMPLETE_FILENAME="pick_complete.flag";\n'
helpers += '''static void startProductionCopy(final InputStream pickedSource,
final File destFile, final File pickedRoot, final String destName,
final boolean directRequired) {\n'''
helpers += method(src, 'if (directRequired || isImporterDestination(destName))')
helpers = helpers.replace('final boolean directRequired) {\nif',
'final boolean directRequired) {\nboolean handedOff=false;\nif')
helpers += '\n}\n'
helpers += 'static File cancelRoot;\nstatic final String PICK_CANCELLED_PREFIX="cancelled:";\n'
helpers += 'static void writeSaveDirFlag(String name,String body) { writeFlagFile(cancelRoot,name,body); }\n'
helpers += 'static void productionCancel(String destName) { Object uri=null; try {\n'+cancelled+'\n} finally { pickerTransfer.finish(); } }\n'
fixture = r'''
static void check(boolean value, String label) {
if (!value) throw new AssertionError(label);
}
static class Log { static void d(String tag, String message) {} }
static void awaitReleased(String label) throws Exception {
long until=System.nanoTime()+java.util.concurrent.TimeUnit.SECONDS.toNanos(5);
while(!pickerTransfer.beginPicker()) {
if(System.nanoTime()>=until) throw new AssertionError(label+" did not release gate");
Thread.yield();
}
pickerTransfer.finish();
}
static class PausedInput extends ByteArrayInputStream {
final java.util.concurrent.CountDownLatch entered=new java.util.concurrent.CountDownLatch(1);
final java.util.concurrent.CountDownLatch proceed=new java.util.concurrent.CountDownLatch(1);
PausedInput(byte[] bytes) { super(bytes); }
public synchronized int read(byte[] b,int off,int len) {
entered.countDown();
try { check(proceed.await(5,java.util.concurrent.TimeUnit.SECONDS),"paused source released"); }
catch(InterruptedException e) { throw new AssertionError(e); }
return super.read(b,off,len);
}
}
static class CheckedInput extends InputStream {
final byte[] data; final File target; int cursor; boolean closed;
CheckedInput(byte[] data, File target) { this.data=data; this.target=target; }
public int read() {
check(!target.exists(), "final basename published before source EOF");
return cursor < data.length ? data[cursor++] & 255 : -1;
}
public int read(byte[] b, int offset, int count) {
check(!target.exists(), "final basename visible during copy");
if (cursor == data.length) return -1;
int n=Math.min(Math.min(count, 32771), data.length-cursor);
System.arraycopy(data,cursor,b,offset,n); cursor+=n; return n;
}
public void close() { closed=true; }
}
public static void main(String[] args) throws Exception {
File root=new File(args[0]); root.mkdirs();
check(isImporterDestination("picked_importer_gen5_bw.bin"),"gen5 accepted");
check(isImporterDestination("picked_importer_test-2.bin"),"safe identifier accepted");
for (String bad : new String[]{"picked_importer_.bin", "picked_importer_GEN5.bin",
"../picked_importer_gen5_bw.bin", "x/picked_importer_gen5_bw.bin",
"picked_importer_a\\b.bin", "picked_importer_gen5_bwXbin", "picked_rom.gb"})
check(!isImporterDestination(bad),"unsafe/non-importer rejected: "+bad);
byte[] data=new byte[3*1024*1024+17];
for(int i=0;i<data.length;i++) data[i]=(byte)(i*31);
File target=new File(root,"picked_importer_gen5_bw.bin");
CheckedInput input=new CheckedInput(data,target);
PickCopyResult ok=copyRequiredImport(input,target);
check(ok.ok && ok.bytes==data.length,"successful stream copy size");
check(input.closed,"successful stream closed");
check(ok.md5.equals(hex(MessageDigest.getInstance("MD5").digest(data))),"MD5 exact");
check(java.util.Arrays.equals(data,java.nio.file.Files.readAllBytes(target.toPath())),"bytes exact");
check(!new File(target+".part").exists(),"success has no partial");
byte[] old=java.nio.file.Files.readAllBytes(target.toPath());
InputStream broken=new InputStream(){public int read() throws IOException {throw new IOException("fixture read failure");}};
PickCopyResult fail=copyRequiredImport(broken,target);
check(!fail.ok,"failure reported");
check(java.util.Arrays.equals(old,java.nio.file.Files.readAllBytes(target.toPath())),"read failure preserves prior final");
check(!new File(target+".part").exists(),"failure partial removed");
File missing=new File(root,"missing.bin");
check(!copyRequiredImport(new InputStream(){public int read() throws IOException {throw new IOException();}},missing).ok,"missing failure");
check(!missing.exists() && !new File(missing+".part").exists(),"failure never publishes final");
File blocked=new File(root,"blocked.bin"); blocked.mkdir();
new FileOutputStream(new File(blocked,"prevent-delete")).close();
check(!copyRequiredImport(new ByteArrayInputStream(data),blocked).ok,"publish failure reported");
check(blocked.isDirectory() && !new File(blocked+".part").exists(),"publication failure cleanup");
// Force the old race window: worker A owns the shared .part while B
// requests the exact same importer destination or submits a duplicate result.
File racing=new File(root,"picked_importer_race.bin");
PausedInput paused=new PausedInput(data);
check(pickerTransfer.beginPicker(),"first picker accepted");
check(!pickerTransfer.beginPicker(),"second picker cannot replace pending metadata");
check(pickerTransfer.claimResult(),"first result claimed");
startProductionCopy(paused,racing,root,racing.getName(),true);
check(paused.entered.await(5,java.util.concurrent.TimeUnit.SECONDS),"actual worker entered stream read");
check(new File(racing+".part").exists(),"worker owns shared partial");
check(!pickerTransfer.beginPicker(),"same importer blocked during actual transfer");
check(!pickerTransfer.claimResult(),"duplicate result cannot launch a second worker");
pickerTransfer.restorePicker();
check(!pickerTransfer.beginPicker(),"activity restoration cannot unlock active worker");
paused.proceed.countDown(); awaitReleased("successful worker");
pickerTransfer.restorePicker();
check(pickerTransfer.beginPicker(),"old PICKING bundle cannot re-arm after actual async completion");pickerTransfer.finish();
check(java.util.Arrays.equals(data,java.nio.file.Files.readAllBytes(racing.toPath())),"raced destination is complete and unchanged");
check(!new File(racing+".part").exists(),"race leaves no partial");
String marker=new String(java.nio.file.Files.readAllBytes(new File(root,PICK_COMPLETE_FILENAME).toPath()),"UTF-8");
check(marker.contains(racing.getName()) && marker.contains(hex(MessageDigest.getInstance("MD5").digest(data))),"gate retained through completion marker");
check(pickerTransfer.beginPicker() && pickerTransfer.claimResult(),"new picker accepted after completion");
startProductionCopy(new InputStream(){public int read() throws IOException {throw new IOException("worker failure");}},racing,root,racing.getName(),false);
awaitReleased("failed worker");
check(java.util.Arrays.equals(data,java.nio.file.Files.readAllBytes(racing.toPath())),"failed replacement preserves final");
check(new File(root,PICK_ERROR_FILENAME).isFile(),"failure signalled before gate release");
check(pickerTransfer.beginPicker() && pickerTransfer.claimResult(),"cancel fixture starts");
cancelRoot=root; productionCancel(racing.getName());
String cancel=new String(java.nio.file.Files.readAllBytes(new File(root,PICK_ERROR_FILENAME).toPath()),"UTF-8");
check(cancel.equals("cancelled:"+racing.getName()),"all Android cancellation signals matching pending destination");
check(pickerTransfer.beginPicker(),"cancel/start rejection releases picker"); pickerTransfer.finish();
pickerTransfer.restorePicker();
check(pickerTransfer.beginPicker(),"stale same-process saved picker does not re-arm completed transfer"); pickerTransfer.finish();
PickerTransferGate freshProcess=new PickerTransferGate();freshProcess.restorePicker();
check(freshProcess.isPicking() && freshProcess.claimResult(),"fresh-process restored pending picker accepts its result");freshProcess.finish();
final java.util.concurrent.atomic.AtomicInteger winners=new java.util.concurrent.atomic.AtomicInteger();
final java.util.concurrent.CountDownLatch simultaneous=new java.util.concurrent.CountDownLatch(1);
Thread[] contenders=new Thread[16];
for(int i=0;i<contenders.length;i++) {
contenders[i]=new Thread(new Runnable(){public void run(){
try {simultaneous.await();}catch(InterruptedException e){throw new AssertionError(e);}
if(pickerTransfer.beginPicker()) winners.incrementAndGet();
}}); contenders[i].start();
}
simultaneous.countDown();for(Thread contender:contenders) contender.join();
check(winners.get()==1,"simultaneous picker requests have exactly one owner");pickerTransfer.finish();
System.out.println("PASS Android importer copy: allowlist, async routing, atomic publication, exact bytes/MD5, failures, production-worker serialization, duplicate result/picker rejection and restoration");
}
'''
code='import java.io.*; import java.security.*; import java.util.*; public class PickerCopyFixture {\n'+helpers+fixture+'\n}'
with tempfile.TemporaryDirectory(prefix='android-picker-copy-', dir=args.work_dir) as directory:
work=Path(directory)
(work/'PickerCopyFixture.java').write_text(code)
subprocess.run([args.javac,'-encoding','UTF-8',str(work/'PickerCopyFixture.java')],check=True)
subprocess.run([args.java,'-cp',str(work),'PickerCopyFixture',str(work/'fixtures')],check=True)
if __name__ == '__main__':
main()