This commit is contained in:
Alexander Sosedkin 2025-10-20 17:14:28 +05:30 committed by GitHub
commit c099b954b7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 129 additions and 54 deletions

View file

@ -5,7 +5,7 @@ on:
schedule: schedule:
- cron: 0 0 * * 1 - cron: 0 0 * * 1
jobs: jobs:
lint: lint-nix:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -17,3 +17,19 @@ jobs:
- name: Run nix-formatter-pack-check - name: Run nix-formatter-pack-check
run: nix build .#checks.x86_64-linux.nix-formatter-pack-check run: nix build .#checks.x86_64-linux.nix-formatter-pack-check
lint-py:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install nix
uses: cachix/install-nix-action@v25
- name: Run ruff linter
run: nix run 'nixpkgs#ruff' -- check
- name: Run ruff formatter
run: nix run 'nixpkgs#ruff' -- format --diff

32
.ruff.toml Normal file
View file

@ -0,0 +1,32 @@
line-length = 79
preview = true
lint.select = [ "ALL" ]
lint.ignore = [
"D203", # one-blank-line-before-class
"D213", # multi-line-summary-second-line
]
lint.per-file-ignores."tests/emulator/**" = [
"ANN", # flake-8 annotations
"D100", # undocumented-public-module
"D103", # undocumented-public-function
"INP001", # implicit-namespace-package
"PLR0915", # too-many-statements
"S101", # assert
"T201", # print
]
lint.per-file-ignores."tests/emulator/common.py" = [
"FBT002", # boolean-default-value-positional-argument
]
lint.per-file-ignores."tests/emulator/android_integration.py" = [
"C901", # complex-structure
]
lint.per-file-ignores."tests/emulator/test_channels_shell.py" = [
"S404", # suspicious-subprocess-import
"S603", # subprocess-without-shell-equals-true
"S607", # start-process-with-partial-path
]
lint.flake8-quotes.inline-quotes = "single"
lint.flake8-quotes.multiline-quotes = "single"
lint.flake8-copyright.notice-rgx = '# Copyright \(c\) 2019-20\d\d, see AUTHORS\. Licensed under MIT License, see LICENSE\n'
format.quote-style = "single"
format.preview = true

View file

@ -1,16 +1,23 @@
# Copyright (c) 2019-2024, see AUTHORS. Licensed under MIT License, see LICENSE
import base64 import base64
import time import time
import bootstrap_channels import bootstrap_channels
from common import screenshot, wait_for from common import screenshot, wait_for
OPENERS = ['termux-open', 'termux-open-url', 'xdg-open']
TOOLS = [
'am',
'termux-setup-storage',
'termux-reload-settings',
'termux-wake-lock',
'termux-wake-unlock',
*OPENERS,
]
def run(d): def run(d):
OPENERS = ['termux-open', 'termux-open-url', 'xdg-open']
TOOLS = ['am', 'termux-setup-storage', 'termux-reload-settings',
'termux-wake-lock', 'termux-wake-unlock'] + OPENERS
nod = bootstrap_channels.run(d) nod = bootstrap_channels.run(d)
# Verify that android-integration tools aren't installed by default # Verify that android-integration tools aren't installed by default
@ -21,9 +28,11 @@ def run(d):
screenshot(d, f'no-{toolname}') screenshot(d, f'no-{toolname}')
# Apply a config that enables android-integration tools # Apply a config that enables android-integration tools
cfg = ('/data/local/tmp/n-o-d/unpacked/tests/on-device/' cfg = (
'config-android-integration.nix') '/data/local/tmp/n-o-d/unpacked/tests/on-device/'
d(f'input text \'cp {cfg} .config/nixpkgs/nix-on-droid.nix\'') 'config-android-integration.nix'
)
d(f"input text 'cp {cfg} .config/nixpkgs/nix-on-droid.nix'")
d.ui.press('enter') d.ui.press('enter')
screenshot(d, 'pre-switch') screenshot(d, 'pre-switch')
d('input text "nix-on-droid switch && echo integration tools installed"') d('input text "nix-on-droid switch && echo integration tools installed"')
@ -135,7 +144,7 @@ def run(d):
d.ui(text='ALLOW').click() d.ui(text='ALLOW').click()
screenshot(d, 'wake-lock-permission-granted') screenshot(d, 'wake-lock-permission-granted')
d.ui.open_notification() d.ui.open_notification()
time.sleep(.5) time.sleep(0.5)
screenshot(d, 'notification-opened') screenshot(d, 'notification-opened')
wait_for(d, '(wake lock held)') wait_for(d, '(wake lock held)')
if 'Release wakelock' not in d.ui.dump_hierarchy(): if 'Release wakelock' not in d.ui.dump_hierarchy():
@ -152,7 +161,7 @@ def run(d):
d.ui.press('enter') d.ui.press('enter')
screenshot(d, 'wake-unlock-command') screenshot(d, 'wake-unlock-command')
d.ui.open_notification() d.ui.open_notification()
time.sleep(.5) time.sleep(0.5)
screenshot(d, 'notification-opened') screenshot(d, 'notification-opened')
if 'Acquire wakelock' not in d.ui.dump_hierarchy(): if 'Acquire wakelock' not in d.ui.dump_hierarchy():
d.ui(text='Nix').right(resourceId='android:id/expand_button').click() d.ui(text='Nix').right(resourceId='android:id/expand_button').click()

