mirror of
https://github.com/nix-community/nix-on-droid.git
synced 2025-11-08 19:46:07 +01:00
tests/emulator, .github/workflows/emulator: add
This commit is contained in:
parent
d72ab2a167
commit
291970c4c4
9 changed files with 374 additions and 0 deletions
100
.github/workflows/emulator.yml
vendored
Normal file
100
.github/workflows/emulator.yml
vendored
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
name: Test nix-on-droid in an emulator
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
schedule:
|
||||||
|
- cron: 0 0 * * 1
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
emulate:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
api-level: [29]
|
||||||
|
# below 28: didn't start, IDK why
|
||||||
|
# 34: sometimes work, but doesn't seem stable, even w/o caching images
|
||||||
|
way:
|
||||||
|
- bootstrap_flakes
|
||||||
|
- bootstrap_channels
|
||||||
|
- poke_around
|
||||||
|
- test_channels_uiautomator
|
||||||
|
- test_channels_shell
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Nix / enable KVM
|
||||||
|
uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
|
||||||
|
- name: Setup cachix
|
||||||
|
uses: cachix/cachix-action@v14
|
||||||
|
with:
|
||||||
|
name: nix-on-droid
|
||||||
|
signingKey: "${{ secrets.CACHIX_SIGNING_KEY }}"
|
||||||
|
|
||||||
|
- name: Build droidctl
|
||||||
|
run: nix build 'github:t184256/droidctl' --out-link droidctl
|
||||||
|
|
||||||
|
- name: Build zipball, channel tarball and flake to inject
|
||||||
|
run: |
|
||||||
|
rm -rf n-o-d
|
||||||
|
mkdir -p n-o-d
|
||||||
|
git -C . archive --format=tar.gz --prefix n-o-d/ HEAD > n-o-d/archive.tar.gz
|
||||||
|
ARCHES=x86_64 nix run '.#deploy' -- file:///data/local/tmp/n-o-d/archive.tar.gz n-o-d/
|
||||||
|
|
||||||
|
- name: Configure AVD cache
|
||||||
|
if: matrix.api-level == 29
|
||||||
|
uses: actions/cache@v4
|
||||||
|
id: avd-cache
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.android/avd/*
|
||||||
|
~/.android/adb*
|
||||||
|
key: avd-${{ matrix.api-level }}-${{ matrix.way }} # concurrent save
|
||||||
|
|
||||||
|
- name: Create AVD and generate snapshot for caching
|
||||||
|
if: matrix.api-level == 29 && steps.avd-cache.outputs.cache-hit != 'true'
|
||||||
|
uses: reactivecircus/android-emulator-runner@v2
|
||||||
|
with:
|
||||||
|
target: default
|
||||||
|
arch: x86_64
|
||||||
|
api-level: ${{ matrix.api-level }}
|
||||||
|
force-avd-creation: false
|
||||||
|
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
|
||||||
|
disable-animations: false
|
||||||
|
script: echo "Generated AVD snapshot for caching."
|
||||||
|
|
||||||
|
- name: Test way=${{ matrix.way}} api-level=${{ matrix.api-level }}
|
||||||
|
uses: reactivecircus/android-emulator-runner@v2
|
||||||
|
with:
|
||||||
|
target: default
|
||||||
|
arch: x86_64
|
||||||
|
api-level: ${{ matrix.api-level }}
|
||||||
|
force-avd-creation: false
|
||||||
|
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
|
||||||
|
disable-animations: true
|
||||||
|
# https://github.com/ReactiveCircus/android-emulator-runner/issues/385
|
||||||
|
script: >
|
||||||
|
trap 'pkill --exact --echo --signal SIGKILL crashpad_handle || true' EXIT &&
|
||||||
|
adb shell 'rm -rf /data/local/tmp/n-o-d' &&
|
||||||
|
adb push n-o-d /data/local/tmp/ &&
|
||||||
|
echo 'pushed' &&
|
||||||
|
adb shell 'cd /data/local/tmp/n-o-d && tar xzof archive.tar.gz && mv n-o-d unpacked' &&
|
||||||
|
echo 'unpacked' &&
|
||||||
|
cd tests/emulator &&
|
||||||
|
adb shell settings put secure enabled_accessibility_services com.google.android.marvin.talkback/com.google.android.marvin.talkback.TalkBackService &&
|
||||||
|
echo 'ready' &&
|
||||||
|
nix run 'github:t184256/droidctl' -- run ${{ matrix.way }}.py
|
||||||
|
# TODO: push to cachix from within the emulator
|
||||||
|
|
||||||
|
- name: Upload screenshots
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: screenshots-${{ matrix.way }}-${{ matrix.api-level }}
|
||||||
|
path: tests/emulator/screenshots
|
||||||
|
if-no-files-found: warn # 'error' or 'ignore' are also available
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -2,3 +2,4 @@
|
||||||
/source.tar.gz
|
/source.tar.gz
|
||||||
result
|
result
|
||||||
result-*
|
result-*
|
||||||
|
**/__pycache__
|
||||||
|
|
|
||||||
43
tests/emulator/bootstrap_channels.py
Normal file
43
tests/emulator/bootstrap_channels.py
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
from common import screenshot, wait_for, APK, BOOTSTRAP_URL
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
def run(d):
|
||||||
|
nod = d.app('com.termux.nix', url=APK)
|
||||||
|
nod.permissions.allow_notifications()
|
||||||
|
nod.launch()
|
||||||
|
|
||||||
|
wait_for(d, 'Bootstrap zipball location')
|
||||||
|
d.ui(className='android.widget.EditText').set_text(BOOTSTRAP_URL)
|
||||||
|
screenshot(d, 'entered-url')
|
||||||
|
for i in range(2):
|
||||||
|
if 'text="OK"' not in d.ui.dump_hierarchy():
|
||||||
|
d.ui.press('back')
|
||||||
|
time.sleep(.5)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
screenshot(d, 'entered-url-back')
|
||||||
|
d.ui(text='OK').click()
|
||||||
|
screenshot(d, 'ok-clicked')
|
||||||
|
|
||||||
|
wait_for(d, 'Welcome to Nix-on-Droid!')
|
||||||
|
screenshot(d, 'bootstrap-begins')
|
||||||
|
wait_for(d, 'Do you want to set it up with flakes? (y/N)')
|
||||||
|
d.ui.press('enter')
|
||||||
|
wait_for(d, 'Setting up Nix-on-Droid with channels...')
|
||||||
|
|
||||||
|
wait_for(d, 'Installing and updating nix-channels...')
|
||||||
|
wait_for(d, 'unpacking channels...')
|
||||||
|
wait_for(d, 'Installing first Nix-on-Droid generation...', timeout=600)
|
||||||
|
wait_for(d, 'Copying default Nix-on-Droid config...', timeout=180)
|
||||||
|
wait_for(d, 'Congratulations!')
|
||||||
|
wait_for(d, 'See config file for further information.')
|
||||||
|
wait_for(d, 'bash-5.2$')
|
||||||
|
screenshot(d, 'bootstrap-ends')
|
||||||
|
|
||||||
|
d('input text "echo smoke-test | base64"')
|
||||||
|
d.ui.press('enter')
|
||||||
|
wait_for(d, 'c21va2UtdGVzdAo=')
|
||||||
|
|
||||||
|
screenshot(d, 'success-bootstrap-channels')
|
||||||
43
tests/emulator/bootstrap_flakes.py
Normal file
43
tests/emulator/bootstrap_flakes.py
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
from common import screenshot, wait_for, APK, BOOTSTRAP_URL
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
def run(d):
|
||||||
|
nod = d.app('com.termux.nix', url=APK)
|
||||||
|
nod.permissions.allow_notifications()
|
||||||
|
nod.launch()
|
||||||
|
|
||||||
|
wait_for(d, 'Bootstrap zipball location')
|
||||||
|
d.ui(className='android.widget.EditText').set_text(BOOTSTRAP_URL)
|
||||||
|
screenshot(d, 'entered-url')
|
||||||
|
for i in range(2):
|
||||||
|
if 'text="OK"' not in d.ui.dump_hierarchy():
|
||||||
|
d.ui.press('back')
|
||||||
|
time.sleep(.5)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
screenshot(d, 'entered-url-back')
|
||||||
|
d.ui(text='OK').click()
|
||||||
|
screenshot(d, 'ok-clicked')
|
||||||
|
|
||||||
|
wait_for(d, 'Welcome to Nix-on-Droid!')
|
||||||
|
screenshot(d, 'bootstrap-begins')
|
||||||
|
wait_for(d, 'Do you want to set it up with flakes? (y/N)')
|
||||||
|
d('input text y')
|
||||||
|
d.ui.press('enter')
|
||||||
|
wait_for(d, 'Setting up Nix-on-Droid with flakes...')
|
||||||
|
|
||||||
|
wait_for(d, 'Installing flake from default template...')
|
||||||
|
wait_for(d, 'Overriding input urls / arch in flake...')
|
||||||
|
wait_for(d, 'Installing first Nix-on-Droid generation...', timeout=600)
|
||||||
|
wait_for(d, 'Building activation package')
|
||||||
|
wait_for(d, 'Congratulations!', timeout=900)
|
||||||
|
wait_for(d, 'bash-5.2$')
|
||||||
|
screenshot(d, 'bootstrap-ends')
|
||||||
|
|
||||||
|
d('input text "echo smoke-test | base64"') # remove
|
||||||
|
d.ui.press('enter')
|
||||||
|
wait_for(d, 'c21va2UtdGVzdAo=')
|
||||||
|
|
||||||
|
screenshot(d, 'success-bootstrap-flakes')
|
||||||
36
tests/emulator/common.py
Normal file
36
tests/emulator/common.py
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
SERVER = 'https://nix-on-droid.unboiled.info'
|
||||||
|
# Just use F-Droid through fdroidctl later when F-Droid has x86_64 builds
|
||||||
|
APK = f'{SERVER}/com.termux.nix_188035-x86_64.apk'
|
||||||
|
BOOTSTRAP_URL = 'file:///data/local/tmp/n-o-d'
|
||||||
|
|
||||||
|
|
||||||
|
def screenshot(d, suffix=''):
|
||||||
|
os.makedirs('screenshots', exist_ok=True)
|
||||||
|
fname_base = f'screenshots/{time.time()}-{suffix}'
|
||||||
|
d.ui.screenshot(f'{fname_base}.png')
|
||||||
|
with open(f'{fname_base}.xml', 'w') as f:
|
||||||
|
f.write(d.ui.dump_hierarchy())
|
||||||
|
print(f'screenshotted: {fname_base}.{{png,xml}}')
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for(d, on_screen_text, timeout=90, critical=True):
|
||||||
|
start = time.time()
|
||||||
|
last_displayed_time = None
|
||||||
|
while (elapsed := time.time() - start) < timeout:
|
||||||
|
display_time = int(timeout - elapsed)
|
||||||
|
if display_time != last_displayed_time:
|
||||||
|
print(f'waiting for `{on_screen_text}`: {display_time}s...')
|
||||||
|
sys.stdout.flush()
|
||||||
|
last_displayed_time = display_time
|
||||||
|
if on_screen_text in d.ui.dump_hierarchy():
|
||||||
|
print(f'found: {on_screen_text} after {elapsed:.1f}s')
|
||||||
|
return
|
||||||
|
time.sleep(.75)
|
||||||
|
print(f'NOT FOUND: {on_screen_text} after {timeout}s')
|
||||||
|
screenshot(d, suffix='error')
|
||||||
|
if critical:
|
||||||
|
sys.exit(1)
|
||||||
33
tests/emulator/on_device_tests.py
Normal file
33
tests/emulator/on_device_tests.py
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
from common import screenshot, wait_for
|
||||||
|
|
||||||
|
|
||||||
|
def run(d):
|
||||||
|
wait_for(d, 'bash-5.2$')
|
||||||
|
|
||||||
|
d('input text "nix-on-droid on-device-test"')
|
||||||
|
d.ui.press('enter')
|
||||||
|
wait_for(d, 'These semi-automated tests are destructive', timeout=180)
|
||||||
|
wait_for(d, 'Proceeding will wreck your installation.')
|
||||||
|
wait_for(d, 'Do you still wish to proceed?')
|
||||||
|
d('input text "I do"')
|
||||||
|
d.ui.press('enter')
|
||||||
|
screenshot(d, 'tests-started')
|
||||||
|
|
||||||
|
d.ui.open_notification()
|
||||||
|
d.ui(text='Nix').right(resourceId='android:id/expand_button').click()
|
||||||
|
screenshot(d, 'notification_expanded')
|
||||||
|
d.ui(description='Acquire wakelock').click()
|
||||||
|
screenshot(d, 'wakelock_acquired')
|
||||||
|
d.ui(description='Release wakelock').wait()
|
||||||
|
screenshot(d, 'gotta-go-back')
|
||||||
|
d.ui.press('back')
|
||||||
|
screenshot(d, 'went-back')
|
||||||
|
|
||||||
|
if 'text="Allow"' in d.ui.dump_hierarchy():
|
||||||
|
d.ui(text='Allow').click()
|
||||||
|
elif 'text="ALLOW"' in d.ui.dump_hierarchy():
|
||||||
|
d.ui(text='ALLOW').click()
|
||||||
|
screenshot(d, 'tests-running')
|
||||||
|
|
||||||
|
wait_for(d, 'tests, 0 failures in', timeout=1800)
|
||||||
|
screenshot(d, 'tests-finished')
|
||||||
54
tests/emulator/poke_around.py
Normal file
54
tests/emulator/poke_around.py
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
import bootstrap_channels
|
||||||
|
|
||||||
|
from common import screenshot, wait_for
|
||||||
|
|
||||||
|
|
||||||
|
def run(d):
|
||||||
|
bootstrap_channels.run(d)
|
||||||
|
|
||||||
|
d('input text "zip"')
|
||||||
|
d.ui.press('enter')
|
||||||
|
wait_for(d, 'bash: zip: command not found')
|
||||||
|
screenshot(d, 'no-zip')
|
||||||
|
|
||||||
|
# Smoke-test nix-shell + change config + apply config
|
||||||
|
d('input text "nix-shell -p gnumake -p gnused"')
|
||||||
|
d.ui.press('enter')
|
||||||
|
wait_for(d, '[nix-shell:~]$')
|
||||||
|
d('input text "make"')
|
||||||
|
d.ui.press('enter')
|
||||||
|
wait_for(d, 'No targets specified and no makefile found.')
|
||||||
|
screenshot(d, 'nix-shell-with-make-and-sed')
|
||||||
|
# Change config and apply it
|
||||||
|
d('input text \'sed -i "s|#zip|zip|g" .config/nixpkgs/nix-on-droid.nix\'')
|
||||||
|
d.ui.press('enter')
|
||||||
|
d('input text "exit"')
|
||||||
|
d.ui.press('enter')
|
||||||
|
screenshot(d, 'pre-switch')
|
||||||
|
d('input text "nix-on-droid switch"')
|
||||||
|
d.ui.press('enter')
|
||||||
|
screenshot(d, 'post-switch')
|
||||||
|
|
||||||
|
# Verify zip is there
|
||||||
|
d('input text "zip -v | head -n2"')
|
||||||
|
d.ui.press('enter')
|
||||||
|
wait_for(d, 'This is Zip')
|
||||||
|
screenshot(d, 'zip-appears')
|
||||||
|
|
||||||
|
# Re-login and make sure login is still operational
|
||||||
|
|
||||||
|
d('input text "exit"')
|
||||||
|
d.ui.press('enter')
|
||||||
|
|
||||||
|
nod = d.app('com.termux.nix')
|
||||||
|
nod.launch()
|
||||||
|
screenshot(d, 're-login')
|
||||||
|
wait_for(d, 'Installing new login-inner...')
|
||||||
|
wait_for(d, 'bash-5.2$')
|
||||||
|
screenshot(d, 're-login-done')
|
||||||
|
|
||||||
|
# And verify zip is still there
|
||||||
|
d('input text "zip -v | head -n2"')
|
||||||
|
d.ui.press('enter')
|
||||||
|
wait_for(d, 'This is Zip')
|
||||||
|
screenshot(d, 'zip-is-still-there')
|
||||||
57
tests/emulator/test_channels_shell.py
Normal file
57
tests/emulator/test_channels_shell.py
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
import bootstrap_channels
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from common import screenshot, wait_for
|
||||||
|
|
||||||
|
STD = '/data/data/com.termux.nix/files/home/.cache/nix-on-droid-self-test'
|
||||||
|
|
||||||
|
|
||||||
|
def run(d):
|
||||||
|
bootstrap_channels.run(d)
|
||||||
|
|
||||||
|
# re-login for variety. the other on-device-test (uiautomator one) does not
|
||||||
|
d('input text "exit"')
|
||||||
|
screenshot(d, 'pre-relogin')
|
||||||
|
d.ui.press('enter')
|
||||||
|
|
||||||
|
nod = d.app('com.termux.nix')
|
||||||
|
nod.launch()
|
||||||
|
d.ui.press('enter')
|
||||||
|
screenshot(d, 'post-relogin')
|
||||||
|
wait_for(d, 'bash-5.2$')
|
||||||
|
|
||||||
|
# run tests in a way that'd display progress in CI
|
||||||
|
user = d.su('stat -c %U /data/data/com.termux.nix').output.strip()
|
||||||
|
# WARNING: assumes `su 0` style `su` that doesn't support -c from now on
|
||||||
|
print(f'{user=}')
|
||||||
|
sys.stdout.flush()
|
||||||
|
sys.stderr.flush()
|
||||||
|
for cmd in [
|
||||||
|
'id',
|
||||||
|
f'mkdir -p {STD}',
|
||||||
|
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 id',
|
||||||
|
('cd /data/data/com.termux.nix/files/home; '
|
||||||
|
'pwd; '
|
||||||
|
'id; '
|
||||||
|
'/data/data/com.termux.nix/files/usr/bin/login '
|
||||||
|
' nix-on-droid on-device-test')
|
||||||
|
]:
|
||||||
|
print(f'running {cmd} as {user} with capture:')
|
||||||
|
p = subprocess.Popen(['adb', 'shell', 'su', '0', 'su', user,
|
||||||
|
'sh', '-c', f"'{cmd}'"],
|
||||||
|
encoding='utf-8',
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT)
|
||||||
|
out = ''
|
||||||
|
while p.poll() is None:
|
||||||
|
line = p.stdout.readline()
|
||||||
|
out += line
|
||||||
|
sys.stdout.write('> ' + line)
|
||||||
|
sys.stdout.flush()
|
||||||
|
print(f'returncode: {p.returncode}')
|
||||||
|
# guess what, it can swallow the exit code!
|
||||||
|
|
||||||
|
assert 'tests, 0 failures in' in out # of the last command
|
||||||
7
tests/emulator/test_channels_uiautomator.py
Normal file
7
tests/emulator/test_channels_uiautomator.py
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import bootstrap_channels
|
||||||
|
import on_device_tests
|
||||||
|
|
||||||
|
|
||||||
|
def run(d):
|
||||||
|
bootstrap_channels.run(d)
|
||||||
|
on_device_tests.run(d)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue