Release Vulpes OS 2.7-preview.22

This commit is contained in:
OverSu 2026-10-22 20:42:26 +02:00
commit a138f97877
294 changed files with 515786 additions and 187 deletions

View file

@ -0,0 +1,26 @@
/* SPDX-License-Identifier: MPL-2.0 */
#include <assert.h>
#define main keypad_reader_main
#include "../probes/beatles-keypad.c"
#undef main
int main(void) {
for(unsigned code=KEY_1;code<=KEY_0;code++)assert(symbol(code)!=NoSymbol);
const unsigned controls[]={KEY_UP,KEY_DOWN,KEY_LEFT,KEY_RIGHT,KEY_OK,
KEY_BACK,KEY_BACKSPACE,KEY_MENU,KEY_SEND,KEY_POWER,KEY_CAMERA,
KEY_NUMERIC_STAR,KEY_NUMERIC_POUND,KEY_VOLUMEUP,KEY_VOLUMEDOWN};
for(unsigned i=0;i<sizeof(controls)/sizeof(controls[0]);i++)
assert(symbol(controls[i])!=NoSymbol);
assert(native_code("Notifications")==KEY_CAMERA);
assert(symbol(KEY_CAMERA)==XK_F2);
/* Do not give the assistance key an accidental call/emergency action. */
assert(symbol(112)==NoSymbol);
assert(!allow_key(KEY_SEND,1,1));
assert(!allow_key(KEY_2,1,1));
assert(allow_key(KEY_SEND,0,1));
assert(allow_key(KEY_VOLUMEUP,1,1));
assert(allow_key(KEY_VOLUMEDOWN,1,1));
assert(allow_key(KEY_2,1,0));
puts("Nokia key map and closed-lid input policy: OK");
return 0;
}

View file

@ -0,0 +1,56 @@
/* SPDX-License-Identifier: MPL-2.0 */
#include <assert.h>
#include <fcntl.h>
#include <linux/fb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#define ROOT "/nonexistent-vulpes-lid-test"
static char keys_path[]="/tmp/vulpes-lid-keys-XXXXXX";
#define BEATLES_KEYS_BRIGHTNESS keys_path
static int last_panel=-1;
static int inner=-1,outer=-1,fail_inner=0;
static int panel_ioctl(int fd,unsigned long request,int value) {
assert(request==FBIOBLANK);
last_panel=fd;
if(fd==10){if(fail_inner)return -1;inner=value;}
else {assert(fd==11);outer=value;}
return 0;
}
#define ioctl panel_ioctl
#include "../probes/beatles-lid.h"
int main(void) {
assert(beatles_hall_open(0)==1);
assert(beatles_hall_open(1)==0);
assert(beatles_hall_open(-1)==-1);
assert(beatles_hall_open(2)==-1);
int fixture=mkstemp(keys_path);assert(fixture>=0);close(fixture);
lid_keys_write(40);assert(lid_keys_read()==40);
struct lid_state lid={.inner=10,.outer=11,.opened=-1,.keys_brightness=40};
lid_apply(&lid,1,1,100);assert(inner==FB_BLANK_UNBLANK && outer==FB_BLANK_POWERDOWN);
lid_apply(&lid,0,1,101);assert(inner==FB_BLANK_POWERDOWN && outer==FB_BLANK_UNBLANK);
assert(lid_keys_read()==0);
lid_apply(&lid,0,1,117);assert(outer==FB_BLANK_POWERDOWN);
lid_apply(&lid,-1,1,118);assert(inner==FB_BLANK_POWERDOWN);
lid_apply(&lid,1,1,119);assert(inner==FB_BLANK_UNBLANK && outer==FB_BLANK_POWERDOWN);
assert(last_panel==10 && lid_keys_read()==40);
fail_inner=1;lid_apply(&lid,0,1,120);assert(lid.opened==1 && !lid.outer_on);
fail_inner=0;lid_apply(&lid,0,0,121);assert(!lid.outer_on);
lid_apply(&lid,0,1,150);assert(lid.outer_on); /* late renderer still gets 15s */
lid_apply(&lid,0,1,166);assert(!lid.outer_on);
struct lid_state closed={.inner=10,.outer=11,.opened=-1,.keys_brightness=40};
lid_apply(&closed,0,0,200);
assert(inner==FB_BLANK_POWERDOWN && !closed.outer_on && lid_keys_read()==0);
/* Xorg unblanks fb0 while initializing, without a physical lid transition. */
inner=FB_BLANK_UNBLANK;
lid_apply(&closed,0,0,205);
lid_apply(&closed,0,1,220);
assert(inner==FB_BLANK_POWERDOWN && closed.outer_on && lid_keys_read()==0);
lid_apply(&closed,0,1,234);assert(closed.outer_on);
lid_apply(&closed,0,1,235);assert(!closed.outer_on);
lid.outer=-1;lid_apply(&lid,1,1,170);lid_apply(&lid,0,1,171);assert(!lid.outer_on);
lid_restore(&lid);assert(lid_keys_read()==40);unlink(keys_path);
puts("Clamshell panel transitions and keypad backlight: OK");return 0;
}

View file

@ -0,0 +1,44 @@
# SPDX-License-Identifier: MPL-2.0
import gzip
from pathlib import Path
import stat
import struct
import sys
import unittest
sys.path.insert(0,str(Path(__file__).parents[1]/'tools'))
from beatles_boot import read_cpio, write_cpio, add_file, pack_boot, unpack_boot
class BootTests(unittest.TestCase):
def archive(self):
entries={'.':{'fields':[1,stat.S_IFDIR|0o750,1000,1001,2,17,0,0,0,0,0,2,0],'data':b''}}
add_file(entries,'init.rc',b'on boot\n',0o640)
entries['init.rc']['fields'][2:4]=[1000,1001]
return entries
def test_repack_preserves_ownership_and_modes(self):
original=self.archive();repacked=read_cpio(write_cpio(original))
self.assertEqual(repacked['init.rc']['data'],b'on boot\n')
self.assertEqual(repacked['init.rc']['fields'][1:6],original['init.rc']['fields'][1:6])
self.assertEqual(write_cpio(original),write_cpio(original))
def test_missing_trailer_and_truncated_entry_rejected(self):
raw=gzip.decompress(write_cpio(self.archive()))
for end in (90,115,180):
with self.assertRaises(ValueError):read_cpio(gzip.compress(raw[:end]))
def test_traversal_rejected(self):
entries=self.archive();add_file(entries,'../escape',b'unsafe')
with self.assertRaises(ValueError):read_cpio(write_cpio(entries))
def test_duplicate_added_file_rejected(self):
with self.assertRaises(ValueError):add_file(self.archive(),'init.rc',b'bad')
def image(self):
header=bytearray(2048);header[:8]=b'ANDROID!'
struct.pack_into('<10I',header,8,3,0x80008000,4,0x81000000,0,0,0x80000100,2048,0,0)
return bytes(header)+b'abc'+b'\0'*2045+b'test'+b'\0'*2044+b'old signature'
def test_changed_image_drops_original_signature_and_preserves_kernel(self):
result=unpack_boot(pack_boot(self.image(),b'new ramdisk'))
self.assertEqual(result['kernel'],b'abc');self.assertEqual(result['ramdisk'],b'new ramdisk')
self.assertEqual(result['trailer'],b'')
def test_unsupported_header_and_short_payload_rejected(self):
with self.assertRaises(ValueError):unpack_boot(self.image()[:2500])
image=bytearray(self.image());struct.pack_into('<I',image,40,1)
with self.assertRaises(ValueError):unpack_boot(image)
if __name__=='__main__':unittest.main()