View file

@ -1,29 +1,31 @@
from common import screenshot, wait_for, APK, BOOTSTRAP_URL # Copyright (c) 2019-2024, see AUTHORS. Licensed under MIT License, see LICENSE
import time import time
from common import APK, BOOTSTRAP_URL, screenshot, wait_for
def run(d): def run(d):
nod = d.app('com.termux.nix', url=APK) nod = d.app('com.termux.nix', url=APK)
nod.permissions.allow_notifications() nod.permissions.allow_notifications()
nod.launch() nod.launch()
time.sleep(.5) time.sleep(0.5)
wait_for(d, 'Bootstrap zipball location') wait_for(d, 'Bootstrap zipball location')
time.sleep(.5) time.sleep(0.5)
screenshot(d, 'initial') screenshot(d, 'initial')
d.ui(className='android.widget.EditText').set_text(BOOTSTRAP_URL) d.ui(className='android.widget.EditText').set_text(BOOTSTRAP_URL)
time.sleep(.5) time.sleep(0.5)
screenshot(d, 'entered-url') screenshot(d, 'entered-url')
for i in range(2): for _ in range(2):
if 'text="OK"' not in d.ui.dump_hierarchy(): if 'text="OK"' not in d.ui.dump_hierarchy():
d.ui.press('back') d.ui.press('back')
time.sleep(.5) time.sleep(0.5)
else: else:
break break
time.sleep(.5) time.sleep(0.5)
screenshot(d, 'entered-url-back') screenshot(d, 'entered-url-back')
time.sleep(.5) time.sleep(0.5)
d.ui(text='OK').click() d.ui(text='OK').click()
screenshot(d, 'ok-clicked') screenshot(d, 'ok-clicked')

View file

@ -1,29 +1,31 @@
from common import screenshot, wait_for, APK, BOOTSTRAP_URL # Copyright (c) 2019-2024, see AUTHORS. Licensed under MIT License, see LICENSE
import time import time
from common import APK, BOOTSTRAP_URL, screenshot, wait_for
def run(d): def run(d):
nod = d.app('com.termux.nix', url=APK) nod = d.app('com.termux.nix', url=APK)
nod.permissions.allow_notifications() nod.permissions.allow_notifications()
nod.launch() nod.launch()
time.sleep(.5) time.sleep(0.5)
wait_for(d, 'Bootstrap zipball location') wait_for(d, 'Bootstrap zipball location')
time.sleep(.5) time.sleep(0.5)
screenshot(d, 'initial') screenshot(d, 'initial')
d.ui(className='android.widget.EditText').set_text(BOOTSTRAP_URL) d.ui(className='android.widget.EditText').set_text(BOOTSTRAP_URL)
time.sleep(.5) time.sleep(0.5)
screenshot(d, 'entered-url') screenshot(d, 'entered-url')
for i in range(2): for _ in range(2):
if 'text="OK"' not in d.ui.dump_hierarchy(): if 'text="OK"' not in d.ui.dump_hierarchy():
d.ui.press('back') d.ui.press('back')
time.sleep(.5) time.sleep(0.5)
else: else:
break break
time.sleep(.5) time.sleep(0.5)
screenshot(d, 'entered-url-back') screenshot(d, 'entered-url-back')
time.sleep(.5) time.sleep(0.5)
d.ui(text='OK').click() d.ui(text='OK').click()
screenshot(d, 'ok-clicked') screenshot(d, 'ok-clicked')

View file

@ -1,4 +1,6 @@
import os # Copyright (c) 2019-2024, see AUTHORS. Licensed under MIT License, see LICENSE
import pathlib
import sys import sys
import time import time
@ -9,11 +11,11 @@ BOOTSTRAP_URL = 'file:///data/local/tmp/n-o-d'
def screenshot(d, suffix=''): def screenshot(d, suffix=''):
os.makedirs('screenshots', exist_ok=True) screenshots = pathlib.Path('screenshots')
fname_base = f'screenshots/{time.time():.3f}-{suffix}' screenshots.mkdir(exist_ok=True)
d.ui.screenshot(f'{fname_base}.png') fname_base = screenshots / f'{time.time():.3f}-{suffix}'
with open(f'{fname_base}.xml', 'w') as f: d.ui.screenshot(str(fname_base.with_suffix('.png')))
f.write(d.ui.dump_hierarchy()) fname_base.with_suffix('.xml').write_text(d.ui.dump_hierarchy())
print(f'screenshotted: {fname_base}.{{png,xml}}') print(f'screenshotted: {fname_base}.{{png,xml}}')
@ -29,7 +31,7 @@ def wait_for(d, on_screen_text, timeout=90, critical=True):
if on_screen_text in d.ui.dump_hierarchy(): if on_screen_text in d.ui.dump_hierarchy():
print(f'found: {on_screen_text} after {elapsed:.1f}s') print(f'found: {on_screen_text} after {elapsed:.1f}s')
return return
time.sleep(.75) time.sleep(0.75)
print(f'NOT FOUND: {on_screen_text} after {timeout}s') print(f'NOT FOUND: {on_screen_text} after {timeout}s')
screenshot(d, suffix='error') screenshot(d, suffix='error')
if critical: if critical:

