Release Preview 20 shared desktop, Android and Sargo updates

Restore Gaia settings and media workflows, extend native network adapters, and align the APK payload. Generate French/English changelogs and website entries from the simplified JSON source.

Validation: shared JS and Tundra unit tests; desktop workflows and persistence; signed APK payload parity; Sargo RAM and installed startup with boot readback. Android runtime qualification is recorded separately.
This commit is contained in:
OverSu 2026-10-13 00:19:45 +02:00
commit 1311bed5e9
170 changed files with 19698 additions and 2495 deletions

View file

@ -0,0 +1,22 @@
import importlib.util
from pathlib import Path
import unittest
spec=importlib.util.spec_from_file_location('bluetooth',Path(__file__).resolve().parents[1]/'runtime/bluetooth-control.py')
bt=importlib.util.module_from_spec(spec);spec.loader.exec_module(bt)
class Bluetooth(unittest.TestCase):
def test_reject_arbitrary_path_command_and_non_boolean_power(self):
for value in [dict(operation='exec',command='id'),dict(operation='pair',address='/org/bluez/hci0'),
dict(operation='power',enabled='false'),dict(operation='rename',name='\nName')]:
with self.assertRaises(ValueError):bt.validate(dict(version=1,**value))
def test_valid_pair_and_name(self):
self.assertEqual(bt.validate(dict(version=1,operation='pair',address='AA:BB:CC:DD:EE:FF')),'pair')
self.assertEqual(bt.validate(dict(version=1,operation='rename',name='Vulpes OS')),'rename')
def test_cod_for_gaia_and_no_manufactured_connection(self):
value=bt.device_record(dict(Address='AA:BB:CC:DD:EE:FF',Class=0x240404,Paired=True))
self.assertEqual(value['cod']['majorDeviceClass'],4)
self.assertEqual(value['cod']['minorDeviceClass'],1)
self.assertTrue(value['paired']);self.assertFalse(value['connected'])
if __name__=='__main__':unittest.main()

View file

@ -0,0 +1,69 @@
# SPDX-License-Identifier: MPL-2.0
import importlib.util
from pathlib import Path
import tempfile
from types import SimpleNamespace
import unittest
from unittest.mock import patch
spec=importlib.util.spec_from_file_location('wifi',Path(__file__).resolve().parents[1]/'runtime/wifi-control.py')
wifi=importlib.util.module_from_spec(spec);spec.loader.exec_module(wifi)
class HotspotTests(unittest.TestCase):
def test_private_profile_roundtrip_and_validation(self):
with tempfile.TemporaryDirectory() as directory, patch.object(wifi,'active_wifi_uuid',return_value=None), patch.object(wifi,'nm',return_value=SimpleNamespace(returncode=0)) as nm:
root=Path(directory)
request={'operation':'hotspotSave','ssid':'Vulpes é','security':'wpa2-psk','password':r' space\s tail '}
value=wifi.dispatch(request,root)
self.assertEqual(value['password'],request['password']);self.assertEqual(value['ssid'],request['ssid'])
path=root/'vulpes-hotspot.nmconnection'
self.assertEqual(path.stat().st_mode&0o777,0o600)
self.assertIn('method=shared',path.read_text());self.assertIn('proto=rsn',path.read_text())
self.assertEqual(wifi.saved_profiles(root),[])
for invalid in ({'ssid':'x\ninject'},{'password':'short'},{'security':'wep'},{'ssid':'é'*17},{'extra':True}):
before=path.read_bytes();count=nm.call_count
with self.assertRaises(wifi.WifiError):wifi.dispatch(request|invalid,root)
self.assertEqual(path.read_bytes(),before);self.assertEqual(nm.call_count,count)
with patch.object(wifi,'active_wifi_uuid',return_value=wifi.HOTSPOT_UUID):
with self.assertRaisesRegex(wifi.WifiError,'HOTSPOT_IN_USE'):wifi.dispatch(request,root)
def test_password_is_not_rewritten_as_profile_configuration(self):
with tempfile.TemporaryDirectory() as directory, patch.object(wifi,'active_wifi_uuid',return_value=None), patch.object(wifi,'nm',return_value=SimpleNamespace(returncode=0)):
for password in ['autoconnect=true','mode=infrastructure','id=Vulpes Wi-Fi']:
result=wifi.dispatch({'operation':'hotspotSave','ssid':'test','security':'wpa2-psk','password':password},Path(directory))
self.assertEqual(result['password'],password)
def test_save_failure_restores_previous(self):
with tempfile.TemporaryDirectory() as directory, patch.object(wifi,'active_wifi_uuid',return_value=None):
root=Path(directory);path=root/'vulpes-hotspot.nmconnection'
request={'operation':'hotspotSave','ssid':'test','security':'wpa2-psk','password':'testpassword'}
with patch.object(wifi,'nm',return_value=SimpleNamespace(returncode=1)) as nm:
with self.assertRaisesRegex(wifi.WifiError,'HOTSPOT_SAVE_FAILED'):wifi.dispatch(request,root)
self.assertFalse(path.exists());self.assertIn(('connection','delete','uuid',wifi.HOTSPOT_UUID),[c.args for c in nm.call_args_list])
path.write_text('previous')
with self.assertRaisesRegex(wifi.WifiError,'HOTSPOT_SAVE_FAILED'):wifi.dispatch(request,root)
self.assertEqual(path.read_text(),'previous')
def test_power_readback_and_reconnect_after_failure_or_stop(self):
previous='8a16e3b3-c49b-44ca-9041-c3bac464f748'
for fail in (False,True):
active=[previous];commands=[]
def nm(*args,**kwargs):
commands.append(args)
if args[:2]==('connection','up'):
if fail and args[-1]==wifi.HOTSPOT_UUID:return SimpleNamespace(returncode=1)
active[0]=args[-1]
if args[:2]==('connection','down'):active[0]=None
return SimpleNamespace(returncode=0)
with tempfile.TemporaryDirectory() as directory, patch.object(wifi,'active_wifi_uuid',side_effect=lambda:active[0]),patch.object(wifi,'nm',side_effect=nm):
root=Path(directory)
with patch.object(wifi,'HOTSPOT_RETURN',root/'return.json'):
wifi.dispatch({'operation':'hotspotSave','ssid':'test','security':'open','password':''},root)
if fail:
with self.assertRaisesRegex(wifi.WifiError,'HOTSPOT_START_FAILED'):wifi.dispatch({'operation':'hotspotPower','enabled':True},root)
else:
self.assertTrue(wifi.dispatch({'operation':'hotspotPower','enabled':True},root)['enabled'])
self.assertFalse(wifi.dispatch({'operation':'hotspotPower','enabled':False},root)['enabled'])
self.assertEqual(active[0],previous);self.assertFalse(wifi.HOTSPOT_RETURN.exists())
self.assertIn(('connection','up','uuid',previous),commands)
if __name__=='__main__':unittest.main()