View file

@ -0,0 +1,38 @@
# SPDX-License-Identifier: MPL-2.0
from pathlib import Path
import socket
import subprocess
import tempfile
import unittest
SOURCE=Path(__file__).parents[1]/'devices/beatles/boot/vulpes-boot.sh'
class GuardTests(unittest.TestCase):
def run_guard(self,mode):
with tempfile.TemporaryDirectory() as directory:
root=Path(directory);base=root/'runtime';base.mkdir()
screen=root/'fb';screen.touch();ril=socket.socket(socket.AF_UNIX)
try:
ril.bind(str(root/'ril'))
start=root/'start';start.write_text('#!/bin/sh\necho "$1" >> '+str(root/'fallback')+'\n');start.chmod(0o755)
supervisor=root/'display'
action='exit 1' if mode=='launch-failed' else 'exit 0'
if mode=='ready':action='printf \'{"readyMs":100}\' > '+str(base/'rootfs/work/gaia-audit.json')
supervisor.write_text('#!/bin/sh\n'+action+'\n');supervisor.chmod(0o755)
if mode!='missing':
(base/'rootfs/opt/firefox').mkdir(parents=True);(base/'rootfs/work').mkdir()
engine=base/'rootfs/opt/firefox/firefox';engine.touch();engine.chmod(0o755)
(base/'runtime-ready').touch()
if mode=='disabled':(base/'disable-boot').touch()
script=SOURCE.read_text().replace('/data/local/vulpes-beatles',str(base)).replace('/system/bin/start',str(start)).replace('/sbin/vulpes-display',str(supervisor)).replace('/system/bin/busybox grep','grep').replace('/dev/graphics/fb0',str(screen)).replace('/dev/socket/rilproxy',str(root/'ril')).replace('sleep 1',':')
proc=subprocess.run(['sh','-c',script],capture_output=True,text=True,timeout=5)
self.assertEqual(proc.returncode,0,proc.stderr)
return (root/'fallback').exists(),(base/'stop-session').exists()
finally:ril.close()
def test_missing_runtime_falls_back(self):self.assertEqual(self.run_guard('missing'),(True,False))
def test_opt_out_falls_back(self):self.assertEqual(self.run_guard('disabled'),(True,False))
def test_failed_launch_falls_back(self):self.assertEqual(self.run_guard('launch-failed'),(True,False))
def test_stalled_gaia_requests_stop_and_falls_back(self):self.assertEqual(self.run_guard('stalled'),(True,True))
def test_ready_gaia_does_not_start_second_ui(self):self.assertEqual(self.run_guard('ready'),(False,False))
if __name__=='__main__':unittest.main()

View file

@ -0,0 +1,60 @@
# SPDX-License-Identifier: MPL-2.0
import concurrent.futures
from pathlib import Path
import socket
import subprocess
import tempfile
import unittest
class ControlRelayTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.directory=tempfile.TemporaryDirectory()
cls.root=Path(cls.directory.name)
code=(Path(__file__).parents[1]/'probes/beatles-control-client.c').read_text()
for name in ('phone-control','wifi-control'):
code=code.replace('/work/'+name+'.sock',str(cls.root/(name+'.sock')))
source=cls.root/'client.c';source.write_text(code);cls.binary=cls.root/'client'
subprocess.run(['cc','-Wall','-Wextra','-Werror',str(source),'-o',str(cls.binary)],check=True)
@classmethod
def tearDownClass(cls):cls.directory.cleanup()
def exchange(self,mode,reply):
path=self.root/(('phone' if mode=='--radio' else 'wifi')+'-control.sock')
path.unlink(missing_ok=True)
with socket.socket(socket.AF_UNIX,socket.SOCK_STREAM) as server:
server.bind(str(path));server.listen(1);server.settimeout(3)
def broker():
with server.accept()[0] as conn:
conn.settimeout(3);data=b''
while b'\n' not in data:data+=conn.recv(4096)
for part in reply:conn.sendall(part)
return data
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
future=pool.submit(broker)
result=subprocess.run([str(self.binary),mode],input=b'{"operation":"status"}',capture_output=True,timeout=4)
request=future.result()
return result,request
def test_radio_and_wifi_preserve_broker_envelopes(self):
for mode in ('--radio','--wifi'):
result,request=self.exchange(mode,[b'{"ok":true,',b'"result":[]}\n'])
self.assertEqual(result.returncode,0)
self.assertEqual(result.stdout,b'{"ok":true,"result":[]}\n')
self.assertEqual(request,b'{"operation":"status"}\n')
def test_disconnect_and_oversized_response_fail(self):
for reply in ([b'{"incomplete":'],[b'x'*65537]):
result,_=self.exchange('--wifi',reply)
self.assertNotEqual(result.returncode,0)
self.assertEqual(result.stdout,b'')
def test_unknown_target_and_multiple_requests_are_rejected(self):
for mode,data in [('--shell',b'{}'),('--radio',b'{}\n{}'),('--wifi',b'x'*8193)]:
result=subprocess.run([str(self.binary),mode],input=data,capture_output=True,timeout=3)
self.assertNotEqual(result.returncode,0)
self.assertEqual(result.stdout,b'')
if __name__=='__main__':unittest.main()

View file