View file

@ -1,3 +1,5 @@
# Copyright (c) 2019-2024, see AUTHORS. Licensed under MIT License, see LICENSE
from common import screenshot, wait_for from common import screenshot, wait_for

View file

@ -1,5 +1,9 @@
import bootstrap_channels # Copyright (c) 2019-2024, see AUTHORS. Licensed under MIT License, see LICENSE
import base64
import time
import bootstrap_channels
from common import screenshot, wait_for from common import screenshot, wait_for
@ -54,13 +58,13 @@ def run(d):
screenshot(d, 'zip-is-still-there') screenshot(d, 'zip-is-still-there')
def change_shell_and_relogin(shell, descr): def change_shell_and_relogin(shell, descr):
import base64 config = (
import time '{pkgs, ...}: {user.shell = %SHELL%; '
config = ('{pkgs, ...}: {user.shell = %SHELL%; ' + 'system.stateVersion = "24.05";}'
'system.stateVersion = "24.05";}').replace('%SHELL%', shell) ).replace('%SHELL%', shell)
config_base64 = base64.b64encode(config.encode()).decode() config_base64 = base64.b64encode(config.encode()).decode()
d(f'input text "echo {config_base64} | base64 -d > ' cfg_file = '~/.config/nixpkgs/nix-on-droid.nix"'
'~/.config/nixpkgs/nix-on-droid.nix"') d(f'input text "echo {config_base64} | base64 -d > {cfg_file}')
d.ui.press('enter') d.ui.press('enter')
screenshot(d, f'pre-switch-{descr}') screenshot(d, f'pre-switch-{descr}')
d(f'input text "nix-on-droid switch && echo switched {descr}"') d(f'input text "nix-on-droid switch && echo switched {descr}"')
@ -85,8 +89,7 @@ def run(d):
change_shell_and_relogin('"${pkgs.fish}"', 'fish-directory') change_shell_and_relogin('"${pkgs.fish}"', 'fish-directory')
wait_for(d, 'Cannot execute shell ') wait_for(d, 'Cannot execute shell ')
wait_for(d, 'it is a directory.') wait_for(d, 'it is a directory.')
wait_for(d, wait_for(d, "You should point 'user.shell' to the exact binary.")
"You should point 'user.shell' to the exact binary.")
wait_for(d, 'Falling back to bash.') wait_for(d, 'Falling back to bash.')
wait_for(d, 'bash-5.2$') wait_for(d, 'bash-5.2$')
screenshot(d, 're-login-done-shell-dir-fallback') screenshot(d, 're-login-done-shell-dir-fallback')

View file

@ -1,7 +1,9 @@
import bootstrap_channels # Copyright (c) 2019-2024, see AUTHORS. Licensed under MIT License, see LICENSE
import subprocess import subprocess
import sys import sys
import bootstrap_channels
from common import screenshot, wait_for from common import screenshot, wait_for
STD = '/data/data/com.termux.nix/files/home/.cache/nix-on-droid-self-test' STD = '/data/data/com.termux.nix/files/home/.cache/nix-on-droid-self-test'
@ -33,18 +35,21 @@ def run(d):
f'touch {STD}/confirmation-granted', f'touch {STD}/confirmation-granted',
'/data/data/com.termux.nix/files/usr/bin/login echo test', '/data/data/com.termux.nix/files/usr/bin/login echo test',
'/data/data/com.termux.nix/files/usr/bin/login id', '/data/data/com.termux.nix/files/usr/bin/login id',
('cd /data/data/com.termux.nix/files/home; ' (
'pwd; ' 'cd /data/data/com.termux.nix/files/home; '
'id; ' 'pwd; '
'env PATH= /data/data/com.termux.nix/files/usr/bin/login ' 'id; '
' nix-on-droid on-device-test'), 'env PATH= /data/data/com.termux.nix/files/usr/bin/login '
' nix-on-droid on-device-test'
),
]: ]:
print(f'running {cmd} as {user} with capture:') print(f'running {cmd} as {user} with capture:')
p = subprocess.Popen(['adb', 'shell', 'su', '0', 'su', user, p = subprocess.Popen(
'sh', '-c', f"'{cmd}'"], ['adb', 'shell', 'su', '0', 'su', user, 'sh', '-c', f"'{cmd}'"],
encoding='utf-8', encoding='utf-8',
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT) stderr=subprocess.STDOUT,
)
out = '' out = ''
while p.poll() is None: while p.poll() is None:
line = p.stdout.readline() line = p.stdout.readline()

View file

@ -1,3 +1,5 @@
# Copyright (c) 2019-2024, see AUTHORS. Licensed under MIT License, see LICENSE
import bootstrap_channels import bootstrap_channels
import on_device_tests import on_device_tests