View file

@ -0,0 +1,69 @@
import base64,contextlib,importlib.util,json,tempfile,unittest,subprocess,sys,shutil
from pathlib import Path
from unittest.mock import patch
ROOT=Path(__file__).resolve().parents[2]
spec=importlib.util.spec_from_file_location('mms_control',ROOT/'tundra/runtime/mms-control.py');m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m)
class MMS(unittest.TestCase):
def setUp(self):
self.tmp=tempfile.TemporaryDirectory();self.old=m.ROOT;m.ROOT=Path(self.tmp.name)
def tearDown(self):m.ROOT=self.old;self.tmp.cleanup()
def test_reject_paths_and_oversize(self):
value={'recipients':['+15550199'],'parts':[{'id':'../secret','type':'text/plain','data':'AA=='}]}
with self.assertRaises(ValueError):m.validate_send(value)
value['parts'][0]['id']='text.txt';value['parts'][0]['data']=base64.b64encode(b'x'*(m.LIMIT+1)).decode()
with self.assertRaises(ValueError):m.validate_send(value)
def test_queue_acceptance_is_not_delivery(self):
with patch.object(m,'codec',return_value={'data':'AA==','transaction':'t'}):
r=m.request({'version':1,'operation':'send','recipients':['+15550199'],'parts':[{'id':'text.txt','type':'text/plain','data':'AA=='}]})
self.assertEqual(m.request({'version':1,'operation':'state','path':r['path']})['state'],'pending')
def test_operator_acceptance_required(self):
row={'direction':'outgoing','encoded':{'data':'AA==','transaction':'t'}}
with patch.object(m,'bearer',return_value=contextlib.nullcontext({'url':'http://mmsc.invalid'})),patch.object(m,'transfer',return_value=b'confirmation'),patch.object(m,'codec',return_value={'accepted':False}):
with self.assertRaisesRegex(ValueError,'MMS_REJECTED'):m.process(row)
def test_ack_does_not_delete_outgoing_or_unread_queue(self):
m.save(m.ROOT/'queue.json',[{'id':'o','direction':'outgoing','state':'pending'},{'id':'i','direction':'incoming','state':'received','message':{'receipt':'i'}}])
m.request({'version':1,'operation':'ack','ids':['o','i']});r=m.records()
self.assertEqual(r[0]['state'],'pending');self.assertEqual(r[1]['state'],'stored');self.assertNotIn('message',r[1])
def test_incoming_network_error_keeps_notification_retryable(self):
m.save(m.ROOT/'queue.json',[{'id':'i','direction':'incoming','state':'pending','notification':{}}])
with patch.object(m,'process',side_effect=ValueError('MMS_DATA_UNAVAILABLE')):self.assertTrue(m.process_once())
row=m.records()[0];self.assertEqual(row['state'],'pending');self.assertGreater(row['retryAt'],0)
self.assertFalse(m.process_once())
def test_outgoing_transport_error_is_never_retried_automatically(self):
m.save(m.ROOT/'queue.json',[{'id':'o','direction':'outgoing','state':'pending'}])
with patch.object(m,'process',side_effect=ValueError('MMS_TRANSPORT_ERROR')):m.process_once()
self.assertEqual(m.records()[0]['state'],'failed');self.assertFalse(m.process_once())
def test_retrieval_ack_retry_survives_gaia_acknowledgement(self):
m.save(m.ROOT/'queue.json',[{'id':'i','direction':'incoming','state':'received','ackPending':True,'notification':{},'message':{}}])
m.request({'version':1,'operation':'ack','ids':['i']})
with patch.object(m,'process',return_value={'ackPending':False}):m.process_once()
row=m.records()[0];self.assertEqual(row['state'],'stored');self.assertFalse(row['ackPending'])
def test_connection_failure_closes_http_socket(self):
from unittest.mock import Mock
sock=Mock();sock.connect.side_effect=OSError('network unavailable')
with patch.object(m.socket,'socket',return_value=sock):
with self.assertRaises(OSError):m.transfer({'proxy':True,'url':'http://mmsc.invalid','host':'192.0.2.1','port':8080,'interface':'test0','address':'192.0.2.2'},'http://mmsc.invalid',b'body')
sock.close.assert_called_once()
@unittest.skipUnless(shutil.which('dbus-run-session') and importlib.util.find_spec('dbus') and importlib.util.find_spec('gi'),'D-Bus/GLib integration dependencies unavailable')
def test_private_bus_cleanup_does_not_exit_the_daemon(self):
script='''
import os,sys,importlib.util
os.environ['DBUS_SYSTEM_BUS_ADDRESS']=os.environ['DBUS_SESSION_BUS_ADDRESS']
from dbus.mainloop.glib import DBusGMainLoop
from gi.repository import GLib
DBusGMainLoop(set_as_default=True)
s=importlib.util.spec_from_file_location('mms',sys.argv[1]);m=importlib.util.module_from_spec(s);s.loader.exec_module(m)
loop=GLib.MainLoop()
def close_bearer():
try:
with m.bearer():pass
except Exception:pass
GLib.timeout_add(100,alive)
return False
def alive():
print('daemon-survived');loop.quit();return False
GLib.idle_add(close_bearer);loop.run()
'''
r=subprocess.run(['dbus-run-session',sys.executable,'-c',script,str(ROOT/'tundra/runtime/mms-control.py')],capture_output=True,text=True,timeout=5)
self.assertEqual(r.returncode,0,r.stderr);self.assertIn('daemon-survived',r.stdout)
if __name__=='__main__':unittest.main()

