-
Notifications
You must be signed in to change notification settings - Fork 81
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
tests/emulator, .github/workflows/emulator: add
- Loading branch information
Showing
9 changed files
with
377 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
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: 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 }}-${{ 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,3 +2,4 @@ | |
/source.tar.gz | ||
result | ||
result-* | ||
**/__pycache__ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |