Complete BigTask
Sync changed files to Google Drive / sync (push) Successful in 1m34s

This commit is contained in:
admin_d committed 2026-09-14 20:47:20 +05:00
1 parent 15a968a2f7
commit b52921e171
38 files changed
+564 -2956

No files matched your search

Executable → Regular
+70 -119
View File
@@ -1,134 +1,85 @@
"""Install and collect a selected Android decision probe with diagnostic evidence."""
"""Install and launch the current Android application package."""
from __future__ import annotations
import argparse, json, re, shutil, subprocess, time
import argparse, json, shutil, subprocess, xml.etree.ElementTree as ET
from pathlib import Path
from common import ROOT, CPUS, ProbeError, safe_relative, sha256, stamp, write_json
DECISION_PROFILES={'oq_host_view','oq_host_callback'}
from common import (ROOT, CPUS, ProbeError, sha256, CURRENT_BUILD_METADATA,
CURRENT_PACKAGE_METADATA, CURRENT_APK, CURRENT_BUILD_SCHEMA,
CURRENT_PACKAGE_SCHEMA)
class Adb:
def __init__(self,serial:str|None): self.base=['adb']+(['-s',serial] if serial else [])
def __init__(self, serial: str | None): self.base=['adb']+(['-s',serial] if serial else [])
def run(self,*args:str,check:bool=True,timeout:int=120)->subprocess.CompletedProcess:
p=subprocess.run(self.base+list(args),capture_output=True,timeout=timeout)
if check and p.returncode: raise ProbeError('ADB command failed: '+' '.join(args)+'\n'+p.stderr.decode(errors='replace'))
return p
def text(self,*args:str,check:bool=True)->str: return self.run(*args,check=check).stdout.decode(errors='replace').strip()
def own_report_paths(text:str)->list[str]:
paths=[]
for line in text.splitlines():
name=line.strip()[2:] if line.strip().startswith('./') else line.strip()
try: safe_relative(name)
except ProbeError: continue
if name.endswith('kam-architecture-probe/runtime.json'): paths.append(name)
return paths
def fail(message: str) -> None: raise ProbeError(message)
def validate_runtime(data:dict,package:dict,require_frames:bool=True)->list[str]:
errors=[]
for key,value in (('schema','kam-android-runtime/1'),('build_id',package['build_id']),('profile',package['profile']),('cpu_expected',package['cpu'])):
if data.get(key)!=value: errors.append('Wrong/stale runtime '+key)
if data.get('automatic_smoke_status')!='PASS': errors.append('Automatic smoke did not pass')
tests=data.get('tests')
if not isinstance(tests,list) or not tests: return errors+['No runtime tests']
by_id={x.get('id'):x for x in tests if isinstance(x,dict)}; profile=package['profile']; required=['android.startup']; observed=[]
if profile not in DECISION_PROFILES:
required += ['abi.pointer','abi.scalar_layout','abi.endianness','files.native_utf8','files.seek','files.case_sensitive','fpc.zstream','fpc.thread']
observed += ['decision.persistence.rename_replace','decision.persistence.append_reopen']
if profile=='oq_host_view': required += ['decision.host.view']
elif profile=='oq_host_callback': required += ['decision.host.callback']
if profile=='platform': required += ['decision.text.cwstring_unit']; observed += ['decision.text.cp1251_decode','decision.text.cp1251_encode']
if require_frames: required += ['render.frame_callbacks','render.context']
for name in required:
if by_id.get(name,{}).get('status')!='PASS': errors.append('Required runtime check did not pass: '+name)
for name in observed:
if name not in by_id: errors.append('Decision observation missing: '+name)
if any(t.get('status')=='FAIL' for t in tests if isinstance(t,dict)): errors.append('One or more runtime checks failed')
if require_frames and data.get('session',{}).get('frames',0)<30: errors.append('Fewer than 30 frames')
return errors
def load_json(path: Path, missing: str) -> dict:
try: value=json.loads(path.read_text())
except FileNotFoundError: fail(missing)
except (OSError, ValueError) as error: fail(f'Malformed {path.name}: {error}')
if not isinstance(value,dict): fail(f'Malformed {path.name}')
return value
def read_runtime(adb:Adb,pkg:str)->tuple[dict|None,str|None]:
paths=own_report_paths(adb.text('shell','run-as',pkg,'find','.','-name','runtime.json',check=False))
if len(paths)!=1: return None,None
raw=adb.run('exec-out','run-as',pkg,'cat',paths[0],check=False).stdout
try: return json.loads(raw),paths[0]
except (ValueError,UnicodeDecodeError): return None,None
def wait_runtime(adb:Adb,pkg:str,predicate,timeout:int)->tuple[dict|None,str|None]:
end=time.monotonic()+timeout
while time.monotonic()<end:
data,path=read_runtime(adb,pkg)
if data is not None and predicate(data): return data,path
time.sleep(1)
return read_runtime(adb,pkg)
def save_command(out:Path,name:str,p:subprocess.CompletedProcess)->None:
out.joinpath(name).write_text('exit='+str(p.returncode)+'\nstdout:\n'+p.stdout.decode(errors='replace')+'\nstderr:\n'+p.stderr.decode(errors='replace'),encoding='utf-8')
def main()->int:
ap=argparse.ArgumentParser(description=__doc__)
ap.add_argument('--profile',default='platform');ap.add_argument('--serial');ap.add_argument('--index',type=Path,default=ROOT/'dist/LATEST.json')
ap.add_argument('--seconds',type=int,default=30);ap.add_argument('--exercise-lifecycle',action='store_true');ap.add_argument('--stage-file',type=Path)
a=ap.parse_args()
if a.seconds<5: ap.error('--seconds must be at least 5')
out=ROOT/'reports/device'/stamp(); n=1
while out.exists(): out=ROOT/'reports/device'/(stamp()+'-'+str(n));n+=1
out.mkdir(parents=True)
record={'schema':'kam-android-device/2','status':'BLOCKED','errors':[],'preflight':{'status':'NOT_RUN'},'manual_visual_touch_audio':'NOT_VERIFIED','world_simulation':'NOT_RUN','command_environment':{'host_adb':shutil.which('adb'),'timeout_seconds':a.seconds}}
adb=None
def manifest_identity() -> dict:
try:
index=json.loads(a.index.read_text()); matches=[p for p in index.get('packages',[]) if p.get('profile')==a.profile]
if len(matches)!=1: raise ProbeError('No unique successfully-built APK for '+a.profile)
package=matches[0]; apk=ROOT/package['file']; pkg=package['package'];record.update(build_id=package['build_id'],profile=a.profile,package=package)
if not apk.is_file() or sha256(apk)!=package['sha256']: raise ProbeError('APK missing/changed')
if not re.fullmatch(r'org\.kammobile\.archprobe\.[a-z0-9]+',pkg): raise ProbeError('Refusing unrelated Android package')
if not shutil.which('adb'): record['preflight']={'status':'BLOCKED','reason':'adb is not installed on host'};raise ProbeError(record['preflight']['reason'])
listed=Adb(None).text('devices').splitlines()[1:]; devices=[line.split()[:2] for line in listed if line.strip()]
record['preflight']={'adb':'PASS','devices':devices}
if len(devices)!=1 or len(devices[0])!=2 or devices[0][1]!='device': record['preflight'].update(status='BLOCKED',reason='Require exactly one authorized device; found '+repr(devices));raise ProbeError(record['preflight']['reason'])
if a.serial and a.serial!=devices[0][0]: record['preflight'].update(status='BLOCKED',reason='Requested serial is not the sole authorized device');raise ProbeError(record['preflight']['reason'])
adb=Adb(devices[0][0]); record['device']={'serial':devices[0][0],'abi':adb.text('shell','getprop','ro.product.cpu.abilist'),'android':adb.text('shell','getprop','ro.build.version.release'),'sdk':adb.text('shell','getprop','ro.build.version.sdk'),'model':adb.text('shell','getprop','ro.product.model')}
if CPUS[package['cpu']][0] not in record['device']['abi'].split(','): record['preflight'].update(status='BLOCKED',reason='Device ABI does not support arm64-v8a');raise ProbeError(record['preflight']['reason'])
record['preflight']['status']='PASS'
install=adb.run('install','-r',str(apk),check=False);save_command(out,'install.log',install);record['install']={'package_id':pkg,'initial_exit':install.returncode,'result':install.stdout.decode(errors='replace').strip()}
root=ET.parse(ROOT/'app/CastleEngineManifest.xml').getroot(); version=root.find('version')
if root.tag!='project' or version is None: fail('Malformed permanent manifest')
return {'name':root.attrib['name'],'package':root.attrib['qualified_name'],
'version':version.attrib['value'],'version_code':int(version.attrib['code'])}
except (OSError, ET.ParseError, KeyError, ValueError) as error: fail(f'Malformed permanent manifest: {error}')
def current_package() -> tuple[dict, str]:
if not CURRENT_BUILD_METADATA.is_file(): raise RuntimeError('Current build metadata is missing')
build=load_json(CURRENT_BUILD_METADATA,'Current build metadata is missing')
if set(build)!={'schema','build_id','status','cpu','abi','application','upstream_commit','resource_count','compile','package','package_metadata','error'} or build.get('schema')!=CURRENT_BUILD_SCHEMA: fail('Invalid current build schema')
if build.get('status')!='PASS': raise RuntimeError('Current build is not PASS')
app=build.get('application'); identity=manifest_identity()
if not isinstance(app,dict) or any(app.get(key)!=value for key,value in identity.items()): fail('Build application identity mismatch')
if build.get('package_metadata')!='out/current/package.json': fail('Build package metadata locator mismatch')
package=load_json(CURRENT_PACKAGE_METADATA,'Current package metadata is missing')
if set(package)!={'schema','build_id','application','cpu','abi','apk'} or package.get('schema')!=CURRENT_PACKAGE_SCHEMA: fail('Invalid current package schema')
if package.get('build_id')!=build.get('build_id') or package.get('application')!=identity: fail('Package metadata identity mismatch')
if package.get('cpu')!=build.get('cpu') or package.get('abi')!=build.get('abi'): fail('Package build mismatch')
apk=package.get('apk')
if not isinstance(apk,dict) or set(apk)!={'file','sha256','size','native_library_path_in_apk'} or apk.get('file')!='out/current/kam-mobile.apk': fail('Package APK locator mismatch')
if package['cpu'] not in CPUS or package['abi']!=CPUS[package['cpu']][0]: fail('Invalid package ABI')
if not CURRENT_APK.is_file() or apk.get('sha256')!=sha256(CURRENT_APK): fail('Current APK missing or changed')
return package, identity['package']
def main() -> int:
ap=argparse.ArgumentParser(description=__doc__); ap.add_argument('--serial'); a=ap.parse_args()
try:
package,pkg=current_package()
except ProbeError as error:
print('FAIL\n'+str(error)); return 1
except RuntimeError as error:
print('BLOCKED\n'+str(error)); return 2
except (KeyError, TypeError) as error:
print('FAIL\n'+str(error)); return 1
if not shutil.which('adb'):
print('BLOCKED\nadb is not installed on host'); return 2
try:
devices=[line.split()[:2] for line in Adb(None).text('devices').splitlines()[1:] if line.strip()]
if len(devices)!=1 or len(devices[0])!=2 or devices[0][1]!='device': fail('Require exactly one authorized device')
serial=devices[0][0]
if a.serial and a.serial!=serial: fail('Requested serial is not the sole authorized device')
adb=Adb(serial)
if package['abi'] not in adb.text('shell','getprop','ro.product.cpu.abilist').split(','): fail('Device ABI does not support package ABI')
except (ProbeError, OSError, subprocess.TimeoutExpired) as error:
print('BLOCKED\n'+str(error)); return 2
try:
install=adb.run('install','-r',str(CURRENT_APK),check=False)
if install.returncode and b'INSTALL_FAILED_UPDATE_INCOMPATIBLE' in install.stdout+install.stderr:
uninstall=adb.run('uninstall',pkg,check=False);save_command(out,'uninstall.log',uninstall);record['install']['uninstall_for_ephemeral_key']={'exit':uninstall.returncode,'result':uninstall.stdout.decode(errors='replace').strip()}
install=adb.run('install',str(apk),check=False);save_command(out,'install-retry.log',install);record['install'].update(retry_exit=install.returncode,retry_result=install.stdout.decode(errors='replace').strip())
if install.returncode: raise ProbeError('APK install failed; see install.log')
run_as=adb.run('shell','run-as',pkg,'id',check=False);save_command(out,'run-as.log',run_as);record['run_as']={'status':'PASS' if run_as.returncode==0 else 'BLOCKED','output':run_as.stdout.decode(errors='replace').strip(),'error':run_as.stderr.decode(errors='replace').strip()}
if run_as.returncode: record['infrastructure_blocker']={'stage':'run_as_preflight','reason':'run-as unavailable for '+pkg};raise ProbeError(record['infrastructure_blocker']['reason'])
if a.stage_file:
if not a.stage_file.is_file(): raise ProbeError('Stage file not found: '+str(a.stage_file))
rel='files/kam-architecture-probe/driver-staged-fixture.bin';adb.run('shell','run-as',pkg,'sh','-c','mkdir -p files/kam-architecture-probe')
p=subprocess.run(adb.base+['exec-in','run-as',pkg,'sh','-c','cat > '+rel],input=a.stage_file.read_bytes(),stdout=subprocess.PIPE,stderr=subprocess.PIPE);save_command(out,'stage.log',p)
size=adb.text('shell','run-as',pkg,'stat','-c','%s',rel,check=False);record['fixture_staging_driver']={'status':'PASS' if p.returncode==0 and size==str(a.stage_file.stat().st_size) else 'FAIL','source':str(a.stage_file),'bytes':a.stage_file.stat().st_size,'device_bytes':size}
adb.run('shell','am','force-stop',pkg); listing=adb.text('shell','run-as',pkg,'find','.','-name','runtime.json',check=False)
for name in own_report_paths(listing): adb.run('shell','run-as',pkg,'rm','--',name)
adb.run('logcat','-c');record['launch_marker_epoch_ms']=int(time.time()*1000)
component=adb.text('shell','cmd','package','resolve-activity','--brief','-a','android.intent.action.MAIN','-c','android.intent.category.LAUNCHER',pkg); candidates=[x.strip() for x in component.splitlines() if x.strip().startswith(pkg+'/')]
if len(candidates)!=1 or not re.fullmatch(r'[\w.$/]+',candidates[0]): raise ProbeError('Cannot resolve probe launch Activity')
launch=adb.run('shell','am','start','-W','-n',candidates[0],check=False);save_command(out,'launch.log',launch);record['launch']={'activity':candidates[0],'exit':launch.returncode,'result':launch.stdout.decode(errors='replace').strip()}
if launch.returncode: raise ProbeError('Launch failed; see launch.log')
data,path=wait_runtime(adb,pkg,lambda d:any(x.get('id')=='android.startup' and x.get('status')=='PASS' for x in d.get('tests',[])),a.seconds)
if data is None: raise ProbeError('android.startup was not observed before timeout; see logcat.log and process.log')
if a.exercise_lifecycle:
adb.run('shell','input','keyevent','KEYCODE_HOME');time.sleep(1);adb.run('shell','am','start','-W','-n',candidates[0])
expected=[]
if a.exercise_lifecycle and a.profile=='platform': expected=['decision.schedule.callback_progress','decision.schedule.worker_progress','decision.schedule.lifecycle']
if a.profile=='platform': expected += ['render.frame_callbacks','decision.text.cwstring_unit','decision.text.cp1251_decode','decision.text.cp1251_encode']
if expected: data,path=wait_runtime(adb,pkg,lambda d: all(any(x.get('id')==name for x in d.get('tests',[])) for name in expected),a.seconds)
if data is None: raise ProbeError('Runtime report disappeared; see logcat.log and process.log')
out.joinpath('runtime.json').write_text(json.dumps(data,ensure_ascii=False,indent=2)+'\n',encoding='utf-8');record['runtime_path']=path
record['errors']+=validate_runtime(data,package,require_frames=package['profile'] not in DECISION_PROFILES)
if expected and not all(any(x.get('id')==name for x in data.get('tests',[])) for name in expected): record['errors'].append('Timed out waiting for observations: '+', '.join(expected))
if a.stage_file and record.get('fixture_staging_driver',{}).get('status')!='PASS': record['errors'].append('Existing server fixture staging size mismatch')
record['status']='FAIL' if record['errors'] else 'AUTOMATIC_SMOKE_PASS'
except (ProbeError,OSError,ValueError,KeyError,subprocess.TimeoutExpired) as e:
record['errors'].append(str(e))
if record['preflight'].get('status')=='BLOCKED': record['infrastructure_blocker']={'stage':'device_preflight','reason':record['preflight'].get('reason',str(e))}
finally:
if adb:
p=adb.run('shell','pidof',record.get('package',{}).get('package',''),check=False);save_command(out,'process.log',p);record['process_after_launch']=p.stdout.decode(errors='replace').strip()
p=adb.run('logcat','-d','-v','threadtime',check=False);save_command(out,'logcat.log',p)
write_json(out/'device-result.json',record);print(record['status']+'\n'+str(out));[print(x) for x in record['errors']]
return 0 if record['status']=='AUTOMATIC_SMOKE_PASS' else 1 if record['status']=='FAIL' else 2
adb.run('uninstall',pkg,check=False); install=adb.run('install',str(CURRENT_APK),check=False)
if install.returncode: fail('APK install failed')
candidates=[line.strip() for line in adb.text('shell','cmd','package','resolve-activity','--brief','-a','android.intent.action.MAIN','-c','android.intent.category.LAUNCHER',pkg).splitlines() if line.strip()]
if len(candidates)!=1 or not candidates[0].startswith(pkg+'/'): fail('Cannot resolve exactly one application launcher activity')
if adb.run('shell','am','start','-W','-n',candidates[0],check=False).returncode: fail('Launch failed')
except (ProbeError, OSError, subprocess.TimeoutExpired) as error:
print('FAIL\n'+str(error)); return 1
print('PASS'); return 0
if __name__=='__main__': raise SystemExit(main())