View file

@ -0,0 +1,115 @@
# SPDX-License-Identifier: MPL-2.0
import importlib.util
from pathlib import Path
import unittest
from unittest.mock import patch
spec=importlib.util.spec_from_file_location('mobile',Path(__file__).resolve().parents[1]/'runtime/mobile-settings.py')
mobile=importlib.util.module_from_spec(spec);spec.loader.exec_module(mobile)
class MobileSettingsTests(unittest.TestCase):
def test_sim_result_excludes_subscriber_identity_and_pin_counters(self):
with patch.object(mobile,'properties',return_value={'Present':True,'CardIdentifier':'test-card','SubscriberIdentity':'secret','Retries':{'pin':3},'PinRequired':'none','ServiceProviderName':'Test','FixedDialing':False}):
result=mobile.request({'version':1,'operation':'mobileSettings','action':'sim'})
self.assertEqual(result['id'],'test-card')
self.assertNotIn('secret',str(result));self.assertNotIn('Retries',result)
def test_only_read_queries_allowed(self):
for value in ({'action':'setPin'},{'action':'sim','command':'anything'},{'action':'calls','value':True}):
with self.assertRaises(ValueError):mobile.request(value)
def test_smsc_has_no_message_content(self):
with patch.object(mobile,'properties',return_value={'ServiceCenterAddress':'test-center','Messages':['private']}):
self.assertEqual(mobile.request({'action':'smsc'}),{'address':'test-center','deliveryReports':None})
def test_call_query_cannot_report_unknown_as_disabled(self):
with patch.object(mobile,'properties',return_value={}):
self.assertIsNone(mobile.request({'action':'calls'})['VoiceCallWaiting'])
def test_report_setting_validates_boolean_and_reads_back_modem(self):
from types import SimpleNamespace
with self.assertRaises(ValueError):mobile.request({'action':'deliveryReports','enabled':'true'})
with patch.object(mobile.subprocess,'run',return_value=SimpleNamespace(returncode=0)) as run, patch.object(mobile,'properties',return_value={'UseDeliveryReports':True}):
self.assertEqual(mobile.request({'action':'deliveryReports','enabled':True}),{'deliveryReports':True})
self.assertEqual(run.call_args.args[0][-4:],['sv','UseDeliveryReports','b','true'])
with patch.object(mobile.subprocess,'run',return_value=SimpleNamespace(returncode=0)), patch.object(mobile,'properties',return_value={'UseDeliveryReports':False}):
with self.assertRaises(RuntimeError):mobile.request({'action':'deliveryReports','enabled':True})
def test_data_write_requires_modem_readback(self):
with patch.object(mobile,'set_property') as setter, patch.object(mobile,'properties',return_value={'Powered':False}):
with self.assertRaisesRegex(RuntimeError,'RADIO_REQUEST_FAILED'):
mobile.request({'action':'dataEnabled','enabled':True})
setter.assert_called_once_with('ConnectionManager','Powered','b',True)
def test_clock_rejects_path_and_type_injection_before_execution(self):
with patch.object(mobile.subprocess,'run') as command:
for value in ({'action':'setTimezone','timezone':'../../etc/passwd'}, {'action':'setTimezone','timezone':'/etc/passwd'},
{'action':'setTime','milliseconds':True}, {'action':'setTime','milliseconds':float('nan')},
{'action':'clock','timezone':'UTC'}, {'action':'setTime','milliseconds':0}):
with self.assertRaises(ValueError):mobile.device_request(value)
command.assert_not_called()
def test_clock_returns_actual_service_state(self):
from types import SimpleNamespace
with patch.object(mobile.subprocess,'run',return_value=SimpleNamespace(returncode=0,stdout='Timezone=Europe/Paris\nNTP=no\nCanNTP=no\n')), patch.object(mobile,'clock_automatic',return_value=False):
self.assertEqual(mobile.device_request({'action':'clock'}), {'timezone':'Europe/Paris','automatic':False,'automaticAvailable':True})
def test_apn_read_never_returns_password_or_ims_context(self):
import json
from types import SimpleNamespace
props=lambda kind:{'Type':{'data':kind},'Password':{'data':'private-secret'},'AccessPointName':{'data':'example'},'Active':{'data':False}}
payload={'data':[[['/ril_0/context1',props('internet')],['/ril_0/context3',props('ims')]]]}
with patch.object(mobile.subprocess,'run',return_value=SimpleNamespace(returncode=0,stdout=json.dumps(payload))):
rows=mobile.request({'action':'apns'})
self.assertEqual(len(rows),1);self.assertTrue(rows[0]['hasPassword'])
self.assertNotIn('private-secret',str(rows))
def test_apn_edit_reads_back_and_rolls_back_failed_write(self):
path='/ril_0/context1';original={'Type':'internet','Active':False,'AccessPointName':'example','Username':'old'}
state=dict(original)
def write(path,name,value):state[name]=value
with patch.object(mobile,'contexts',side_effect=lambda:{path:dict(state)}),patch.object(mobile,'set_context',side_effect=write):
result=mobile.request({'action':'editApn','id':path,'fields':{'apn':'new.example','user':'new'}})
self.assertEqual(result['fields']['apn'],'new.example');self.assertEqual(state['Username'],'new')
state=dict(original)
def fail(path,name,value):
state[name]=value
if name=='Username' and value=='new':raise RuntimeError('APN_WRITE_FAILED')
with patch.object(mobile,'contexts',side_effect=lambda:{path:dict(state)}),patch.object(mobile,'set_context',side_effect=fail):
with self.assertRaisesRegex(RuntimeError,'APN_WRITE_FAILED'):
mobile.request({'action':'editApn','id':path,'fields':{'apn':'new.example','user':'new'}})
self.assertEqual(state,original)
def test_apn_failed_rollback_is_not_reported_as_restored(self):
original={'/ril_0/context1':{'Type':'internet','Active':False,'AccessPointName':'example'}}
with patch.object(mobile,'contexts',side_effect=[original,RuntimeError('RADIO_NOT_READY')]),patch.object(mobile,'set_context',side_effect=RuntimeError('APN_WRITE_FAILED')):
with self.assertRaisesRegex(RuntimeError,'APN_ROLLBACK_FAILED'):
mobile.request({'action':'editApn','id':'/ril_0/context1','fields':{'apn':'new.example'}})
def test_apn_rejects_in_use_profiles_unknown_fields_and_injection(self):
with patch.object(mobile,'contexts',return_value={'/ril_0/context1':{'Type':'internet','Active':True,'AccessPointName':'example'}}),patch.object(mobile,'set_context') as write:
with self.assertRaisesRegex(RuntimeError,'APN_IN_USE'):mobile.request({'action':'editApn','id':'/ril_0/context1','fields':{'apn':'new.example'}})
for fields in ({'command':'id'},{'apn':'bad\nname'},{'protocol':'evil'},{'mmsc':'file:///etc/passwd'},{'password':'x'*513}):
with self.assertRaises(ValueError):mobile.request({'action':'editApn','id':'/ril_0/context1','fields':fields})
with self.assertRaises(ValueError):mobile.request({'action':'editApn','id':'/ril_0/voicecall1','fields':{'apn':'new'}})
write.assert_not_called()
def test_radio_modes_match_real_modem_capabilities(self):
state={'TechnologyPreference':'lte','AvailableTechnologies':['gsm','umts','lte']}
def write(interface,name,signature,value):state[name]=value
with patch.object(mobile,'properties',side_effect=lambda _:dict(state)),patch.object(mobile,'set_property',side_effect=write):
self.assertEqual(mobile.request({'action':'setRadioMode','mode':'any'})['mode'],'any')
with self.assertRaises(ValueError):mobile.request({'action':'setRadioMode','mode':'nr'})
def test_supplementary_settings_validate_and_confirm_network_state(self):
state={'HideCallerId':'default','VoiceCallWaiting':'enabled'}
def write(interface,name,signature,value,**kwargs):state[name]=value
with patch.object(mobile,'set_property',side_effect=write) as setter,patch.object(mobile,'properties',side_effect=lambda *a,**k:dict(state)):
self.assertEqual(mobile.request({'action':'setCallWaiting','enabled':False})['VoiceCallWaiting'],'disabled')
self.assertEqual(mobile.request({'action':'setCallerId','mode':'enabled'})['HideCallerId'],'enabled')
setter.assert_called_with('CallSettings','HideCallerId','s','enabled',timeout=20)
with patch.object(mobile,'set_property'),patch.object(mobile,'properties',return_value={'HideCallerId':'unknown'}):
with self.assertRaisesRegex(RuntimeError,'RADIO_READBACK_FAILED'):mobile.request({'action':'setCallerId','mode':'default'})
with patch.object(mobile,'set_property') as setter:
for value in ({'action':'setCallWaiting','enabled':'true'},{'action':'setCallerId','mode':'secret'},{'action':'calls','number':'1'}):
with self.assertRaises(ValueError):mobile.request(value)
setter.assert_not_called()
def test_forwarding_only_changes_requested_rule_and_checks_readback(self):
state={'VoiceUnconditional':'','VoiceBusy':'123','VoiceNoReply':'','VoiceNotReachable':''}
def write(interface,name,signature,value,**kwargs):state[name]=value
with patch.object(mobile,'set_property',side_effect=write),patch.object(mobile,'properties',side_effect=lambda *a,**k:dict(state)):
result=mobile.request({'action':'setForwarding','reason':'noReply','enabled':True,'number':'+33123456789'})
self.assertEqual(result['noReply'],'+33123456789');self.assertEqual(result['busy'],'123')
result=mobile.request({'action':'setForwarding','reason':'noReply','enabled':False,'number':''})
self.assertEqual(result['noReply'],'');self.assertEqual(result['busy'],'123')
with patch.object(mobile,'set_property') as setter:
for value in ({'reason':'all','enabled':False,'number':''},{'reason':'busy','enabled':True,'number':'*123#'},{'reason':'busy','enabled':False,'number':'123'}):
with self.assertRaises(ValueError):mobile.request({'action':'setForwarding',**value})
setter.assert_not_called()
if __name__=='__main__':unittest.main()

