scripts/test-cue-filenames.py
raw · 6911 bytes
#!/usr/bin/env python3
"""Test actual MusicLibrary filename resolution with host Android stubs (JDK 17+).
Optionally pass a UTF-8 CUE file to test it against an NFD audio filename too.
"""
from pathlib import Path
import subprocess
import sys
import tempfile
repo = Path(__file__).resolve().parents[1]
with tempfile.TemporaryDirectory(prefix='loopdeck-cue-') as temp:
root = Path(temp)
def stub(name, body, base=''):
package, cls = name.rsplit('/', 1)
path = root / (name + '.java')
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text('package ' + package.replace('/', '.') + '; public class ' + cls + base + ' {' + body + '}', encoding='utf-8')
stub('android/net/Uri', 'final String id; public Uri(String s){id=s;} public String toString(){return id;}')
stub('android/os/ParcelFileDescriptor', 'public int getFd(){return 1;} public void close(){}', ' implements AutoCloseable')
stub('android/database/Cursor', 'public boolean moveToNext(){return false;} public String getString(int n){return null;} public void close(){}', ' implements AutoCloseable')
stub('android/content/Context', 'public final ContentResolver resolver=new ContentResolver(); public ContentResolver getContentResolver(){return resolver;}')
stub('android/content/ContentResolver', '''
public String cue;
public android.database.Cursor query(android.net.Uri u,String[] c,String a,String[] b,String d){return new android.database.Cursor();}
public java.io.InputStream openInputStream(android.net.Uri u){return new java.io.ByteArrayInputStream(cue.getBytes(java.nio.charset.StandardCharsets.UTF_8));}
public android.os.ParcelFileDescriptor openFileDescriptor(android.net.Uri u,String m){return new android.os.ParcelFileDescriptor();}
''')
stub('android/provider/DocumentsContract', '''
public static android.net.Uri buildChildDocumentsUriUsingTree(android.net.Uri u,String s){return new android.net.Uri(s);}
public static android.net.Uri buildDocumentUriUsingTree(android.net.Uri u,String s){return new android.net.Uri(s);}
public static class Document {public static final String COLUMN_DOCUMENT_ID="id",COLUMN_DISPLAY_NAME="name",COLUMN_MIME_TYPE="mime",MIME_TYPE_DIR="dir";}
''')
stub('net/astropenguin/loopdeck/NativeDecoder', '''
static long open(int fd){return 1;} static void close(long h){}
static String cueSheet(long h){return "";} static String[] chapters(long h){return new String[0];} static long duration(long h){return 120000;}
''')
stub('net/astropenguin/loopdeck/FilenameTest', r'''
static void expect(boolean ok){if(!ok)throw new AssertionError();}
static MusicLibrary.Entry entry(String id,String name){return new MusicLibrary.Entry(id,name,"application/octet-stream",new android.net.Uri(id));}
static String sheet(String name){return "\ufeffFILE \""+name+"\" WAVE\r\nTRACK 01 AUDIO\r\nINDEX 01 00:00:00\r\nTRACK 02 AUDIO\r\nINDEX 01 01:00:00\r\n";}
static void rejected(android.content.Context context,MusicLibrary.Entry cue,java.util.List<MusicLibrary.Entry> folder)throws Exception {
try{MusicLibrary.tracks(context,cue,folder);throw new AssertionError("accepted unresolved sheet");}
catch(java.io.IOException expected){}
}
public static void main(String[] args)throws Exception {
android.content.Context context=new android.content.Context();
String nfc="聖骸メロフォビア.tta", nfd=java.text.Normalizer.normalize(nfc,java.text.Normalizer.Form.NFD);
expect(!nfc.equals(nfd));
MusicLibrary.Entry cue=entry("cue","album.cue"), audio=entry("audio",nfd);
context.resolver.cue=sheet(nfc);
java.util.List<Track> tracks=MusicLibrary.tracks(context,cue,java.util.List.of(cue,audio));
expect(tracks.size()==2 && tracks.get(0).uri==audio.uri && tracks.get(0).endMs==60000);
expect(MusicLibrary.tracks(context,audio,java.util.List.of(cue,audio)).size()==2);
// Also support decomposed FILE references and composed storage names.
context.resolver.cue=sheet(nfd);audio=entry("composed",nfc);
expect(MusicLibrary.tracks(context,cue,java.util.List.of(cue,audio)).size()==2);
// Existing literal matches win when both spellings exist.
MusicLibrary.Entry decomposed=entry("decomposed",nfd);
expect(MusicLibrary.tracks(context,cue,java.util.List.of(cue,audio,decomposed)).get(0).uri==decomposed.uri);
// Missing files and multiple fallback matches must still fail.
context.resolver.cue=sheet(nfc);
rejected(context,cue,java.util.List.of(cue,entry("wrong","other.tta")));
rejected(context,cue,java.util.List.of(cue,decomposed,entry("duplicate",nfd)));
context.resolver.cue=sheet("./ALBUM.TTA");
expect(MusicLibrary.tracks(context,cue,java.util.List.of(cue,entry("ascii","album.tta"))).size()==2);
// Canonical equivalence only: fullwidth ASCII is a distinct filename.
context.resolver.cue=sheet("A.tta");
rejected(context,cue,java.util.List.of(cue,entry("ascii","A.tta")));
if(args.length>0){
context.resolver.cue=java.nio.file.Files.readString(java.nio.file.Path.of(args[0]));
java.util.List<CueSheet.Part> parts=CueSheet.parse(context.resolver.cue);
expect(!parts.isEmpty());
String filename=java.text.Normalizer.normalize(parts.get(0).file,java.text.Normalizer.Form.NFD);
MusicLibrary.Entry image=entry("uploaded-audio",filename);
tracks=MusicLibrary.tracks(context,cue,java.util.List.of(cue,image));
expect(tracks.size()==parts.size());
for(int i=0;i<tracks.size();i++)expect(tracks.get(i).startMs==parts.get(i).start && tracks.get(i).uri==image.uri);
expect(MusicLibrary.tracks(context,image,java.util.List.of(cue,image)).size()==parts.size());
System.out.println("Uploaded CUE: "+tracks.size()+" tracks resolved with decomposed audio filename");
}
System.out.println("CUE filenames: NFC/NFD both directions, audio/CUE selection, literal precedence, missing/ambiguous rejection and ASCII compatibility passed");
}
''')
app = repo / 'app/src/main/java/net/astropenguin/loopdeck'
sources = list(root.rglob('*.java')) + [app / (n + '.java') for n in ('MusicLibrary', 'CueSheet', 'Track')]
subprocess.run(['java', '-m', 'jdk.compiler/com.sun.tools.javac.Main', '--release', '17', '-encoding', 'UTF-8', '-d', str(root / 'classes'), *map(str, sources)], check=True)
subprocess.run(['java', '-cp', str(root / 'classes'), 'net.astropenguin.loopdeck.FilenameTest', *sys.argv[1:]], check=True)