Skip to content

Commit

Permalink
tests/emulator, .github/workflows/emulator: add
Browse files Browse the repository at this point in the history
  • Loading branch information
t184256 committed Jun 15, 2024
1 parent a812566 commit 55b70fb
Show file tree
Hide file tree
Showing 9 changed files with 364 additions and 0 deletions.
100 changes: 100 additions & 0 deletions .github/workflows/emulator.yml
Original file line number Diff line number Diff line change
@@ -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, 34] # lowest working ATM, highest working ATM
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: Gradle cache
uses: gradle/actions/setup-gradle@v3

- 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 }}

- name: Create AVD and generate snapshot for caching
if: steps.avd-cache.outputs.cache-hit != 'true' && matrix.api-level == 29
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
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/ &&
adb shell 'cd /data/local/tmp/n-o-d && tar xzof archive.tar.gz && mv n-o-d unpacked' &&
cd tests/emulator &&
adb shell settings put secure enabled_accessibility_services com.google.android.marvin.talkback/com.google.android.marvin.talkback.TalkBackService &&
nix run 'github:t184256/droidctl' -- run ${{ matrix.way }}.py
- 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

# TODO: push to cachix from device
# - name: Push to cachix
# if: always() && github.event_name != 'pull_request'
# run: ...
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
/source.tar.gz
result
result-*
**/__pycache__
43 changes: 43 additions & 0 deletions tests/emulator/bootstrap_channels.py
Original file line number Diff line number Diff line change
@@ -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 changes: 43 additions & 0 deletions tests/emulator/bootstrap_flakes.py
Original file line number Diff line number Diff line change
@@ -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')
35 changes: 35 additions & 0 deletions tests/emulator/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
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...')
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 changes: 33 additions & 0 deletions tests/emulator/on_device_tests.py
Original file line number Diff line number Diff line change
@@ -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 changes: 54 additions & 0 deletions tests/emulator/poke_around.py
Original file line number Diff line number Diff line change
@@ -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')
48 changes: 48 additions & 0 deletions tests/emulator/test_channels_shell.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
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()
d('rm -f /data/data/com.termux.nix/files/home/odt-success')
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'
' && touch odt-success')
]:
subprocess.run(['adb', 'shell', 'su', '0', 'su', user,
'sh', '-c', f"'{cmd}'"])
# guess what, it can swallow the exit code!

d('ls -l /data/data/com.termux.nix/files/home/odt-success')
7 changes: 7 additions & 0 deletions tests/emulator/test_channels_uiautomator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import bootstrap_channels
import on_device_tests


def run(d):
bootstrap_channels.run(d)
on_device_tests.run(d)

0 comments on commit 55b70fb

Please sign in to comment.