View file

@ -0,0 +1,31 @@
import importlib.util,json,tempfile,unittest
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
spec=importlib.util.spec_from_file_location('mobile',Path(__file__).parents[1]/'runtime/mobile-settings.py')
m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m)
class NetworkClock(unittest.TestCase):
def test_monotonic_age_is_added_to_nitz(self):
payload={'data':[{'UTC':{'data':1791836110},'Received':{'data':530}}]}
with patch.object(m.subprocess,'run',return_value=SimpleNamespace(returncode=0,stdout=json.dumps(payload))),patch.object(m.time,'monotonic',return_value=550):
self.assertEqual(m.network_time(),1791836130)
def test_rejects_future_or_missing_received_timestamp(self):
for received in (None,999999,True):
payload={'data':[{'UTC':{'data':1791836110},'Received':{'data':received}}]}
with patch.object(m.subprocess,'run',return_value=SimpleNamespace(returncode=0,stdout=json.dumps(payload))),patch.object(m.time,'monotonic',return_value=550):
with self.assertRaisesRegex(RuntimeError,'NETWORK_TIME_UNAVAILABLE'):m.network_time()
def test_manual_mode_persists_and_prevents_network_write(self):
with tempfile.TemporaryDirectory() as folder,patch.object(m,'CLOCK_CONFIG',Path(folder)/'clock.json'),patch.object(m,'network_time') as read,patch.object(m.time,'clock_settime') as write:
self.assertTrue(m.clock_automatic());m.save_clock_automatic(False)
self.assertFalse(m.clock_automatic());m.sync_network_clock();read.assert_not_called();write.assert_not_called()
self.assertEqual(m.CLOCK_CONFIG.stat().st_mode & 0o777,0o600)
def test_only_corrects_a_real_difference(self):
with patch.object(m,'clock_automatic',return_value=True),patch.object(m,'network_time',return_value=1791836130),patch.object(m.time,'time',return_value=1791836129),patch.object(m.time,'clock_settime') as write:
m.sync_network_clock();write.assert_not_called()
with patch.object(m,'clock_automatic',return_value=True),patch.object(m,'network_time',return_value=1791836130),patch.object(m.time,'time',return_value=1791000000),patch.object(m.time,'clock_settime') as write:
m.sync_network_clock();write.assert_called_once_with(m.time.CLOCK_REALTIME,1791836130)
def test_manual_write_rejected_in_auto_mode(self):
with patch.object(m,'clock_automatic',return_value=True),patch.object(m.subprocess,'run') as command:
with self.assertRaisesRegex(RuntimeError,'AUTOMATIC_TIME_ENABLED'):m.device_request({'action':'setTime','milliseconds':1791836130000})
command.assert_not_called()
if __name__=='__main__':unittest.main()