@ -0,0 +1,63 @@
# SPDX-License-Identifier: MPL-2.0
import importlib.util
from pathlib import Path
import unittest
import tempfile
import struct
from unittest.mock import patch
spec=importlib.util.spec_from_file_location('cover',Path(__file__).parents[1]/'devices/beatles/cover-display.py')
cover=importlib.util.module_from_spec(spec);spec.loader.exec_module(cover)
class CoverTests(unittest.TestCase):
def test_animation_timing_padding_and_loop(self):
with tempfile.TemporaryDirectory() as directory:
path=Path(directory)/'animation'
first=b'\x00\xf8'*(240*240);second=b'\x1f\x00'*(240*240)
path.write_bytes(b'VULPANM1'+struct.pack('<I',2)+struct.pack('<I',50)+first+struct.pack('<I',100)+second)
animation=cover.Animation(path)
try:
self.assertEqual(animation.frame(0,480),(first,.05,0))
self.assertEqual(animation.frame(.05,480),(second,.1,1))
self.assertEqual(animation.frame(.15,480),(first,.05,0))
padded,_,_=animation.frame(0,484)
self.assertEqual(len(padded),240*484)
self.assertEqual(padded[480:484],bytes(4))
finally:animation.close()
def test_animation_rejects_invalid_and_truncated_files(self):
with tempfile.TemporaryDirectory() as directory:
path=Path(directory)/'animation'
for data in (b'',b'WRONGHDR'+struct.pack('<I',1),b'VULPANM1'+struct.pack('<I',241),
b'VULPANM1'+struct.pack('<II',1,1)+bytes(240*240*2)):
path.write_bytes(data)
with self.assertRaises(ValueError):cover.Animation(path)
def test_unknown_and_stale_are_not_full_battery_or_registered_network(self):
self.assertEqual(cover.labels({},100),('—','—'))
self.assertEqual(cover.labels({'recordedAt':1,'battery':{'level':1}},100),('—','—'))
def test_live_values_and_absent_second_sim(self):
state={'recordedAt':100,'battery':{'level':.73},'radio':[
{'available':False},{'available':True,'operator':{'longName':'Orange F'}}]}
self.assertEqual(cover.labels(state,105),('73 %','Orange F'))
def test_custom_title_and_colors(self):
title,ink,battery=cover.preferences({'vulpes.cover.title':' Mon Nokia ',
'vulpes.cover.titleColor':'#FF0000','vulpes.cover.batteryColor':'#00ff00'})
self.assertEqual(title,'Mon Nokia');self.assertEqual(ink,(1,0,0));self.assertEqual(battery,(0,1,0))
def test_invalid_preferences_and_long_title_are_safe(self):
title,ink,battery=cover.preferences({'vulpes.cover.title':['invalid'],
'vulpes.cover.titleColor':'url(file:///x)','vulpes.cover.batteryColor':None})
self.assertEqual(title,'VULPES');self.assertEqual(ink,battery)
self.assertEqual(cover.preferences({'vulpes.cover.title':'A'*30+'\n'})[0],'A'*24)
self.assertEqual(cover.preferences({'vulpes.cover.title':'\n'})[0],'VULPES')
def test_invalid_battery(self):
self.assertEqual(cover.labels({'recordedAt':100,'battery':{'level':2}},100),('—','—'))
def test_rejected_refresh_can_recover_without_stopping_gaia(self):
with patch.object(cover.fcntl,'ioctl',side_effect=[PermissionError(1,'panel asleep'),0]):
self.assertEqual(cover.present(3,bytearray(160)),{'ok':False,'errno':1})
self.assertEqual(cover.present(3,bytearray(160)),{'ok':True})
if __name__=='__main__':unittest.main()

View file

@ -0,0 +1,37 @@
# SPDX-License-Identifier: MPL-2.0
import sys,tempfile,unittest
from pathlib import Path
from unittest.mock import patch
sys.path.insert(0,str(Path(__file__).parents[1]/'devices/beatles'))
from hardware_control import HardwareControl
class HardwareTests(unittest.TestCase):
def setUp(self):
self.temp=tempfile.TemporaryDirectory();self.addCleanup(self.temp.cleanup)
self.root=Path(self.temp.name)
for name,value in [('backlight',170),('backlight-max',255),('vibrator',0)]:
(self.root/name).write_text(str(value))
self.hw=HardwareControl(self.root)
def value(self,name):return int((self.root/name).read_text())
def test_brightness_and_restore(self):
self.hw.request({'operation':'screen','enabled':True,'level':.2})
self.assertEqual(self.value('backlight'),51)
self.hw.request({'operation':'screen','enabled':False,'level':.2})
self.assertEqual(self.value('backlight'),0)
self.hw.restore();self.assertEqual(self.value('backlight'),170)
def test_vibration_sequence_cancel_and_shutdown(self):
with patch('hardware_control.time.monotonic',return_value=10) as clock:
self.hw.request({'operation':'vibrate','pattern':[120,80,200]})
self.assertEqual(self.value('vibrator'),120)
clock.return_value=10.13;self.hw.tick();self.assertEqual(self.value('vibrator'),0)
clock.return_value=10.22;self.hw.tick();self.assertEqual(self.value('vibrator'),200)
self.hw.request({'operation':'vibrate','pattern':[]})
self.assertEqual(self.value('vibrator'),0);self.assertFalse(self.hw.pattern)
self.hw.request({'operation':'vibrate','pattern':[200]});self.hw.restore()
self.assertEqual(self.value('vibrator'),0)
def test_invalid_requests_never_write(self):
for request in ({'operation':'vibrate','pattern':[10001]}, {'operation':'vibrate','pattern':[True]},
{'operation':'vibrate','pattern':[5000,5001]}, {'operation':'screen','enabled':True,'level':float('nan')},
{'operation':'screen','enabled':1,'level':.5},{'operation':'shell'},None):
with self.assertRaises(ValueError):self.hw.request(request)
self.assertEqual(self.value('backlight'),170);self.assertEqual(self.value('vibrator'),0)

View file

