mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-06 14:00:34 +02:00
Render complete Steam artwork for every sideload and fix VR shortcut identity
Add optional SteamGridDB settings, source-first fallbacks rendered with Steam canvas, backfill commands and shared APK/native library details. Verify live artwork and Open Saber Steam Play/Stop; document SuperTux's clipboard crash. Co-Authored-By: GPT-6 Astra <noreply@openai.com>
This commit is contained in:
1 parent
c06b3285f2
commit
f695f398de
18 files changed
+1097
-289
No files matched your search
@@ -0,0 +1,32 @@
|
||||
// Synthetic canvas for API-path tests only. It emits transparent PNGs, not visual proof.
|
||||
const zlib = require('zlib');
|
||||
function crc(data) {
|
||||
let c=0xffffffff;
|
||||
for(const b of data) {c^=b;for(let i=0;i<8;i++)c=(c>>>1)^((c&1)?0xedb88320:0);}
|
||||
return (c^0xffffffff)>>>0;
|
||||
}
|
||||
function chunk(name,data) {
|
||||
const body=Buffer.concat([Buffer.from(name),data]), n=Buffer.alloc(4), sum=Buffer.alloc(4);
|
||||
n.writeUInt32BE(data.length);sum.writeUInt32BE(crc(body));return Buffer.concat([n,body,sum]);
|
||||
}
|
||||
function png(w,h) {
|
||||
const header=Buffer.alloc(13);header.writeUInt32BE(w);header.writeUInt32BE(h,4);header[8]=8;header[9]=6;
|
||||
return Buffer.concat([Buffer.from('89504e470d0a1a0a','hex'),chunk('IHDR',header),
|
||||
chunk('IDAT',zlib.deflateSync(Buffer.alloc((w*4+1)*h))),chunk('IEND',Buffer.alloc(0))]).toString('base64');
|
||||
}
|
||||
function surface() {
|
||||
const canvases=[];
|
||||
const document={fonts:{load:async()=>[]},createElement(tag) {
|
||||
if(tag!=='canvas')throw Error('unexpected element');
|
||||
const canvas={width:1,height:1,text:[],draws:0};
|
||||
const ctx={measureText:t=>({width:String(t).length*30}),fillText(t){canvas.text.push(t);},
|
||||
drawImage(){canvas.draws++;},getImageData:()=>({data:new Uint8ClampedArray(canvas.width*canvas.height*4)}),
|
||||
createLinearGradient:()=>({addColorStop(){}}),createRadialGradient:()=>({addColorStop(){}})};
|
||||
for(const method of ['save','restore','beginPath','rect','roundRect','clip','fillRect','putImageData','arc','fill','stroke'])ctx[method]=()=>{};
|
||||
canvas.getContext=()=>ctx;canvas.toDataURL=()=> 'data:image/png;base64,'+png(canvas.width,canvas.height);
|
||||
canvases.push(canvas);return canvas;
|
||||
}};
|
||||
class Image {constructor(){this.width=2;this.height=2;} async decode(){if(this.src.includes('YmFk'))throw Error('bad image');}}
|
||||
return {document,Image,canvases};
|
||||
}
|
||||
module.exports={surface};
|
||||
@@ -37,7 +37,7 @@ function build(steam) {
|
||||
},
|
||||
});
|
||||
const shortcutOverview = s => ({
|
||||
appid: s.appid, display_name: s.name, sort_as: s.name, app_type: SHORTCUT_TYPE,
|
||||
appid: s.appid, display_name: s.name, sort_as: s.name, app_type: SHORTCUT_TYPE, devkit_gameid: s.devkit_gameid,
|
||||
local_per_client_data: { installed: true, display_status: 1, status_percentage: 0 },
|
||||
});
|
||||
const allApps = () => [...steam.apps.map(gameOverview), ...steam.shortcuts.map(shortcutOverview)];
|
||||
@@ -65,6 +65,7 @@ function build(steam) {
|
||||
value,
|
||||
});
|
||||
return {
|
||||
...require('./canvas_stub').surface(),
|
||||
collectionStore: {
|
||||
GetUserCollectionsByName(name) { return steam.collections.filter(c => c.name === name).map(collection); },
|
||||
NewUnsavedCollection(name, filter, apps) { return collection({name, apps: apps.map(a => a.appid)}); },
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
const fs=require('fs'),vm=require('vm'),assert=require('assert');
|
||||
const stub=require(process.cwd()+'/tests/fakeframe/rootfs/usr/local/lib/fakeframe/canvas_stub');
|
||||
(async()=>{
|
||||
const surface=stub.surface(),ctx=vm.createContext(surface);
|
||||
vm.runInContext(fs.readFileSync('frame/android/library_artwork.js','utf8'),ctx);
|
||||
const result=await ctx.renderLibraryArtwork({label:'Example Game',images:{icon:['png','fixture']}});
|
||||
assert.deepEqual(Object.keys(result.images),['grid','wide','hero','logo','icon']);
|
||||
for(const [slot,size] of Object.entries({grid:[600,900],wide:[920,430],hero:[3840,1240],logo:[1280,480],icon:[256,256]})) {
|
||||
const b=Buffer.from(result.images[slot],'base64');assert.equal(b.readUInt32BE(16),size[0]);assert.equal(b.readUInt32BE(20),size[1]);
|
||||
}
|
||||
const hero=surface.canvases.find(c=>c.width===3840);assert.equal(hero.text.length,0);
|
||||
const logo=surface.canvases.find(c=>c.width===1280);assert(logo.text.length);assert.equal(logo.draws,0);
|
||||
const before=surface.canvases.length;await ctx.renderLibraryArtwork({label:'No Icon',images:{}});
|
||||
assert.equal(surface.canvases.slice(before).find(c=>c.width===3840).text.length,0);
|
||||
console.log('five dimensions, textless hero, title logo: OK');
|
||||
})().catch(e=>{console.error(e);process.exit(1)});
|
||||
@@ -32,6 +32,7 @@ class LauncherTests(unittest.TestCase):
|
||||
(app / 'launch.sh').write_bytes((ROOT / 'frame/android/lepton-app.sh').read_bytes())
|
||||
(app / 'app.apk').touch()
|
||||
(app / 'instance.id').write_text('2800000001')
|
||||
(app / 'shortcut.id').write_text('3346865537')
|
||||
bin_dir = d / 'bin'
|
||||
bin_dir.mkdir()
|
||||
lepton = d / '.local/share/Steam/steamapps/common/Lepton/lepton'
|
||||
@@ -40,6 +41,8 @@ class LauncherTests(unittest.TestCase):
|
||||
path.write_text('#!' + sys.executable + '\n' + body)
|
||||
path.chmod(0o755)
|
||||
script(lepton, 'import os,time\nfrom pathlib import Path\n'
|
||||
'assert os.environ["SteamAppId"] == "2800000001"\n'
|
||||
'assert os.environ["LEPTON_ENV_SteamAppId"] == "3346865537"\n'
|
||||
'Path(os.environ["HOME"],"started").write_text(str(os.getpid()))\n'
|
||||
+ ('time.sleep(30)\n' if terminate else 'raise SystemExit(23)\n'))
|
||||
script(bin_dir / 'setsid', 'import os,sys\nos.setsid()\nos.execv(sys.argv[2],sys.argv[2:])\n')
|
||||
@@ -118,41 +121,39 @@ class ArtworkTests(unittest.TestCase):
|
||||
self.assertEqual(background[8:12], bytes([4, 9, 142, 255]))
|
||||
self.assertEqual(background[12:], bytes([10, 20, 30, 255]))
|
||||
|
||||
def test_all_slots_have_expected_dimensions(self):
|
||||
images = art.prepare('Open Saber Plus', (FIXTURES / 'icon.png').read_bytes())
|
||||
self.assertEqual(set(images), set(art.SLOTS))
|
||||
for slot, (ext, data) in images.items():
|
||||
self.assertEqual(ext, 'png')
|
||||
self.assertEqual(struct.unpack_from('>II', data, 16), art.SLOTS[slot])
|
||||
# Transparent logo, opaque capsules; corrupt/missing icon gets a monogram.
|
||||
_, _, logo = art.decode(images['logo'][1])
|
||||
self.assertIn(0, logo[3::4])
|
||||
self.assertIn(255, logo[3::4])
|
||||
self.assertEqual(set(art.fallback('日本語', b'bad icon')), set(art.SLOTS))
|
||||
|
||||
def test_supplied_slots_and_url(self):
|
||||
def test_source_inputs_and_url(self):
|
||||
import io
|
||||
data = (FIXTURES / 'icon.png').read_bytes()
|
||||
response = io.BytesIO(data)
|
||||
response.geturl = lambda: 'https://example.org/icon.png'
|
||||
with patch.object(art.urllib.request, 'urlopen', return_value=response) as fetch:
|
||||
images = art.prepare('Game', artwork={'hero': data, 'icon': 'https://example.org/icon.png'})
|
||||
self.assertEqual(images['hero'], ('png', data))
|
||||
with patch('frame_steamgriddb.lookup', return_value=({}, [])), \
|
||||
patch.object(art.urllib.request, 'urlopen', return_value=response) as fetch:
|
||||
images, warnings = art.prepare('Game', artwork={'banner': data, 'icon': 'https://example.org/icon.png'})
|
||||
self.assertEqual(images['banner'], ('png', data))
|
||||
self.assertEqual(images['icon'], ('png', data))
|
||||
self.assertEqual(warnings, [])
|
||||
self.assertEqual(fetch.call_count, 1)
|
||||
self.assertEqual(fetch.call_args.kwargs['timeout'], 20)
|
||||
self.assertEqual(fetch.call_args.args[0].get_header('User-agent'), 'FrameControl/1.0')
|
||||
|
||||
def test_provider_precedence_and_bad_source_fallback(self):
|
||||
data = (FIXTURES / 'icon.png').read_bytes()
|
||||
jpg = (FIXTURES / 'icon.jpg').read_bytes()
|
||||
with patch('frame_steamgriddb.lookup', return_value=({'hero': jpg}, [])):
|
||||
images, warnings = art.prepare('Game', data, {'hero': data, 'wide': b'bad', 'screenshots': [b'bad', data]})
|
||||
self.assertEqual(images['hero'], ('jpg', jpg))
|
||||
self.assertEqual(images['icon'], ('png', data))
|
||||
self.assertEqual(images['screenshot'], ('png', data))
|
||||
self.assertNotIn('wide', images)
|
||||
self.assertEqual(len(warnings), 2)
|
||||
|
||||
def test_supplied_jpeg(self):
|
||||
data = (FIXTURES / 'icon.jpg').read_bytes()
|
||||
self.assertEqual(art.image_type(data), 'jpg')
|
||||
self.assertEqual(art.prepare('Game', artwork={'wide': data})['wide'], ('jpg', data))
|
||||
with self.assertRaises(ValueError):
|
||||
art.image_type(data[:30])
|
||||
|
||||
def test_bad_artwork_and_expansion_limits(self):
|
||||
import zlib
|
||||
for value in ({'bad': b'bad'}, {'hero': 'file:///etc/passwd'}, {'hero': b'bad'}, ['hero']):
|
||||
for value in ({'bad': b'bad'}, ['hero']):
|
||||
with self.subTest(value=value), self.assertRaises(ValueError):
|
||||
art.prepare('Game', artwork=value)
|
||||
data = bytearray((FIXTURES / 'icon.png').read_bytes())
|
||||
@@ -209,10 +210,12 @@ class InstallTests(unittest.TestCase):
|
||||
def shortcut(*args, **kwargs):
|
||||
if args[0] == 'add':
|
||||
return '3346865537'
|
||||
if args[0] == 'render':
|
||||
return json.dumps({'paths': {slot: '/home/steamos/Applications/Android/org.test.vr/artwork/' + slot + '.png' for slot in art.SLOTS}})
|
||||
if args[0] == 'list':
|
||||
return json.dumps(self.responses['shortcuts'])
|
||||
return json.dumps(self.responses['configure'])
|
||||
with patch.object(android.frame_artwork, 'prepare', return_value=self.images), \
|
||||
with patch.object(android.frame_artwork, 'prepare', return_value=(self.images, [])), \
|
||||
patch.object(android, 'read_meta', return_value=existing), \
|
||||
patch.object(android, '_copy') as copy, \
|
||||
patch.object(android, 'ssh', return_value=self.responses['home']) as ssh, \
|
||||
@@ -229,7 +232,7 @@ class InstallTests(unittest.TestCase):
|
||||
self.assertEqual(configure[1:3], ('3346865537', 'New name'))
|
||||
self.assertEqual(configure[6], '1')
|
||||
self.assertEqual(set(json.loads(configure[7])), set(art.SLOTS))
|
||||
self.assertTrue(configure[5].endswith('/org.test.vr/icon.png'))
|
||||
self.assertTrue(configure[5].endswith('/org.test.vr/artwork/icon.png'))
|
||||
self.assertEqual(result['shortcut'], self.existing['shortcut'])
|
||||
self.assertEqual(result['label'], 'New name')
|
||||
self.assertEqual(result['library_warnings'], [])
|
||||
@@ -238,7 +241,7 @@ class InstallTests(unittest.TestCase):
|
||||
|
||||
def test_first_install_adds_shortcut(self):
|
||||
_, _, api, _ = self.install(None)
|
||||
self.assertEqual([c.args[0] for c in api.call_args_list], ['add', 'configure'])
|
||||
self.assertEqual([c.args[0] for c in api.call_args_list], ['add', 'render', 'configure'])
|
||||
|
||||
def test_artwork_forwarded_through_patch(self):
|
||||
artwork = {'hero': b'provided'}
|
||||
@@ -250,13 +253,15 @@ class InstallTests(unittest.TestCase):
|
||||
self.assertIs(install.call_args.args[-1], artwork)
|
||||
|
||||
def test_failed_new_install_removes_shortcut(self):
|
||||
def tool(*args):
|
||||
def tool(*args, **kwargs):
|
||||
if args[0] == 'add':
|
||||
return '3346865537'
|
||||
if args[0] == 'render':
|
||||
return json.dumps({'paths': {slot: '/tmp/' + slot + '.png' for slot in art.SLOTS}})
|
||||
if args[0] == 'configure':
|
||||
raise android.FrameError('write failed')
|
||||
return '{}'
|
||||
with patch.object(android.frame_artwork, 'prepare', return_value=self.images), \
|
||||
with patch.object(android.frame_artwork, 'prepare', return_value=(self.images, [])), \
|
||||
patch.object(android, 'read_meta', return_value=None), \
|
||||
patch.object(android, '_copy'), patch.object(android, 'ssh', return_value='/home/steamos') as ssh, \
|
||||
patch.object(android, 'shortcut_tool', side_effect=tool) as api:
|
||||
@@ -304,7 +309,7 @@ class SteamAPITests(unittest.TestCase):
|
||||
with patch.object(shortcuts, 'evaluate', return_value={}) as evaluate:
|
||||
shortcuts.remove(42)
|
||||
js = evaluate.call_args.args[0]
|
||||
self.assertIn('[0, 1, 2, 3, 4]', js)
|
||||
self.assertIn('[0, 1, 2, 3]', js)
|
||||
self.assertLess(js.index('ClearCustomArtworkForApp(id, type)'), js.index('RemoveShortcut(id)'))
|
||||
self.assertIn('const wanted = []', js)
|
||||
|
||||
@@ -335,10 +340,11 @@ class SteamContextTests(unittest.TestCase):
|
||||
{slot: str(FIXTURES / 'icon.png') for slot in art.SLOTS})
|
||||
self.assertEqual(result['warnings'], [])
|
||||
self.assertTrue(steam['shortcuts'][0]['vr'])
|
||||
self.assertEqual(set(steam['shortcuts'][0]['artwork']), {'0', '1', '2', '3', '4'})
|
||||
self.assertEqual(set(steam['shortcuts'][0]['artwork']), {'0', '1', '2', '3'})
|
||||
self.assertEqual(steam['collections'], [{'name': 'Android', 'apps': [999, 42]},
|
||||
{'name': 'Android VR', 'apps': [42]}])
|
||||
shortcuts.configure(42, 'Renamed', '/exe', '/dir', '/icon', False, {})
|
||||
shortcuts.configure(42, 'Renamed', '/exe', '/dir', '/icon', False,
|
||||
{slot: str(FIXTURES / 'icon.png') for slot in art.SLOTS})
|
||||
self.assertEqual(steam['shortcuts'][0]['name'], 'Renamed')
|
||||
self.assertEqual(steam['collections'][1]['apps'], [])
|
||||
shortcuts.remove(42)
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
"""Every public sideload entry point reaches the mandatory five-slot writer."""
|
||||
import contextlib
|
||||
import io
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / 'ui'))
|
||||
import frame_android as android
|
||||
import frame_artwork as artwork
|
||||
import frame_catalog as catalog
|
||||
import frame_apk_versions as versions
|
||||
import frame_titles as titles
|
||||
import frame_steamgriddb as sgdb
|
||||
import server
|
||||
import frame_webinstall as webinstall
|
||||
|
||||
|
||||
class EntryPoints(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.stack = contextlib.ExitStack()
|
||||
self.addCleanup(self.stack.close)
|
||||
self.info = {'package':'org.example.game', 'label':'Example', 'version':'1', 'version_code':1,
|
||||
'vr':False, 'repairable':False, 'launchable':True, 'min_sdk':24, 'abis':[], 'icon_png':None}
|
||||
self.stack.enter_context(patch.object(android, 'apk_info', return_value=self.info))
|
||||
self.stack.enter_context(patch.object(android, 'read_meta', return_value=None))
|
||||
self.stack.enter_context(patch.object(android, '_copy'))
|
||||
self.stack.enter_context(patch.object(android, 'ssh', return_value='/home/steamos'))
|
||||
self.stack.enter_context(patch.object(artwork, 'prepare', return_value=({}, [])))
|
||||
self.api = self.stack.enter_context(patch.object(android, 'shortcut_tool', side_effect=self.steam))
|
||||
|
||||
def steam(self, *args, **kwargs):
|
||||
if args[0] == 'add': return '3346865537'
|
||||
if args[0] == 'render': return json.dumps({'paths': {s:'/tmp/'+s+'.png' for s in artwork.SLOTS}})
|
||||
if args[0] == 'list': return json.dumps([{'appid':3346865537,'name':'Example','devkit_gameid':'Example'}])
|
||||
return '{"warnings":[]}'
|
||||
|
||||
def assert_art(self):
|
||||
calls = [c.args for c in self.api.call_args_list]
|
||||
self.assertEqual(sum(c[0] == 'render' for c in calls), 1)
|
||||
configs = [c for c in calls if c[0] == 'configure']
|
||||
self.assertEqual(len(configs), 1)
|
||||
self.assertEqual(set(json.loads(configs[0][7])), set(artwork.SLOTS))
|
||||
|
||||
def test_cli_install(self):
|
||||
with patch.object(sys, 'argv', ['frame_android.py', 'install', 'game.apk']), patch('builtins.print'):
|
||||
android.main()
|
||||
self.assert_art()
|
||||
|
||||
def test_file_upload(self):
|
||||
class Request:
|
||||
headers = {'X-Filename':'game.apk','X-Mode':'apk','Content-Length':'3'}
|
||||
rfile = io.BytesIO(b'apk')
|
||||
with patch.object(server, 'ensure_master'):
|
||||
result = server.Handler.upload(Request())
|
||||
self.assertEqual(result['app']['package'], self.info['package'])
|
||||
self.assert_art()
|
||||
|
||||
def test_catalogue_install(self):
|
||||
with patch.object(catalog, 'app', return_value={'r':'yes','t':False,'n':'Example'}), \
|
||||
patch.object(catalog, 'fetch_apk', return_value='game.apk'), \
|
||||
patch.object(catalog, 'fetch_icon', return_value=None):
|
||||
catalog.install(self.info['package'])
|
||||
self.assert_art()
|
||||
|
||||
def test_version_finder_install(self):
|
||||
record = {'url':'https://example.org/app.apk','sha256':'fixture','version_code':1,'source':'F-Droid'}
|
||||
with patch.object(versions, '_versions', return_value=([record], [])), \
|
||||
patch.object(catalog, 'fetch_apk', return_value='game.apk'):
|
||||
versions.install(self.info['package'], record['url'])
|
||||
self.assert_art()
|
||||
|
||||
def test_web_download_install(self):
|
||||
result = webinstall.dispatch('game.apk', source='https://example.org/game.apk')
|
||||
self.assertEqual(result['kind'], 'apk')
|
||||
self.assert_art()
|
||||
|
||||
def test_refresh_api(self):
|
||||
with patch.object(server, 'ensure_master'), \
|
||||
patch.object(server, 'start_job', side_effect=lambda label, work: work()), \
|
||||
patch.object(android, 'refresh_art', return_value=[]) as refresh:
|
||||
self.assertEqual(server.android({'action':'refresh-art', 'all':True}), {'apps':[]})
|
||||
refresh.assert_called_once_with(None)
|
||||
|
||||
def test_source_search_shared_installer_contract(self):
|
||||
# Source workers hand their download and optional images to this public seam.
|
||||
android.install('game.apk', name='Example', source='source-search', artwork={'banner': b'fixture'})
|
||||
self.assert_art()
|
||||
|
||||
def test_native_title_install(self):
|
||||
with tempfile.TemporaryDirectory() as root:
|
||||
plan = {'id':'Example','name':'Example','root':root,'size':3,'target':'game.exe',
|
||||
'runtime':'proton-experimental','source':'example.zip'}
|
||||
def ssh(cmd, **kwargs):
|
||||
if 'test -d' in cmd: return ''
|
||||
if 'steamos-prepare-upload' in cmd: return '{"directory":"/home/steamos/devkit-game/Example"}'
|
||||
if 'steam-client-create-shortcut' in cmd: return '{"success":"registered"}'
|
||||
return ''
|
||||
with patch.object(titles, 'ssh', side_effect=ssh), patch.object(titles, 'ensure_utils'), \
|
||||
patch.object(titles, '_copy_tree'), patch.object(titles, '_rsync', return_value=True):
|
||||
result = titles._install(plan, lambda *args: None)
|
||||
self.assertEqual(result['shortcut'], 3346865537)
|
||||
self.assert_art()
|
||||
config = next(c.args for c in self.api.call_args_list if c.args[0] == 'configure')
|
||||
self.assertEqual(json.loads(config[8])['category'], 'Sideloaded')
|
||||
self.assertEqual(config[3:5], ('','')) # Never replace devkit's executable/runtime wiring.
|
||||
|
||||
def test_native_renamed_shortcut_uses_saved_identity(self):
|
||||
self.api.side_effect = lambda *args, **kw: '[{"appid":42,"name":"Renamed"}]'
|
||||
with patch.object(titles, 'ssh', return_value='{"shortcut":42}'):
|
||||
self.assertEqual(titles._library_shortcut('Original', 'Original'), 42)
|
||||
|
||||
def test_native_failure_removes_new_blank_shortcut(self):
|
||||
with tempfile.TemporaryDirectory() as root:
|
||||
plan = {'id':'Example','name':'Example','root':root,'size':3,'target':'game.exe',
|
||||
'runtime':'proton-experimental','source':'example.zip'}
|
||||
def ssh(cmd, **kwargs):
|
||||
if 'steamos-prepare-upload' in cmd: return '{"directory":"/home/steamos/devkit-game/Example"}'
|
||||
if 'steam-client-create-shortcut' in cmd: return '{"success":"registered"}'
|
||||
return ''
|
||||
with patch.object(titles, 'ssh', side_effect=ssh), patch.object(titles, 'ensure_utils'), \
|
||||
patch.object(titles, '_copy_tree'), patch.object(titles, '_rsync', return_value=True), \
|
||||
patch.object(android, 'apply_library', side_effect=android.FrameError('render failed')):
|
||||
with self.assertRaisesRegex(android.FrameError, 'render failed'):
|
||||
titles._install(plan, lambda *args: None)
|
||||
self.assertIn(('remove', '3346865537'), [c.args for c in self.api.call_args_list])
|
||||
|
||||
def test_refresh_does_not_reinstall_or_stop(self):
|
||||
meta = {**self.info, 'instance':2800000001, 'shortcut':3346865537, 'flatscreen':False}
|
||||
with patch.object(android, '_meta_or_fail', return_value=meta), \
|
||||
patch.object(android, 'ssh', side_effect=lambda cmd, **kw: json.dumps({'icon_png':''}) if cmd == 'python3 -' else '/home/steamos'), \
|
||||
patch.object(android, 'stop') as stop, patch.object(android, '_copy') as copy:
|
||||
android.refresh_art(self.info['package'])
|
||||
stop.assert_not_called(); copy.assert_not_called(); self.assert_art()
|
||||
|
||||
def test_all_refresh_reports_partial_failures(self):
|
||||
with patch.object(android, 'list_apps', return_value=[{'package':'org.a.game'},{'package':'org.b.game'}]), \
|
||||
patch.object(android, '_meta_or_fail', side_effect=android.FrameError('missing')):
|
||||
result=android.refresh_art()
|
||||
self.assertEqual(len(result),2)
|
||||
self.assertTrue(all('error' in a for a in result))
|
||||
|
||||
def test_render_failure_cannot_report_success(self):
|
||||
self.api.side_effect = lambda *args, **kw: '3346865537' if args[0]=='add' else '{"paths":{}}'
|
||||
with self.assertRaisesRegex(android.FrameError,'every slot'):
|
||||
android.install('game.apk')
|
||||
|
||||
|
||||
class Renderer(unittest.TestCase):
|
||||
@unittest.skipUnless(shutil.which('node'), 'Node is needed for the canvas contract fixture')
|
||||
def test_canvas_slots_and_title_placement(self):
|
||||
subprocess.run(['node', str(ROOT / 'tests/fixtures/library/check-renderer.js')],
|
||||
cwd=str(ROOT), check=True, capture_output=True, timeout=30)
|
||||
|
||||
def test_desktop_packages_include_renderer_and_settings(self):
|
||||
config = json.loads((ROOT / 'app/package.json').read_text())
|
||||
resources = {r['from']: r['filter'] for r in config['build']['extraResources']}
|
||||
self.assertIn('*.js', resources['../ui'])
|
||||
self.assertIn('*.js', resources['../frame/android'])
|
||||
|
||||
|
||||
class SteamGridDB(unittest.TestCase):
|
||||
def test_no_key_is_silent_and_offline(self):
|
||||
with patch.object(sgdb,'api_key',return_value=''), patch.object(sgdb,'_get') as get:
|
||||
self.assertEqual(sgdb.lookup('Game'),({},[]))
|
||||
get.assert_not_called()
|
||||
|
||||
def test_exact_match_and_top_votes_per_slot(self):
|
||||
calls=[]
|
||||
def get(path,key):
|
||||
calls.append(path)
|
||||
if 'search' in path: return [{'id':1,'name':'Other Game'},{'id':2,'name':'Game'}]
|
||||
dims=(600,900) if '600x900' in path else (920,430)
|
||||
return [{'url':'https://example.org/low.png','score':2,'width':dims[0],'height':dims[1]},
|
||||
{'url':'https://example.org/top.png','score':20,'width':dims[0],'height':dims[1]},
|
||||
{'url':'https://example.org/nsfw.png','score':99,'nsfw':True,'width':dims[0],'height':dims[1]}]
|
||||
with patch.object(sgdb,'api_key',return_value='test-key'),patch.object(sgdb,'_get',side_effect=get):
|
||||
images,warnings=sgdb.lookup('Game VR')
|
||||
self.assertEqual(set(images),set(artwork.SLOTS));self.assertEqual(warnings,[])
|
||||
self.assertTrue(all(url.endswith('/top.png') for url in images.values()))
|
||||
self.assertTrue(all('/game/2?' in p for p in calls[1:]))
|
||||
|
||||
def test_wrong_title_and_failed_lookup_fall_back(self):
|
||||
with patch.object(sgdb,'api_key',return_value='test-key'),patch.object(sgdb,'_get',return_value=[{'id':1,'name':'Unrelated'}]):
|
||||
self.assertEqual(sgdb.lookup('Game'),({},[]))
|
||||
with patch.object(sgdb,'api_key',return_value='test-key'),patch.object(sgdb,'_get',side_effect=OSError('private-secret')):
|
||||
result=sgdb.lookup('Game')
|
||||
self.assertNotIn('private-secret',str(result));self.assertEqual(result[0],{})
|
||||
|
||||
def test_settings_never_return_key(self):
|
||||
with tempfile.TemporaryDirectory() as root, patch.object(sgdb,'settings_path',return_value=Path(root)/'settings.json'), \
|
||||
patch.dict(sgdb.os.environ,{},clear=True):
|
||||
result=sgdb.save_settings({'steamgriddb_api_key':'secret-fixture'})
|
||||
self.assertTrue(result['steamgriddb_configured'])
|
||||
self.assertNotIn('secret-fixture',json.dumps(result))
|
||||
self.assertEqual(sgdb.api_key(),'secret-fixture')
|
||||
if sys.platform!='win32':self.assertEqual((Path(root)/'settings.json').stat().st_mode&0o777,0o600)
|
||||
sgdb.save_settings({'steamgriddb_api_key':''})
|
||||
self.assertFalse(sgdb.settings()['steamgriddb_configured'])
|
||||
|
||||
|
||||
if __name__=='__main__':unittest.main()
|
||||
Reference in new issue
Block a user