View file

@ -1,4 +1,7 @@
import importlib.util
import json
import socket
import subprocess
from pathlib import Path
import unittest
from types import SimpleNamespace
@ -12,6 +15,107 @@ spec.loader.exec_module(radio)
class RadioControlTests(unittest.TestCase):
def test_message_state_accepts_modem_hex_case_but_not_other_objects(self):
inbox = SimpleNamespace(outgoing_state=lambda path: 'sent')
spec = SimpleNamespace(loader=SimpleNamespace(exec_module=lambda module: None))
with patch.object(radio.importlib.util, 'spec_from_file_location', return_value=spec), \
patch.object(radio.importlib.util, 'module_from_spec', return_value=inbox):
for path in ['/ril_0/message_18DE1E64E8B16EA9E5E57366556C971B822E2614',
'/ril_0/message_ab12']:
self.assertEqual(radio.request({'version':1, 'operation':'messageState',
'path':path}), {'state':'sent'})
for path in ['/ril_0/voicecall01', '/ril_0/message_Z', '/ril_0/message_1/other']:
with self.assertRaisesRegex(ValueError, 'INVALID_SMS_PATH'):
radio.request({'version':1, 'operation':'messageState', 'path':path})
def test_slow_settings_transaction_does_not_block_call_controls(self):
import threading
started, release = threading.Event(), threading.Event()
worker = radio.SettingsWorker()
settings_server, settings_client = socket.socketpair()
voice_server, voice_client = socket.socketpair()
def request(value):
if value['operation'] == 'mobileSettings':
started.set()
if not release.wait(2): raise RuntimeError('test settings timeout')
return {'saved':True}
return {'calls':[]}
try:
with patch.object(radio,'request',side_effect=request), settings_client, voice_server, voice_client:
settings_client.sendall(b'{"version":1,"operation":"mobileSettings","action":"editApn"}\n')
self.assertTrue(radio.handle_connection(settings_server,worker))
self.assertTrue(started.wait(1))
voice_client.sendall(b'{"version":1,"operation":"calls"}\n')
self.assertFalse(radio.handle_connection(voice_server,worker))
voice_client.settimeout(1)
self.assertEqual(json.loads(voice_client.recv(4096))['result'],{'calls':[]})
release.set()
settings_client.settimeout(1)
self.assertTrue(json.loads(settings_client.recv(4096))['result']['saved'])
finally:
release.set(); worker.close(); settings_server.close()
def test_stale_queued_settings_are_not_applied(self):
worker=radio.SettingsWorker()
server, client=socket.socketpair()
worker.slots.acquire()
try:
with client, patch.object(radio.time,'monotonic',return_value=20), patch.object(radio,'request') as request:
worker.run(server,{'operation':'mobileSettings'},0)
self.assertEqual(json.loads(client.recv(4096)),{'ok':False,'error':'RADIO_BUSY'})
request.assert_not_called()
finally:worker.close()
def test_full_settings_queue_is_bounded(self):
worker=radio.SettingsWorker()
server, client=socket.socketpair()
try:
for _ in range(8):self.assertTrue(worker.slots.acquire(blocking=False))
with server, client:
client.sendall(b'{"version":1,"operation":"mobileSettings","action":"sim"}\n')
self.assertFalse(radio.handle_connection(server,worker))
self.assertEqual(json.loads(client.recv(4096)),{'ok':False,'error':'RADIO_BUSY'})
finally:
for _ in range(8):worker.slots.release()
worker.close()
def test_closed_client_does_not_crash_broker_or_repeat_sms(self):
server, client = socket.socketpair()
with server:
client.sendall(b'{"version":1,"operation":"sms"}\n')
client.close()
with patch.object(radio, 'request', return_value={'state':'pending'}) as request:
radio.handle_connection(server)
request.assert_called_once()
def test_incomplete_request_cannot_dial(self):
server, client = socket.socketpair()
with server, client:
client.sendall(b'{"version":1,"operation":"dial"}')
client.shutdown(socket.SHUT_WR)
with patch.object(radio, 'request') as request:
radio.handle_connection(server)
request.assert_not_called()
def test_modem_timeout_does_not_disclose_sms_arguments(self):
server, client = socket.socketpair()
with server, client:
client.sendall(b'{"version":1,"operation":"sms"}\n')
error = subprocess.TimeoutExpired(['busctl', 'private-number', 'private-body'], 15)
with patch.object(radio, 'request', side_effect=error):
radio.handle_connection(server)
response = json.loads(client.recv(4096))
self.assertEqual(response, {'ok':False, 'error':'RADIO_TIMEOUT'})
def test_slow_client_timeout_does_not_escape_handler(self):
from unittest.mock import Mock
conn = Mock()
conn.recv.side_effect = socket.timeout()
with patch.object(radio, 'request') as request:
radio.handle_connection(conn)
request.assert_not_called()
conn.sendall.assert_not_called()
def test_void_dbus_success_is_not_an_invalid_reply(self):
with patch.object(radio.subprocess, 'run', return_value=SimpleNamespace(
returncode=0, stdout='', stderr='')):