@ -0,0 +1,213 @@
# SPDX-License-Identifier: MPL-2.0
import importlib.util
import json
from pathlib import Path
import socket
import struct
import sys
import tempfile
import unittest
from unittest.mock import patch
DEVICE=Path(__file__).parents[1]/'devices/beatles'
sys.path.insert(0,str(DEVICE))
import radio_control as control
import sms_pdu as sms
from radio_status import Parcel
HELLO='00040A91214365870900006201812100008005E8329BFD06'
class PduTests(unittest.TestCase):
def test_standard_gsm_vector_and_unicode(self):
self.assertEqual(sms.pack7(sms.septets('hello')).hex(),'e8329bfd06')
self.assertEqual(sms.submit('+1234567890','hello'),'01000A912143658709000005E8329BFD06')
row=sms.deliver(HELLO)
self.assertEqual(row['sender'],'+1234567890');self.assertEqual(row['body'],'hello')
self.assertEqual(row['timestamp'],1792317600000)
self.assertEqual(sms.submit('123','漢').lower(),'0100038121f30008026f22')
def test_extensions_and_length_limits(self):
text='é € {} [] ^ ~ \\';values=sms.septets(text)
self.assertEqual(sms.unpack7(sms.pack7(values),len(values)),text)
sms.submit('123','A'*160)
for text in ('A'*161,'€'*81,'漢'*71):
with self.assertRaisesRegex(ValueError,'SMS_TOO_LONG'):sms.submit('123',text)
for value in ('112;reboot','*#06#','',None,'+123\n'):
with self.assertRaises(ValueError):sms.submit(value,'hello')
def test_truncated_and_unsupported_pdus(self):
for end in range(0,len(HELLO),2):
with self.assertRaises((ValueError,UnicodeError)):sms.deliver(HELLO[:end])
for pdu in ('00ff','xx','00040A9121436587097F006201812100008005E8329BFD06'):
with self.assertRaises(ValueError):sms.deliver(pdu)
def test_unicode_and_concatenation(self):
prefix='00440A912143658709000862018121000080'
row=sms.deliver(prefix+'080500030702010041')
self.assertEqual(row['body'],'A');self.assertEqual(row['concat'],(7,2,1))
with tempfile.TemporaryDirectory() as directory:
path=Path(directory)/'inbox.json';inbox=control.Inbox(path)
inbox.receive(prefix+'080500030702020042');self.assertEqual(inbox.value['inbox'],[])
inbox.receive(prefix+'080500030702010041')
self.assertEqual(inbox.value['inbox'][0]['body'],'AB')
self.assertEqual(control.Inbox(path).value['inbox'][0]['body'],'AB')
def test_durable_inbox_dedup_and_failed_save(self):
with tempfile.TemporaryDirectory() as directory:
path=Path(directory)/'inbox.json';inbox=control.Inbox(path)
with patch.object(inbox,'save',side_effect=OSError('disk full')):
with self.assertRaises(OSError):inbox.receive(HELLO)
self.assertEqual(inbox.value['seen'],{})
inbox.receive(HELLO);inbox.receive(HELLO)
self.assertEqual(len(inbox.value['inbox']),1)
self.assertEqual(path.stat().st_mode&0o777,0o600)
receipt=inbox.value['inbox'][0]['receipt'];inbox.acknowledge([receipt])
inbox.receive(HELLO);self.assertEqual(inbox.value['inbox'],[])
class ControlTests(unittest.TestCase):
def test_unanswered_request_is_not_sent_again(self):
radio=control.ControlRadio('/unused',0,None)
radio.pending={10:(9,0)}
broker=control.Broker([radio],None)
with patch.object(radio,'send') as send:
with self.assertRaisesRegex(RuntimeError,'RADIO_BUSY'):broker.exchange(9)
send.assert_not_called()
def test_call_list_timeout_is_short_and_request_stays_pending(self):
radio=control.ControlRadio('/unused',0,None);radio.sock=unittest.mock.Mock()
broker=control.Broker([radio],None)
with patch.object(control.time,'monotonic',side_effect=[0,0,1,2,3]),patch.object(broker,'step'):
with self.assertRaisesRegex(RuntimeError,'RADIO_TIMEOUT'):broker.exchange(9)
self.assertIsNone(radio.waiting)
self.assertEqual(len(radio.pending),1)
def test_call_layout_and_caller_privacy(self):
call=control.integer(1,4,2,145,0,1,0,1,0)+control.string('123456789')+control.integer(0)+control.string(None)+control.integer(0,0)
self.assertEqual(control.current_calls(Parcel(call)),[{'index':2,'state':'incoming','number':'+123456789','direction':'incoming'}])
hidden=control.integer(1,4,2,145,0,1,0,1,0)+control.string('123456789')+control.integer(1)+control.string(None)+control.integer(0,0)
self.assertEqual(control.current_calls(Parcel(hidden))[0]['number'],'')
self.assertEqual(control.current_calls(Parcel(b'')),[])
def test_late_modem_reply_does_not_complete_another_request(self):
radio=control.ControlRadio('/unused',0,None);radio.waiting=(9,20)
radio.response(9,0,Parcel(control.integer(0)),19);self.assertIsNone(radio.result)
radio.response(9,0,Parcel(control.integer(0)),20);self.assertEqual(radio.result,(True,[]))
def test_malformed_call_list_does_not_drop_radio_transport(self):
radio=control.ControlRadio('/unused',0,None);radio.waiting=(9,20)
radio.sock=unittest.mock.Mock()
radio.response(9,0,Parcel(control.integer(1)),20)
self.assertEqual(radio.result,(False,'RADIO_INVALID_CALL_LIST'))
radio.sock.close.assert_not_called()
def test_unknown_operations_do_not_reach_the_modem(self):
broker=control.Broker([],None)
with patch.object(broker,'exchange') as exchange:
for value in ({'version':1,'operation':'shell'},{'operation':'sms'},[],None):
with self.assertRaises(ValueError):broker.request(value)
exchange.assert_not_called()
def test_calls_answer_hangup_and_sms_parcels(self):
radio=control.ControlRadio('/unused',0,None);radio.state={'voice':{'connected':True}}
broker=control.Broker([radio],None)
row={'index':2,'state':'incoming','number':'123','direction':'incoming'}
with patch.object(broker,'exchange',return_value=[row]) as exchange:
calls=broker.request({'version':1,'operation':'calls'});path=calls[0]['path']
broker.request({'version':1,'operation':'hangup','path':path})
self.assertEqual(exchange.call_args.args,(12,control.integer(1,2)))
broker.request({'version':1,'operation':'answer','path':path})
self.assertEqual(exchange.call_args.args,(40,))
with self.assertRaisesRegex(ValueError,'INVALID_CALL'):
broker.request({'version':1,'operation':'hangup','path':'/stale/call/2'})
with patch.object(broker,'exchange',return_value={'state':'sent'}) as exchange:
self.assertEqual(broker.request({'version':1,'operation':'sms','number':'123','body':'hello'}),{'state':'sent'})
self.assertEqual(exchange.call_args.args,(25,control.integer(2)+control.string(None)+control.string(sms.submit('123','hello'))))
def test_second_call_is_rejected_before_dial(self):
radio=control.ControlRadio('/unused',0,None);radio.state={'voice':{'connected':True}}
broker=control.Broker([radio],None)
with patch.object(broker,'calls',return_value=[{'path':'/live/call/1'}]),patch.object(broker,'exchange') as exchange:
with self.assertRaisesRegex(RuntimeError,'RADIO_BUSY'):broker.request({'version':1,'operation':'dial','number':'123'})
exchange.assert_not_called()
def test_sms_ack_only_after_storage(self):
inbox=unittest.mock.Mock();radio=control.ControlRadio('/unused',0,inbox)
with patch.object(radio,'send') as send:
radio.unsolicited(1003,Parcel(control.string(HELLO)))
inbox.receive.assert_called_once_with(HELLO);send.assert_called_with(37,control.integer(2,1,0))
inbox.receive.side_effect=OSError('full')
radio.unsolicited(1003,Parcel(control.string(HELLO)))
send.assert_called_with(37,control.integer(2,0,255))
class RadioPowerTests(unittest.TestCase):
def broker(self):
radios=[control.ControlRadio('/unused',i,None) for i in range(2)]
for radio in radios:
radio.state={'radioState':'enabled'};radio.sock=unittest.mock.Mock()
return control.Broker(radios,None)
def test_slot_payload_readback_and_restore(self):
broker=self.broker()
def exchange(request,payload,slot=0):
self.assertEqual(request,23)
count,enabled=struct.unpack('<ii',payload);self.assertEqual(count,1)
broker.radios[slot].state['radioState']='enabled' if enabled else 'disabled'
with patch.object(broker,'exchange',side_effect=exchange) as send,patch.object(broker,'calls',return_value=[]):
self.assertEqual(broker.request({'version':1,'operation':'radioPower','slot':1,'enabled':False}),
{'slot':1,'radioState':'disabled'})
self.assertEqual(broker.radios[0].state['radioState'],'enabled')
send.assert_called_once_with(23,control.integer(1,0),slot=1)
broker.restore_power()
self.assertEqual(broker.radios[1].state['radioState'],'enabled')
def test_idempotent_power_does_not_send(self):
broker=self.broker()
with patch.object(broker,'exchange') as send:
broker.radio_power(0,True);send.assert_not_called()
def test_calls_invalid_values_and_unknown_state_block_power(self):
broker=self.broker()
with patch.object(broker,'exchange') as send,patch.object(broker,'calls',return_value=[{'state':'active'}]):
with self.assertRaisesRegex(RuntimeError,'RADIO_BUSY'):broker.radio_power(0,False)
for slot,enabled in [(False,False),(2,False),(0,0),(0,'false')]:
with self.assertRaisesRegex(ValueError,'INVALID_REQUEST'):broker.radio_power(slot,enabled)
broker.radios[0].state['radioState']='unknown'
with self.assertRaisesRegex(RuntimeError,'RADIO_NOT_READY'):broker.radio_power(0,False)
send.assert_not_called()
def test_acknowledgement_without_state_change_fails(self):
broker=self.broker()
with patch.object(broker,'exchange'),patch.object(broker,'calls',return_value=[]),patch.object(control.time,'monotonic',side_effect=[0,9]):
with self.assertRaisesRegex(RuntimeError,'RADIO_READBACK_FAILED'):broker.radio_power(0,False)
self.assertEqual(broker.original_power,{0:True})
class AudioTests(unittest.TestCase):
def test_call_mode_routing_and_restore(self):
from call_audio import CallAudio
audio=CallAudio();state={'mode':0,'route':0,'muted':0};commands=[]
def transaction(service,method,*values):
commands.append((service,method,values))
if (service,method)==('policy',6):return state['route']
if (service,method)==('flinger',18):return state['muted']
if (service,method)==('policy',3):state['mode']=values[0]
elif (service,method)==('policy',5):state['route']=values[1]
elif (service,method)==('flinger',17):state['muted']=values[0]
else:raise AssertionError('Unexpected audio transaction')
return 0
with patch.object(audio,'transact',side_effect=transaction):
audio.update([{'state':'incoming'}]);self.assertEqual(state['mode'],1)
audio.update([{'state':'active'}]);self.assertEqual(state['mode'],2)
self.assertTrue(audio.request({'operation':'set','muted':True,'speaker':True})['speaker'])
audio.restore();self.assertEqual(state,{'mode':0,'route':0,'muted':0})
def test_audio_failure_does_not_throw_from_call_tracking(self):
from call_audio import CallAudio
audio=CallAudio()
with patch.object(audio,'transact',side_effect=OSError('service missing')):
audio.update([{'state':'active'}]);self.assertFalse(audio.available)
with self.assertRaisesRegex(RuntimeError,'CALL_AUDIO_UNAVAILABLE'):audio.request({'operation':'status'})
class RilRequestSafetyTests(unittest.TestCase):
def test_call_list_uses_android_request_nine(self):
broker=control.Broker([],None)
with patch.object(broker,'exchange',return_value=[]) as exchange:
self.assertEqual(broker.calls(),[])
exchange.assert_called_once_with(9)
def test_pin_puk_and_identity_requests_never_reach_socket(self):
radio=control.ControlRadio('/unused',0,None)
radio.sock=unittest.mock.Mock()
for request in (2,3,4,5,6,7,8,11,28,38,39,43):
with self.assertRaisesRegex(ValueError,'RADIO_INVALID_REQUEST'):radio.send(request)
radio.sock.sendall.assert_not_called()
self.assertEqual(control.CONTROL_REQUESTS,{9,10,12,23,25,37,40})
if __name__=='__main__':unittest.main()

View file

@ -0,0 +1,65 @@
# SPDX-License-Identifier: MPL-2.0
import importlib.util
from pathlib import Path
import socket
import struct
import tempfile
import unittest
from unittest.mock import patch, Mock
spec=importlib.util.spec_from_file_location('beatles_radio',Path(__file__).resolve().parents[1]/'devices/beatles/radio_status.py')
radio=importlib.util.module_from_spec(spec);spec.loader.exec_module(radio)
def integers(*values):return struct.pack('<'+'i'*len(values),*values)
def string(value):
if value is None:return integers(-1)
data=integers(len(value))+value.encode('utf-16-le')+b'\0\0'
return data+b'\0'*((-len(data))%4)
class RadioTest(unittest.TestCase):
def test_registration_does_not_publish_cell_location(self):
payload=integers(4)+b''.join(map(string,['5','ffff','123456','14']))
result=radio.decode(20,radio.Parcel(payload))['voice']
self.assertEqual(result['type'],'lte');self.assertTrue(result['connected']);self.assertTrue(result['roaming'])
self.assertNotIn('123456',str(result));self.assertNotIn('ffff',str(result))
def test_lte_signal_and_registration_without_data_bearer(self):
values=[99,0,0,0,0,0,0,99,95,0,0,0,0]
self.assertEqual(radio.decode(19,radio.Parcel(integers(*values)))['signal'],47)
values[8]=2147483647
self.assertIsNone(radio.decode(19,radio.Parcel(integers(*values)))['signal'])
payload=integers(4)+b''.join(map(string,['1','','','14']))
state=radio.decode(21,radio.Parcel(payload))['data']
self.assertEqual(state['state'],'registered');self.assertFalse(state['connected'])
def test_sim_absent_and_locked(self):
self.assertEqual(radio.decode(1,radio.Parcel(integers(0,0,-1,-1,-1,0))),{'sim':{'present':False,'cardState':'absent'}})
payload=integers(1,0,0,-1,-1,1,2,2,0)+string('PRIVATE_AID')+string('SIM')+integers(0,1,0)
result=radio.decode(1,radio.Parcel(payload))
self.assertEqual(result,{'sim':{'present':True,'cardState':'pinRequired'}})
def test_bounds_and_unknown_signal(self):
self.assertIsNone(radio.decode(19,radio.Parcel(integers(99)))['signal'])
for data in [b'',integers(999999),integers(2)+b'a\0']:
with self.assertRaises(ValueError):radio.Parcel(data).string()
with self.assertRaises(ValueError):radio.decode(25,radio.Parcel(b''))
def test_partial_frame_and_disconnect_drop_state(self):
client,server=socket.socketpair()
r=radio.Radio('/unused',0);r.sock=client;r.pending={3:(1,0)}
body=integers(0,3,0,0,0,-1,-1,-1,0);frame=struct.pack('>I',len(body))+body
server.sendall(frame[:7]);r.receive();self.assertFalse(r.state['available'])
server.sendall(frame[7:]);r.receive();self.assertFalse(r.state['sim']['present'])
r.close();server.close();self.assertEqual(r.state,{'slot':0,'available':False,'radioState':'unknown'})
def test_handover_grace_and_bounded_reconnect_backoff(self):
r=radio.Radio('/unused',0);r.sock=Mock();r.serial=1;r.last_poll=100;r.pending={1:(9,90)}
with patch.object(radio.time,'monotonic',return_value=110):
r.poll();self.assertIsNotNone(r.sock)
with patch.object(radio.time,'monotonic',return_value=140):
r.poll();self.assertIsNone(r.sock);self.assertEqual(r.retry_at,145)
r.close();self.assertEqual(r.retry_at,150)
for _ in range(8):r.close()
self.assertEqual(r.retry_at,200)
def test_battery_unknown_is_not_empty(self):
with tempfile.TemporaryDirectory() as root:
p=Path(root);self.assertIsNone(radio.battery(p))
(p/'capacity').write_text('73');(p/'status').write_text('Discharging')
self.assertEqual(radio.battery(p),{'level':.73,'charging':False})
if __name__=='__main__':unittest.main()

View file

@ -0,0 +1,20 @@
import importlib.util
from pathlib import Path
import unittest
spec=importlib.util.spec_from_file_location('sync',Path(__file__).resolve().parents[1]/'tools/sync-beatles-gaia.py')
sync=importlib.util.module_from_spec(spec);spec.loader.exec_module(sync)
class SharedGaiaSync(unittest.TestCase):
def test_incremental_changes_and_removal(self):
self.assertEqual(sync.changes({'host/shell.js':'new','assets/a':'same','overrides/new.js':'new'},
{'host/shell.js':'old','assets/a':'same','overrides/old.js':'old'}),
(['host/shell.js','overrides/new.js'],['overrides/old.js']))
def test_profiles_and_escape_cannot_be_deleted(self):
for path in ('profiles/user.json','../profile','host/../../profile','/data/profile','hostile/file'):
with self.subTest(path=path),self.assertRaises(ValueError):sync.changes({}, {path:'digest'})
def test_first_sync_and_no_change(self):
files={'host/shell.js':'digest','release.json':'digest'}
self.assertEqual(sync.changes(files,{}),(sorted(files),[]))
self.assertEqual(sync.changes(files,files),([],[]))
if __name__=='__main__':unittest.main()

View file

@ -0,0 +1,26 @@
import importlib.util
from pathlib import Path
import unittest
ROOT = Path(__file__).resolve().parents[2]
spec = importlib.util.spec_from_file_location(
'package_beatles', ROOT / 'tundra/tools/package-beatles-build.py')
package = importlib.util.module_from_spec(spec)
spec.loader.exec_module(package)
class BeatlesT9Package(unittest.TestCase):
def test_all_offline_lexicons_are_included_at_runtime_path(self):
files = {}
package.add_beatles_t9(files)
self.assertEqual(len(files), 42)
self.assertEqual(
set(files),
{f'opt/vulpes/tundra/devices/beatles/t9/{locale}.tsv'
for locale in (path.stem for path in
(ROOT / 'tundra/devices/beatles/t9').glob('*.tsv'))})
self.assertTrue(all(path.is_file() for path in files.values()))
if __name__ == '__main__':
unittest.main()

View file

@ -0,0 +1,41 @@
# SPDX-License-Identifier: MPL-2.0
import sys
from pathlib import Path
import unittest
from unittest.mock import Mock,patch
sys.path.insert(0,str(Path(__file__).parents[1]/'devices/beatles'))
from stream_volume import StreamVolume
class VolumeTests(unittest.TestCase):
def test_route_readback_and_restoration(self):
volume=StreamVolume();state={(0,1):3,(0,2):5}
def transaction(method,*args):
if method==24:return [3]
stream,device=args[0],args[-1]
if method==17:return [state[stream,device],0]
if method==16:state[stream,device]=args[1];return [0]
raise AssertionError(method)
volume.transact=transaction
self.assertEqual(volume.request({'stream':'telephony','level':.4})['index'],2)
self.assertEqual(state,{(0,1):2,(0,2):2})
volume.request({'stream':'telephony','level':.8})
volume.restore();self.assertEqual(state,{(0,1):3,(0,2):5})
def test_invalid_request_never_touches_hardware(self):
volume=StreamVolume();volume.transact=Mock()
for value in (None,{}, {'stream':'shell','level':.5}, *({'stream':'content','level':n} for n in (True,-1,2,'1',float('nan'),float('inf')))):
with self.assertRaises(ValueError):volume.request(value)
volume.transact.assert_not_called()
def test_readback_failure_is_reported(self):
volume=StreamVolume();volume.transact=Mock(side_effect=[[2],[3,0],[0],[3,0]])
with self.assertRaisesRegex(RuntimeError,'CALL_AUDIO_UNAVAILABLE'):
volume.request({'stream':'content','level':.5})
def test_parcel_formats(self):
volume=StreamVolume()
for text,expected in (("Result: Parcel(00000008 00000000 '........')\n",[8,0]),
("Result: Parcel(\n 0x00000000: 00000008 00000000 '........'\n)\n",[8,0])):
with patch('stream_volume.subprocess.run',return_value=Mock(returncode=0,stdout=text)):
self.assertEqual(volume.transact(17,3,2),expected)

View file