View file

@ -78,6 +78,21 @@ on boot
self.assertNotIn('critical',output)
self.assertIn('class core',output)
def test_hardware_policy_keeps_one_primary_audio_owner(self):
from unittest.mock import patch
source = '''service vendor.audio-hal-2-0 /vendor/bin/hw/android.hardware.audio@2.0-service
class hal
service vendor.camera-provider-2-4 /vendor/bin/hw/android.hardware.camera.provider@2.4-service
class hal
on boot
start vendor.audio-hal-2-0
start vendor.camera-provider-2-4
'''
with patch.object(policy, 'SERVICES', policy.SERVICES | policy.HARDWARE_SERVICES):
output, report = policy.filter_rc(source)
self.assertNotIn('vendor.audio-hal-2-0', output)
self.assertIn('vendor.camera-provider-2-4', report['services'])
def test_display_keeps_its_platform_properties_and_configstore(self):
output,report=policy.filter_rc('on post-fs-data\n load_system_props\nservice vendor.configstore-hal /vendor/bin/hw/android.hardware.configstore@1.1-service\n class hal animation\n user system\n')
self.assertIn('load_system_props',output)

View file

@ -5,6 +5,7 @@ from pathlib import Path
import ssl
import tempfile
import unittest
import xml.etree.ElementTree as ET
ROOT = Path(__file__).resolve().parents[1]
spec = importlib.util.spec_from_file_location('packages', ROOT/'runtime/runtime-packages.py')
@ -33,9 +34,20 @@ class RuntimePackages(unittest.TestCase):
self.assertEqual(packages.stage(root, CACHE), first)
self.assertEqual(json.loads((root/'usr/share/vulpes/runtime-packages.json').read_text()), first)
self.assertTrue((root/'usr/libexec/bluetooth/bluetoothd').is_file())
self.assertFalse((root/'usr/bin/btmon').exists())
self.assertTrue((root/'usr/bin/bluetoothctl').is_file())
self.assertTrue((root/'usr/lib/audiosystem-passthrough/audiosystem-passthrough').is_file())
context = ssl.create_default_context(cafile=str(root/'etc/ssl/certs/ca-certificates.crt'))
self.assertGreater(context.cert_store_stats()['x509_ca'], 50)
self.assertTrue((root/'usr/lib/ssl/cert.pem').is_file())
providers = ET.parse(root/'usr/share/mobile-broadband-provider-info/serviceproviders.xml')
self.assertGreater(len(providers.findall('country')), 100)
orange = [provider for provider in providers.findall("./country[@code='fr']/provider")
if any(n.attrib == {'mcc':'208', 'mnc':'01'}
for n in provider.findall('.//network-id'))]
self.assertTrue(orange)
self.assertTrue(any(apn.get('value') == 'orange' for provider in orange
for apn in provider.findall('.//apn')))
target = root/'usr/libexec/bluetooth/bluetoothd'
target.write_bytes(b'unknown local binary')
with self.assertRaisesRegex(ValueError, 'Conflicting package file'):