@ -0,0 +1,102 @@
# SPDX-License-Identifier: MPL-2.0
import importlib.util
from pathlib import Path
import tempfile
import unittest
from unittest.mock import patch
spec=importlib.util.spec_from_file_location('beatles_wifi',Path(__file__).parents[1]/'devices/beatles/wifi_control.py')
wifi=importlib.util.module_from_spec(spec);spec.loader.exec_module(wifi)
class WifiTests(unittest.TestCase):
def fixture(self):
obj=wifi.Wifi.__new__(wifi.Wifi)
obj.last_scan=[{'id':'aa:bb:cc:dd:ee:ff','ssid':'Test "é"','security':'wpa-psk'}]
obj.enabled=lambda:True
obj.known=lambda:[]
obj.stopping=False
obj.lease=lambda state:None
return obj
def test_scan_decodes_ssids_and_does_not_offer_enterprise_as_psk(self):
data='bssid / frequency / signal level / flags / ssid\n'
data+='aa:bb:cc:dd:ee:ff\t2412\t-45\t[WPA2-PSK-CCMP][ESS]\tCaf\\xc3\\xa9\\\\Test\n'
data+='00:11:22:33:44:55\t5200\t-80\t[WPA2-EAP-CCMP]\tOffice\n'
rows=wifi.parse_scan(data)
self.assertEqual(rows[0]['ssid'],'Café\\Test')
self.assertEqual(rows[0]['signal'],100)
self.assertEqual(rows[1]['security'],'unsupported')
self.assertEqual(wifi.parse_scan('header\nnot-a-bssid\t2412\t-20\t[ESS]\tfake'),[])
def test_invalid_password_cannot_create_or_mutate_network(self):
with patch.object(wifi,'command') as command:
with self.assertRaisesRegex(ValueError,'WIFI_INVALID_PASSWORD'):
self.fixture().connect({'operation':'connect','id':'aa:bb:cc:dd:ee:ff','password':'short'})
command.assert_not_called()
def test_forget_rejects_command_injection(self):
with patch.object(wifi,'command') as command:
with self.assertRaisesRegex(ValueError,'WIFI_INVALID_REQUEST'):
self.fixture().request({'operation':'forget','id':'0\nREMOVE_NETWORK all'})
command.assert_not_called()
def test_connect_encodes_ssid_and_passphrase_before_supplicant(self):
obj=self.fixture();obj.state=lambda:{'state':'connected','network':{'ssid':'Test "é"'}}
calls=[]
def command(text,required=True):
calls.append(text)
return '7' if text=='ADD_NETWORK' else 'OK'
with patch.object(wifi,'command',command),patch.object(wifi,'android',return_value='ok'):
obj.connect({'operation':'connect','id':'aa:bb:cc:dd:ee:ff','password':'test-only-password'})
self.assertIn('SET_NETWORK 7 ssid '+'Test "é"'.encode().hex(),calls)
psk=next(c for c in calls if c.startswith('SET_NETWORK 7 psk ')).split()[-1]
self.assertEqual(len(psk),64)
self.assertNotIn('test-only-password',' '.join(calls))
self.assertIn('SAVE_CONFIG',calls)
def test_saved_network_reuses_credentials_without_reading_password(self):
obj=self.fixture();obj.known=lambda:[{'id':'3','ssid':'Test "é"','security':'wpa-psk'}]
obj.state=lambda:{'state':'connected','network':{'ssid':'Test "é"'}}
with patch.object(wifi,'command',return_value='OK') as command,patch.object(wifi,'android',return_value='ok'):
obj.connect({'operation':'connect','id':'aa:bb:cc:dd:ee:ff'})
self.assertEqual([call.args[0] for call in command.call_args_list],['SELECT_NETWORK 3'])
def test_save_failure_rolls_back_new_profile(self):
obj=self.fixture();obj.state=lambda:{'state':'connected','network':{'ssid':'Test "é"'}}
calls=[]
def command(text,required=True):
calls.append(text)
return '7' if text=='ADD_NETWORK' else 'FAIL' if text=='SAVE_CONFIG' else 'OK'
with patch.object(wifi,'command',command),patch.object(wifi,'android',return_value='ok'):
with self.assertRaisesRegex(RuntimeError,'WIFI_SAVE_FAILED'):
obj.connect({'operation':'connect','id':'aa:bb:cc:dd:ee:ff','password':'test-only-password'})
self.assertIn('REMOVE_NETWORK 7',calls)
self.assertIn('ENABLE_NETWORK all',calls)
class LeaseTests(unittest.TestCase):
def test_dhcp_service_gets_dynamic_interface_argument(self):
obj=wifi.Wifi.__new__(wifi.Wifi)
obj.last_lease=0;obj.lease_network=None;obj.route=None
state={'state':'connected','network':{'ssid':'Test','savedId':'0'}}
with patch.object(wifi,'android',return_value='stopped') as android,patch.object(wifi.time,'monotonic',return_value=100):
obj.lease(state)
commands=[call.args for call in android.call_args_list]
self.assertIn(('setprop','ctl.start','dhcpcd_wlan0:wlan0'),commands)
self.assertNotIn(('setprop','ctl.restart','dhcpcd_wlan0'),commands)
def test_routes_are_restored_when_link_down_removed_cached_routes(self):
obj=wifi.Wifi.__new__(wifi.Wifi)
obj.last_lease=0;obj.lease_network=('Test','0');obj.rule_owned=True
obj.route=('192.0.2.0/24','192.0.2.1')
properties={'dhcp.wlan0.result':'ok','dhcp.wlan0.ipaddress':'192.0.2.3',
'dhcp.wlan0.gateway':'192.0.2.1','dhcp.wlan0.mask':'255.255.255.0'}
def native(*args,**kwargs):
return properties.get(args[1],'') if args[0]=='getprop' else ''
with patch.object(wifi,'android',side_effect=native) as android,patch.object(wifi.time,'monotonic',return_value=100):
obj.lease({'state':'connected','network':{'ssid':'Test','savedId':'0'}})
commands=[call.args for call in android.call_args_list]
self.assertIn(('ip','route','add','default','via','192.0.2.1','dev','wlan0','table','21052'),commands)
self.assertEqual([c for c in commands if c[:3]==('ip','route','flush')],
[('ip','route','flush','table','21052')])
if __name__=='__main__':unittest.main()

View file

@ -22,7 +22,9 @@ class Buttons(unittest.TestCase):
def test_vibration_pattern_progresses_without_blocking_and_can_be_cancelled(self):
with tempfile.TemporaryDirectory() as d,patch.object(b,'VIBRATOR',Path(d)),patch.object(b.time,'monotonic',return_value=10):
(Path(d)/'trigger').write_text('[transient]')
v=b.Vibration();v.start([200,100,300])
self.assertEqual((Path(d)/'trigger').read_text(),'none')
self.assertEqual((Path(d)/'brightness').read_text(),'255')
self.assertEqual(v.deadline,10.2)
v.advance(10.1);self.assertEqual((Path(d)/'brightness').read_text(),'255')
@ -32,3 +34,18 @@ class Buttons(unittest.TestCase):
v.start([200]);v.start([]);self.assertIsNone(v.deadline);self.assertEqual((Path(d)/'brightness').read_text(),'0')
for pattern in [None,[-1],[True],[10001],[5000,5001],[1]*33]:
with self.assertRaises(ValueError):v.start(pattern)
def test_wake_recovery_waits_for_gaia_and_recovers_if_unresponsive(self):
with tempfile.TemporaryDirectory() as d,patch.object(b,'BACKLIGHT',Path(d)):
(Path(d)/'max_brightness').write_text('1000')
(Path(d)/'brightness').write_text('0')
recovery=b.WakeRecovery()
with patch.object(b,'wake_recovery',recovery):
recovery.arm(10);recovery.advance(11)
self.assertEqual((Path(d)/'brightness').read_text(),'0')
b.control({'operation':'screen','enabled':True,'level':.3})
recovery.advance(12)
self.assertEqual((Path(d)/'brightness').read_text(),'300')
b.control({'operation':'screen','enabled':False,'level':.3})
recovery.arm(20);recovery.advance(21.6)
self.assertEqual((Path(d)/'brightness').read_text(),'500')

View file