View file

@ -0,0 +1,65 @@
import importlib.util
import json
from pathlib import Path
import subprocess
import tempfile
from types import SimpleNamespace
import unittest
spec = importlib.util.spec_from_file_location('diagnostic', Path(__file__).resolve().parents[1]/'tools/diagnose-telephony.py')
diagnostic = importlib.util.module_from_spec(spec)
spec.loader.exec_module(diagnostic)
class DiagnosticTests(unittest.TestCase):
def test_qualcomm_attach_crash_is_visible_without_raw_backtrace(self):
report = diagnostic.modem_crashes(
'F libc: Fatal signal 7 (SIGBUS), fault addr 0x123456 in tid 101 (qcrild)\n'
'F DEBUG: qcril_qmi_nas_initial_attach_timeout_handler(void*)\n'
'private subscriber text\n')
self.assertEqual(report, {'signals':[{'signal':7, 'name':'SIGBUS'}],
'initialAttachTimeoutInBacktrace':True})
self.assertNotIn('0x123456', json.dumps(report))
self.assertNotIn('subscriber', json.dumps(report))
def test_crash_time_distinguishes_old_events_after_a_radio_restart(self):
report = diagnostic.modem_crashes(
'10-08 12:37:33.483 50 101 F libc: Fatal signal 7 (SIGBUS) in tid 101 (qcrild)\n')
self.assertEqual(report['signals'][0]['logTime'], '10-08 12:37:33.483')
def test_subscriber_and_cell_identifiers_never_leave_report(self):
props = {'Present': {'data': True}, 'SubscriberIdentity': {'data': 'private-imsi'},
'SubscriberNumbers': {'data': ['private-number']}, 'CardIdentifier': {'data': 'private-card'}}
self.assertEqual(diagnostic.select_properties('SimManager', props), {'Present': True})
line = 'src/binder_network.c:binder_network_poll_voice_state_cb() slot1 denied,lte,14,12345,987654321,11'
events = diagnostic.registration_events(line+'\n'+line)
self.assertEqual(len(events), 1)
self.assertEqual(events[0]['denialReason'], 11)
self.assertNotIn('12345', json.dumps(events))
self.assertNotIn('987654321', json.dumps(events))
def test_unavailable_services_are_not_reported_as_healthy(self):
def unavailable(command):
raise subprocess.TimeoutExpired(command, 12)
with tempfile.TemporaryDirectory() as folder:
report = diagnostic.collect(unavailable, Path(folder))
self.assertEqual(len(report['errors']), len(diagnostic.FIELDS))
self.assertFalse(report['registrationLogAvailable'])
self.assertEqual(report['audio'], {'available': False})
def test_diagnostic_only_reads_fixed_interfaces(self):
calls = []
def command(args):
calls.append(args)
if args[0] == 'busctl':
return SimpleNamespace(returncode=0, stdout='{"data":[{"Status":{"data":"denied"}}]}')
return SimpleNamespace(returncode=0, stdout='active\n' if args[0] == 'systemctl' else '')
with tempfile.TemporaryDirectory() as folder:
report = diagnostic.collect(command, Path(folder))
self.assertEqual(report['properties']['NetworkRegistration']['Status'], 'denied')
for args in calls:
if args[0] == 'busctl': self.assertEqual(args[-1], 'GetProperties')
if args[0] == 'systemctl': self.assertEqual(args[1], 'is-active')
if __name__ == '__main__': unittest.main()