@ -0,0 +1,31 @@
import importlib.util
from pathlib import Path
import json
import tempfile
import unittest
from unittest.mock import patch
spec=importlib.util.spec_from_file_location('clock',Path(__file__).parents[1]/'runtime/clock-state.py')
m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m)
class ClockState(unittest.TestCase):
def test_preserves_time_plus_powered_off_elapsed_without_setting_rtc(self):
with tempfile.TemporaryDirectory() as d, patch.object(m,'STATE',Path(d)/'time.json'), patch.object(m,'RTC',Path(d)/'rtc'):
m.RTC.write_text('1000')
with patch.object(m.time,'time',return_value=1792340000): m.save()
m.RTC.write_text('1600')
with patch.object(m.time,'time',return_value=1792000000), patch.object(m.time,'clock_settime') as write:
self.assertTrue(m.restore());write.assert_called_once_with(m.time.CLOCK_REALTIME,1792340600)
self.assertEqual(m.RTC.read_text(),'1600')
self.assertEqual(m.STATE.stat().st_mode & 0o777,0o600)
def test_never_rewinds_valid_wall_time(self):
with tempfile.TemporaryDirectory() as d,patch.object(m,'STATE',Path(d)/'time.json'),patch.object(m,'rtc_seconds',return_value=None):
m.STATE.write_text(json.dumps({'epoch':1792340000,'rtc':1000}))
with patch.object(m.time,'time',return_value=1792350000),patch.object(m.time,'clock_settime') as write:
self.assertFalse(m.restore());write.assert_not_called()
def test_corrupt_state_or_reset_rtc_does_not_invent_elapsed_time(self):
with tempfile.TemporaryDirectory() as d,patch.object(m,'STATE',Path(d)/'time.json'),patch.object(m,'rtc_seconds',return_value=10),patch.object(m.time,'time',return_value=100):
m.STATE.write_text('invalid')
self.assertFalse(m.restore())
m.STATE.write_text(json.dumps({'epoch':1792340000,'rtc':1000}))
with patch.object(m.time,'clock_settime') as write:
self.assertTrue(m.restore());write.assert_called_once_with(m.time.CLOCK_REALTIME,1792340000)

View file

@ -5,6 +5,8 @@ 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 setUp(self):
self.checkpoint=patch.object(m,'checkpoint_clock');self.checkpoint.start();self.addCleanup(self.checkpoint.stop)
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):

View file

@ -129,6 +129,18 @@ on boot
'mount ext4 /dev/mmcblk0p72 /data','mount tmpfs /dev/block/x /data'):
self.assertFalse(policy.command_allowed(line.split()),line)
def test_network_config_on_empty_overlay_creates_firewall_aliases(self):
import tempfile
spec = importlib.util.spec_from_file_location('native_prepare', ROOT/'runtime/prepare-native.py')
native = importlib.util.module_from_spec(spec); spec.loader.exec_module(native)
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
(root/'opt/vulpes/trial').mkdir(parents=True)
(root/'opt/vulpes/trial/session.py').touch()
native.prepare(root, network=True, hardware=True)
for name in ('iptables', 'ip6tables'):
self.assertEqual((root/'usr/sbin'/name).readlink(), Path(name+'-legacy'))
def test_interactive_boot_checks_are_opt_in(self):
import tempfile
spec = importlib.util.spec_from_file_location('native_prepare', ROOT/'runtime/prepare-native.py')

View file

@ -0,0 +1,43 @@
# SPDX-License-Identifier: MPL-2.0
import io
from pathlib import Path
import sys
import tarfile
import tempfile
import unittest
from unittest.mock import patch
sys.path.insert(0, str(Path(__file__).resolve().parents[1]/'tools'))
import shared_ui
class SharedUiTests(unittest.TestCase):
def test_working_documents_stay_private_and_license_text_is_retained(self):
with tempfile.TemporaryDirectory() as folder:
root = Path(folder)
files = []
for name, text in [('host/app.js', 'app code'),
('overrides/README.md', 'private notes'),
('overrides/LICENSE.md', 'upstream license')]:
path = root/name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text)
files.append(path)
with patch.object(shared_ui, 'ROOT', root/'tundra'), \
patch.object(shared_ui, 'selected_files', return_value=files):
payload, hashes = shared_ui.build_payload()
with tarfile.open(fileobj=io.BytesIO(payload)) as archive:
self.assertEqual(set(archive.getnames()),
{'opt/vulpes/host/app.js', 'opt/vulpes/overrides/LICENSE'})
self.assertEqual(archive.extractfile('opt/vulpes/overrides/LICENSE').read(),
b'upstream license')
self.assertNotIn('overrides/README.md', hashes)
def test_call_log_fix_is_in_incremental_phone_updates(self):
names = {str(p.relative_to(shared_ui.ROOT.parent)) for p in shared_ui.selected_files()}
for name in ('dialer/js/call_log.js', 'dialer/gaia_build_defer_index.js'):
self.assertIn('assets/webapps/communications.gaiamobile.org/'+name, names)
if __name__ == '__main__':
unittest.main()

View file

@ -1,21 +1,40 @@
# SPDX-License-Identifier: MPL-2.0
import importlib.util
import io
import json
from pathlib import Path
import subprocess
import sys
import tempfile
import tarfile
import unittest
from unittest.mock import patch
TOOLS=Path(__file__).resolve().parents[1]/'tools'
sys.path.insert(0,str(TOOLS))
from standalone_volume import checked_volume
from shared_ui import differential_payload
spec=importlib.util.spec_from_file_location('standalone_boot',TOOLS/'build-standalone-boot.py')
boot=importlib.util.module_from_spec(spec);spec.loader.exec_module(boot)
class StandaloneBootTests(unittest.TestCase):
def test_shared_payload_keeps_changed_missing_and_different_mode_files(self):
with tempfile.TemporaryDirectory() as directory:
root=Path(directory)
for name in ('same','changed','mode'):
(root/name).write_bytes(b'old');(root/name).chmod(0o644)
source=io.BytesIO()
with tarfile.open(fileobj=source,mode='w') as archive:
for name,data,mode in [('same',b'old',0o644),('changed',b'new',0o644),('mode',b'old',0o755),('missing',b'new',0o644)]:
item=tarfile.TarInfo(name);item.size=len(data);item.mode=mode
archive.addfile(item,io.BytesIO(data))
result,count=differential_payload(source.getvalue(),root)
self.assertEqual(count,1)
with tarfile.open(fileobj=io.BytesIO(result)) as archive:
self.assertEqual(archive.getnames(),['changed','mode','missing'])
self.assertEqual(archive.extractfile('changed').read(),b'new')
def setUp(self):
self.volume={'schema':1,'device':'sargo','storage':'raw-userdata-ext4','image':'userdata.ext4',
'uuid':'12345678-1234-1234-1234-123456789abc','bytes':8*1024**3,

View file

@ -15,6 +15,17 @@ spec.loader.exec_module(image)
class SystemImageTests(unittest.TestCase):
def test_editable_gaia_overrides_are_in_the_system_image(self):
with tempfile.TemporaryDirectory() as temporary:
product = Path(temporary)
image.copy_source_overrides(product)
project = image.module('test_project', image.PROJECT/'tools/project.py')
for app, names in project.SOURCE_OVERRIDES.items():
for name in names:
relative = Path('gaia/apps')/app/name
self.assertEqual((product/relative).read_bytes(),
(image.PROJECT/relative).read_bytes())
def test_hardware_reference_requires_services_not_just_display(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)