View file

@ -28,6 +28,16 @@ class WifiTests(unittest.TestCase):
self.assertTrue(result[0]['connected'])
self.assertEqual(result[1]['security'], 'unsupported')
def test_connection_information_uses_validated_address_and_actual_bitrate(self):
def nm(*args, **kwargs):
value='invalid\n192.0.2.4/24\n' if args[1]=='IP4.ADDRESS' else '/org/freedesktop/NetworkManager/Devices/4\n'
return SimpleNamespace(returncode=0,stdout=value)
with patch.object(wifi,'nm',side_effect=nm), patch.object(wifi.subprocess,'run',return_value=SimpleNamespace(returncode=0,stdout='{"data":[351000]}')):
self.assertEqual(wifi.connection_info(),{'ipAddress':'192.0.2.4','linkSpeed':351})
with patch.object(wifi,'nm',return_value=SimpleNamespace(returncode=0,stdout='/invalid/path')), patch.object(wifi.subprocess,'run') as run:
self.assertEqual(wifi.connection_info(),{})
run.assert_not_called()
def test_secret_persists_only_after_success(self):
for fail in (False, True):
with tempfile.TemporaryDirectory() as temporary:
@ -86,6 +96,18 @@ class WifiTests(unittest.TestCase):
with self.assertRaises(wifi.WifiError):
wifi.profile_text(AP, password, 'test')
def test_hidden_network_uses_encoded_ssid_and_private_profile(self):
with tempfile.TemporaryDirectory() as folder, patch.object(wifi,'nm',return_value=SimpleNamespace(returncode=0)):
root=Path(folder)
wifi.dispatch({'operation':'connectHidden','ssid':'Hidden; réseau','security':'wpa-psk','password':'testpassword'},root)
saved=next(root.iterdir())
self.assertIn('hidden=true',saved.read_text())
self.assertEqual(stat.S_IMODE(saved.stat().st_mode),0o600)
self.assertEqual(wifi.saved_profiles(root)[0]['ssid'],'Hidden; réseau')
for ssid in ('','x'*33,'é'*17):
with tempfile.TemporaryDirectory() as folder, self.assertRaises(wifi.WifiError):
wifi.dispatch({'operation':'connectHidden','ssid':ssid,'security':'open'},Path(folder))
def test_socket_round_trip_and_limits(self):
a, b = socket.socketpair()
with a, b, patch.object(wifi, 'dispatch', return_value={'networks':[]}) as dispatch: