summaryrefslogtreecommitdiffstats
path: root/tests/src
diff options
context:
space:
mode:
Diffstat (limited to 'tests/src')
-rwxr-xr-xtests/src/install-replicant-6.0-0003.py50
-rw-r--r--tests/src/lib/__init__.py0
-rw-r--r--tests/src/lib/bootloaders.py201
-rw-r--r--tests/src/lib/common.py39
-rw-r--r--tests/src/lib/config.py92
-rw-r--r--tests/src/lib/devices.py207
-rw-r--r--tests/src/lib/fdroid.py60
-rw-r--r--tests/src/lib/heimdall.py68
-rw-r--r--tests/src/lib/hosts.py67
-rw-r--r--tests/src/lib/releases.py206
-rw-r--r--tests/src/lib/replicant_releases.py260
-rw-r--r--tests/src/lib/replicant_source.py127
-rw-r--r--tests/src/lib/test.py117
-rw-r--r--tests/src/replicant_tests.conf24
-rwxr-xr-xtests/src/test-replicant-6.0-0004-rc5-migration.py88
15 files changed, 1606 insertions, 0 deletions
diff --git a/tests/src/install-replicant-6.0-0003.py b/tests/src/install-replicant-6.0-0003.py
new file mode 100755
index 0000000..3649d47
--- /dev/null
+++ b/tests/src/install-replicant-6.0-0003.py
@@ -0,0 +1,50 @@
+#!/usr/bin/env python3
+# Copyright (C) 2020 Denis 'GNUtoo' Carikli <GNUtoo@cyberdimension.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+import os, sys
+
+from lib.common import enable_trace
+# enable_trace(True)
+
+from lib.config import ReplicantConfig
+from lib.replicant_releases import *
+
+def usage(progname):
+ print('Usage: {} <device>'.format(progname))
+ print()
+ print('Example: {} i9300'.format(progname))
+ sys.exit(1)
+
+if __name__ == '__main__':
+ if len(sys.argv) != 2:
+ usage(sys.argv[0])
+
+ device = sys.argv[1]
+
+ config = ReplicantConfig()
+
+ major_version = '6.0'
+ minor_version = '0003'
+
+ install_replicant_release(config.get('replicant-installer'),
+ major_version,
+ minor_version,
+ device,
+ wipe_cache_and_data=False,
+ disable_modem=False,
+ quiet=True)
+
+ print("[ OK ] Installed Replicant {} {}".format(major_version,
+ minor_version))
diff --git a/tests/src/lib/__init__.py b/tests/src/lib/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/src/lib/__init__.py
diff --git a/tests/src/lib/bootloaders.py b/tests/src/lib/bootloaders.py
new file mode 100644
index 0000000..35163db
--- /dev/null
+++ b/tests/src/lib/bootloaders.py
@@ -0,0 +1,201 @@
+#!/usr/bin/env python3
+# Copyright (C) 2020 Denis 'GNUtoo' Carikli <GNUtoo@cyberdimension.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+import os
+import re
+import sh
+import time
+
+from lib.common import get_output
+from lib.common import trace
+from lib.heimdall import Heimdall
+
+# TODO: Derive that information from the PIT in the data repository instead
+class BootloaderRuntime(object):
+ def __init__(self, config, host, device, quiet=False):
+ self.config = config
+ self.device = device
+ self.host = host
+ self.quiet = quiet
+ self.heimdall = Heimdall(self.host, quiet=quiet)
+
+ @trace
+ def get_cache_partition(self):
+ if not self.quiet:
+ print("BootloaderRuntime.get_cache_partition()")
+
+ if str(self.device) in ['espresso3g', 'espressowifi', 'i9100', 'i9300',
+ 'i9305', 'n5100', 'n5110', 'n7000', 'n7100']:
+ return 'CACHE'
+ elif str(self.device) in ['maguro']:
+ return 'cache'
+ else:
+ assert(False)
+
+ @trace
+ def get_recovery_partition(self):
+ if str(self.device) in ['espresso3g', 'espressowifi', 'i9100', 'i9300',
+ 'i9305', 'n5100', 'n5110', 'n7000', 'n7100']:
+ return 'RECOVERY'
+ elif str(self.device) in ['maguro']:
+ return 'recovery'
+ else:
+ assert(False)
+
+ @trace
+ def get_boot_img_partition(self):
+ if str(self.device) in ['espresso3g', 'espressowifi', 'i9100',
+ 'n7000']:
+ return 'KERNEL'
+ elif str(self.device) in ['i9300', 'i9305', 'n5100', 'n5110', 'n7100']:
+ return 'BOOT'
+ elif str(self.device) in ['maguro']:
+ return 'boot'
+ else:
+ assert(False)
+
+ @trace
+ def is_ready(self):
+ return self.heimdall.is_ready()
+
+ @trace
+ def wait_for_ready(self):
+ while True:
+ if self.is_ready():
+ return True
+ else:
+ time.sleep(1)
+
+ @trace
+ def boot_to_bootloader(self):
+ device_offline = False
+ while True:
+ if self.is_ready():
+ return True
+
+ adb_state = None
+ try:
+ adb_state = get_output(self.host.run(['adb', 'get-state']))
+ except sh.ErrorReturnCode_1:
+ if not device_offline and not self.quiet:
+ print('\tNo devices detected. Possible causes:')
+ print('\t- ADB is not enabled? '
+ '(device booted in Replicant with adb off)')
+ print('\t- No devices are connected (bad cable?)')
+ print('\t- The device is off '
+ '(no powered on device detected)')
+ print('\t- The device is booting '
+ 'and adbd is not yet started')
+ device_offline = True
+ time.sleep(1)
+
+ if adb_state in ['device', 'recovery']:
+ self.host.run(['adb', 'reboot', 'bootloader'])
+ return self.wait_for_ready()
+
+ @trace
+ def install_recovery(self, path, retries=100):
+ if not self.quiet:
+ print('Starting to install the Recovery on {}'.format(self.device))
+
+ self.boot_to_bootloader()
+
+ partitions = {
+ self.get_boot_img_partition() : path,
+ self.get_recovery_partition() : path,
+ }
+
+ return self.heimdall.flash(partitions, retries)
+
+ @trace
+ def install_boot_img(self, path, retries=100):
+ if not self.quiet:
+ print('Starting to install the {} boot image on {}'.format(
+ path, self.device))
+
+ self.boot_to_bootloader()
+
+ partitions = {
+ self.get_boot_img_partition() : path,
+ }
+
+ return self.heimdall.flash(partitions)
+
+ @trace
+ def wipe_cache(self):
+ # It currently fails with the following error with a file of 8M on the
+ # Galaxy SIII (GT-I9300):
+ # Uploading CACHE
+ # 100%Ending session...
+ # R... (30 more, please see e.stdout)
+ #
+ # STDERR:
+ #
+ # ERROR: Failed to confirm end of file transfer sequence!
+ # ERROR: CACHE upload failed!
+ #
+ # ERROR: Failed to send end session packet!
+ assert(False)
+ # The code responsible for that failure has been kept below for
+ # reference and to enable to fix it.
+
+ # The protocol that heimdall uses is called Thor
+ # On all Replicant compatible devices with an Exynos, the nonfree
+ # bootloader (s-boot 4.0) has a very buggy implementation of Thor.
+ # Practically speaking:
+ # - If your computer is under heavy I/O and/or CPU load, it increases
+ # the probability of the transfer failling.
+ # - The probability of the transfer failing also increases with the
+ # increase of the image size.
+ # So we create a small file. On devices with other SOCs, like the Galaxy
+ # Nexus (GT-I9250) or the Galaxy Tab 2 (GT-P3100, GT-P3110, GT-P5100,
+ # GT-P5510), the stock bootloader seem to have implemented that protocol
+ # correctly. Upstream U-boot also has a free software implementation of
+ # that protocol.
+ cache_file_size = 8 * 1024 *1024
+ cache_partition_name = self.get_cache_partition()
+ tmpdir = self.host.mktemp()
+
+ part_img_path = '{}{}{}.img'.format(tmpdir, os.sep, cache_partition_name)
+
+ self.host.run(['dd',
+ 'if=/dev/zero',
+ 'of={}'.format(part_img_path),
+ 'count={}'.format(int(cache_file_size / 512))])
+
+ self.boot_to_bootloader()
+
+ partitions = {
+ cache_partition_name : part_img_path,
+ }
+
+ return self.heimdall.flash(partitions)
+
+ @trace
+ def run(self, commands):
+ # Remove that assert if you really want to run commands inside the
+ # bootloader.
+ # For now we support various nonfree bootloader that can run commands
+ # through the UART port and code through an exploit through USB.
+ # While it would probably not be easy to run bootloaders commands
+ # through USB, it might be possible to manipulate the PARAM partition
+ # to implement a subset of the functionalities.
+ assert(False)
+
+ @trace
+ def boot(self, environment, retries=100):
+ if environment == 'bootloader':
+ return self.heimdall.reboot()
diff --git a/tests/src/lib/common.py b/tests/src/lib/common.py
new file mode 100644
index 0000000..1c6d6c6
--- /dev/null
+++ b/tests/src/lib/common.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python3
+# Copyright (C) 2020 Denis 'GNUtoo' Carikli <GNUtoo@cyberdimension.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+import os
+import re
+
+trace_enabled = False
+
+def get_output(running_command):
+ return re.sub(os.linesep + '$', '', str(running_command))
+
+def enable_trace(enabled):
+ global trace_enabled
+ trace_enabled = enabled
+
+def trace(func):
+ def trace_func(*args, **kwargs):
+ print("+ {}()".format(func.__qualname__))
+ result = func(*args, **kwargs)
+ print("- {}()".format(func.__qualname__))
+ return result
+ if trace_enabled:
+ return trace_func
+ else:
+ return func
+
diff --git a/tests/src/lib/config.py b/tests/src/lib/config.py
new file mode 100644
index 0000000..fcd5fdd
--- /dev/null
+++ b/tests/src/lib/config.py
@@ -0,0 +1,92 @@
+#!/usr/bin/env python3
+# Copyright (C) 2020 Denis 'GNUtoo' Carikli <GNUtoo@cyberdimension.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+import configparser
+import os
+
+class ReplicantConfig(object):
+ def __init__(self):
+ # This should implement the XDG Base Directory Specification which is
+ # available here:
+ # https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
+
+ config_file = None
+ xdg_config_home = ''
+ try:
+ xdg_config_home = os.environ['XDG_CONFIG_HOME']
+ except KeyError:
+ pass
+
+ if xdg_config_home == '':
+ try:
+ xdg_config_home = os.environ['HOME'] + os.sep + '.config'
+ except KeyError:
+ # This follow strictly the specification
+ xdg_config_home = os.sep + '.config'
+
+ xdg_config_dirs = ''
+ try:
+ xdg_config_dirs = os.environ['XDG_CONFIG_DIRS']
+ except KeyError:
+ pass
+ if xdg_config_dirs == '':
+ xdg_config_dirs = os.sep + 'etc' + os.sep + 'xdg'
+
+ for base in [xdg_config_home] + xdg_config_dirs.split(os.pathsep):
+ config_path = base + os.sep + 'replicant' + os.sep \
+ + 'replicant_tests.conf'
+ if not os.path.isfile(config_path):
+ continue
+ try:
+ # Silently skip the file in case of issues as per the
+ # specification
+ config_file = open(config_path, 'r')
+ except:
+ pass
+
+ if config_file:
+ break
+
+ if config_file is None:
+ # TODO: raise some error
+ print("Configuration file not found")
+ assert(False)
+
+ self.config = configparser.ConfigParser()
+ self.config.read_file(config_file)
+
+ # The config parameter corresponds to the sections in the configuration
+ # file. At the time of writing we have the following sections:
+ # - replicant-builder
+ # - replicant-installer
+ # - fdroid-installer
+ def get(self, section):
+ if section not in ['replicant-builder',
+ 'replicant-installer',
+ 'fdroid-installer']:
+ # TODO
+ assert(False)
+
+ results = {}
+
+ replicant_installer_config = self.config[section]
+ for k, v in replicant_installer_config.items():
+ if v and v.startswith('~'):
+ results[k] = os.environ['HOME'] + v[1:]
+ else:
+ results[k] = v
+
+ return results
diff --git a/tests/src/lib/devices.py b/tests/src/lib/devices.py
new file mode 100644
index 0000000..be271e7
--- /dev/null
+++ b/tests/src/lib/devices.py
@@ -0,0 +1,207 @@
+#!/usr/bin/env python3
+# Copyright (C) 2020 Denis 'GNUtoo' Carikli <GNUtoo@cyberdimension.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+import os
+import re
+import sh
+import time
+
+import lib.bootloaders
+import lib.replicant_releases
+
+from lib.common import get_output
+from lib.common import trace
+
+class Device(object):
+ def __init__(self, host, name, quiet=False):
+ self.host = host
+ self.name = name
+ self.quiet = quiet
+
+ def __str__(self):
+ # TODO: add codename <-> name
+ return self.name
+
+class DeviceRuntime(object):
+ def __init__(self, config, host, device, release, quiet=False):
+ self.device = device
+ self.config = config
+ self.host = host
+ self.quiet = quiet
+ self.release = release
+
+ self.bootloader = lib.bootloaders.BootloaderRuntime(config, host,
+ device, quiet=quiet)
+ self.recovery = lib.replicant_releases.RecoveryRuntime(config, host,
+ device, release,
+ quiet=quiet)
+ def run(self, commands):
+ # We have no visibility on the device state at this point
+ assert(False)
+
+ @trace
+ def state(self):
+ if self.bootloader.is_ready():
+ return 'bootloader'
+
+ adb_state = None
+ try:
+ # Available adb states: device, recovery, rescue, sideload,
+ # bootloader, and disconnect
+ adb_state = get_output(self.host.run(['adb', 'get-state']))
+ except sh.ErrorReturnCode_1:
+ return 'disconnect'
+ return adb_state
+
+ # If state is None: waits for a state other than disconnected
+ # else waits for the given state
+ @trace
+ def wait_for_state(self, given_state=None, retries=100):
+ while retries > 0:
+ new_state = self.state()
+ if new_state == 'disconnect':
+ continue
+ elif given_state == None:
+ return new_state
+ elif new_state != given_state:
+ continue
+ else:
+ return new_state
+ retries -= 1
+ time.sleep(1)
+
+ @trace
+ def wait_for_boot_complete(self, retries=100):
+ self.wait_for_state('device', retries)
+ while retries > 0:
+ output = get_output(self.host.run(['adb', 'shell',
+ 'getprop', 'dev.bootcomplete']))
+ if output == 1:
+ return True
+ retries -= 1
+ time.sleep(1)
+ return False
+
+ @trace
+ def wipe_cache_and_data(self):
+ # TODO: The bootloader is way less reliable for that, however the
+ # recovery could also be stuck in sideload mode without any adb
+ # access. In that case it would not be possible anymore to use
+ # adb to wipe the cache and data, so we'd need to use the bootloader.
+ return self.recovery.wipe_cache_and_data()
+
+ @trace
+ def install_zip(self, path, add_root=True, disable_modem=False):
+ self.recovery.install_zip(path)
+
+ # We can't simply reflash the boot.img from the bootloader because
+ # if we want to disable the modem without leaving root enabled, once
+ # we flashed back the stock boot.img, we would end up booting
+ # without any way to reboot in the recovery.
+ #
+ # And here we need to always end up in the recovery for consistency
+ # as simply installing a zip (without adding root or disabling the
+ # modem) still ends up in the recovery.
+ # So the solution here is to do as much as possible from within the
+ # recovery when adding root or disabling the modem
+ if add_root:
+ tmpdir = self.host.mktemp()
+ boot_img_path = tmpdir + os.sep + 'boot.img'
+ new_boot_img_path = boot_img_path + '.new'
+
+ self.host.run(['unzip', path, '-d', tmpdir])
+ self.host.run(['add_adb_root.py', boot_img_path, new_boot_img_path])
+
+ self.recovery.install_boot_img(new_boot_img_path)
+ self.host.run(['rm', '-f', new_boot_img_path])
+
+ if disable_modem:
+ # This uses the device fstab, and /system is standard so it should
+ # work on any device
+ self.host.run(['adb', 'shell', 'mount', '/system'])
+
+ # modem.sh off does basically the same thing, so modem.sh on will
+ # also work if the user wants to re-enable the modem
+ self.host.run(['adb', 'shell', 'mv', '/system/lib/libsamsung-ril.so', '/system/lib/libsamsung-ril.so.disabled'])
+
+ self.host.run(['adb', 'shell', 'umount', '/system'])
+ self.host.run(['adb', 'shell', 'sync'])
+
+ @trace
+ def install_apk(self, path):
+ self.wait_for_state('device')
+ self.host.run(['adb', 'install', path])
+
+ @trace
+ def get_adb_root_recovery(self, original_recovery_path):
+ if not self.quiet:
+ print('Starting to create a new recovery with adb root access:')
+
+ recovery_filename = original_recovery_path.split(os.sep)[-1]
+
+ tmpdir = get_output(self.host.run(['mktemp', '-d']))
+ new_recovery_path = tmpdir + os.sep + recovery_filename
+
+ self.host.run(['add_adb_root.py', original_recovery_path,
+ new_recovery_path])
+
+ if not os.path.isfile(original_recovery_path):
+ print("{}: {} not found".format("DeviceRuntime.get_adb_root_recovery",
+ original_recovery_path))
+ assert(False)
+ if not os.path.isfile(new_recovery_path):
+ print("{}: add_adb_root.py failed: {} not found".format(
+ "DeviceRuntime.get_adb_root_recovery",
+ original_recovery_path))
+ assert(False)
+
+ return new_recovery_path
+
+ @trace
+ def install_recovery(self, path, add_root, retries=10):
+ if add_root == True:
+ path = self.get_adb_root_recovery(path)
+
+ self.bootloader.install_recovery(path)
+
+ if add_root == True:
+ self.host.run(['rm', '-f', path])
+
+ # Possible values for environment:
+ # - bootloader
+ # - device
+ # - recovery
+ @trace
+ def boot(self, environment, retries=100):
+ while retries > 0:
+ state = self.state()
+ if not self.quiet:
+ print("\tDeviceRuntime.boot: state: {}".format(state))
+ if state == 'bootloader':
+ return self.bootloader.boot(environment)
+ elif state in ['device', 'recovery']:
+ return self.host.run(['adb', 'reboot', environment])
+ elif state == 'disconnect':
+ # The device can be in that state when adbd has not been started
+ # yet
+ pass
+ elif state in ['rescue', 'sideload']:
+ # TODO
+ assert(False)
+ else:
+ assert(False)
+ time.sleep(1)
+ retries -= 1
+ assert(False)
diff --git a/tests/src/lib/fdroid.py b/tests/src/lib/fdroid.py
new file mode 100644
index 0000000..c02f20c
--- /dev/null
+++ b/tests/src/lib/fdroid.py
@@ -0,0 +1,60 @@
+#!/usr/bin/env python3
+# Copyright (C) 2020 Denis 'GNUtoo' Carikli <GNUtoo@cyberdimension.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+import os
+
+import lib.hosts
+
+from lib.common import trace
+
+class FDroidAPK(object):
+ def __init__(self, config, host, runtime, apk_name, quiet=False):
+ self.config = config
+ self.apk_name = apk_name
+ self.host = host
+ self.runtime = runtime
+
+ @trace
+ def download(self):
+ # TODO, also handles caching and host repository dir
+ pass
+
+ @trace
+ def get_path(self):
+ return self.config['releases_directory'] + os.sep + self.apk_name
+
+ @trace
+ def verify(self):
+ # TODO: verify signature
+ return True
+
+ @trace
+ def install(self):
+ self.verify()
+ self.runtime.install_apk(self.get_path())
+
+ # TODO
+ @trace
+ def get_supported_targets(self):
+ pass
+
+def install_fdroid_apk(config, apk_name, quiet=False):
+ host = lib.hosts.Host(config, quiet=quiet)
+ device = lib.devices.Device(host, 'i9300')
+ runtime = lib.devices.DeviceRuntime(config, host, device, '6.0', quiet=quiet)
+
+ apk = FDroidAPK(config, host, runtime, apk_name, quiet=quiet)
+ apk.install()
diff --git a/tests/src/lib/heimdall.py b/tests/src/lib/heimdall.py
new file mode 100644
index 0000000..2aaed25
--- /dev/null
+++ b/tests/src/lib/heimdall.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python3
+# Copyright (C) 2020 Denis 'GNUtoo' Carikli <GNUtoo@cyberdimension.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+import sh
+import time
+
+from lib.common import get_output
+from lib.common import trace
+
+class Heimdall(object):
+ def __init__(self, host, quiet=False):
+ self.host = host
+ self.quiet = quiet
+
+ # partitions: {'BOOT': '/tmp/tmp.Tq9jPtBpmj/recovery-n7100.img'}
+ @trace
+ def flash(self, partitions, retries=100):
+ command = ['heimdall', 'flash']
+
+ for partition, file_path in partitions.items():
+ command.append('--' + partition)
+ command.append(file_path)
+
+ output = ""
+ while retries > 0:
+ try:
+ output = self.host.run(command)
+ except sh.ErrorReturnCode_1:
+ # Very often, hemdall has error like this one and returns 1:
+ # libusb: error [get_usbfs_fd] libusb couldn't open USB
+ # device /dev/bus/usb/002/009, errno=5
+ # ERROR: Failed to access device. libusb error: -1
+ # In the case above, retrying fixed it. It's probably timing
+ # sensitive somehow.
+ time.sleep(1)
+ retries -= 1
+ break
+
+ return output
+
+ @trace
+ def is_ready(self):
+ try:
+ self.host.run(['heimdall', 'detect'])
+ except sh.ErrorReturnCode_1:
+ return False
+
+ return True
+
+ @trace
+ def reboot(self, retries=100):
+ if environment == 'bootloader':
+ while retries > 0:
+ self.host.run(['heimdall', 'close-pc-screen'])
+ time.sleep(1)
+ retries -= 1
diff --git a/tests/src/lib/hosts.py b/tests/src/lib/hosts.py
new file mode 100644
index 0000000..988ba6e
--- /dev/null
+++ b/tests/src/lib/hosts.py
@@ -0,0 +1,67 @@
+#!/usr/bin/env python
+# Copyright (C) 2020 Denis 'GNUtoo' Carikli <GNUtoo@cyberdimension.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+import os
+import re
+import sh
+
+from lib.common import trace
+
+class Host(object):
+ def __init__(self, config, quiet=False):
+ self.config = config
+ self.quiet = quiet
+ self.tmpdir = None
+
+ @trace
+ def is_remote(self):
+ if self.config['machine'] == None:
+ return False
+ elif re.match('^ssh://', self.config['machine']):
+ return True
+ else:
+ return False
+
+ @trace
+ def run(self, commands):
+ result = None
+
+ if not self.quiet:
+ print('\tRunning \'{}\''.format(' '.join(commands)))
+
+ if type(commands) == type(list()):
+ commands = ' '.join(commands)
+
+ shell_command = 'cd {} && {}'.format('~', commands.replace('"', '\\"'))
+
+ if self.is_remote():
+ machine = re.sub('^ssh://', '', self.config['machine'])
+ result = sh.ssh(machine, shell_command)
+ else:
+ result = sh.bash('-c', shell_command)
+
+ return result
+
+ @trace
+ def get_output(self, running_command):
+ return re.sub(os.linesep + '$', '', str(running_command))
+
+ @trace
+ def mktemp(self):
+ path = '~/.cache/replicant/tmp.XXXXXXXXXX'
+ if self.tmpdir == None:
+ self.run(['mkdir', '-p', os.path.dirname(path)])
+ self.tmpdir = self.get_output(self.run(['mktemp', '-d', path]))
+ return self.tmpdir
diff --git a/tests/src/lib/releases.py b/tests/src/lib/releases.py
new file mode 100644
index 0000000..c75c1a4
--- /dev/null
+++ b/tests/src/lib/releases.py
@@ -0,0 +1,206 @@
+#!/usr/bin/env python3
+# Copyright (C) 2020 Denis 'GNUtoo' Carikli <GNUtoo@cyberdimension.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+import os
+import re
+import sh
+
+import lib.devices
+import lib.hosts
+import lib.releases
+
+from lib.common import get_output
+from lib.common import trace
+
+class ReplicantRelease(object):
+ def __init__(self, config, major_version, minor_version, host, device,
+ quiet=False):
+ self.config = config
+ self.quiet = quiet
+
+ self.major_version = major_version
+ self.minor_version = minor_version
+ self.host = host
+ self.device = device
+
+ @trace
+ def get_recovery_path(self):
+ release_dir = os.sep.join([
+ self.config['releases_directory'],
+ 'replicant' + '-' + self.major_version,
+ self.minor_version])
+ return release_dir + os.sep + 'recovery-{}.img'.format(self.device)
+
+ @trace
+ def get_zip_path(self):
+ release_dir = os.sep.join([
+ self.config['releases_directory'],
+ 'replicant' + '-' + self.major_version,
+ self.minor_version])
+
+ filename = 'replicant-{}-{}.zip'.format(self.major_version,
+ self.device)
+
+ if self.major_version == '6.0':
+ if self.minor_version > '0003':
+ filename = 'replicant-{}-{}-{}.zip'.format(self.major_version,
+ self.minor_version,
+ self.device)
+
+ return release_dir + os.sep + filename
+
+ @trace
+ def get_supported_targets(self):
+ if self.major_version != '6.0':
+ assert(False)
+
+ return ['espresso3g', 'espressowifi', 'i9100', 'i9300', 'i9305',
+ 'maguro', 'n5100', 'n5110', 'n7000', 'n7100']
+
+class RecoveryRuntime(object):
+ def __init__(self, config, host, device, release, quiet=False):
+ self.config = config
+ self.quiet = quiet
+ self.host = host
+ self.device = device
+ self.release = release
+
+ @trace
+ def get_partition(self, partition):
+ if not self.release.major_version == '6.0':
+ assert(False)
+
+ if str(self.device) in ['i9100', 'i9300', 'n5100']:
+ return '/dev/block/platform/dw_mmc/by-name/' + str(partition)
+ elif str(self.device) in ['maguro']:
+ return '/dev/block/platform/omap/omap_hsmmc.0/by-name/' \
+ + str(partition)
+ elif str(self.device) in ['p3100']:
+ return '/dev/block/platform/omap/omap_hsmmc.1/by-name/' \
+ + str(partition)
+ else:
+ # TODO:
+ # - i9100[OK], i9300[OK], i9305, maguro, n5100[OK], n7000, n7100:
+ # /dev/block/platform/*/by-name/
+ # - i9100, i9300, i9305, n7000, n7100:
+ # /dev/block/platform/*/by-name/
+ # - p3100[OK]: /dev/block/platform/*/*/by-name/EFS
+ # - p5100: ?, p5510: ?, p3110: ?
+ assert(False)
+
+ @trace
+ def get_cache_partition(self):
+ if self.release.major_version != '6.0':
+ # TODO
+ assert(False)
+
+ if str(self.device) in ['maguro']:
+ return self.get_partition('cache')
+ elif str(self.device) in ['espresso3g', 'espressowifi', 'i9100',
+ 'i9300', 'i9305', 'n5100', 'n5110', 'n7000',
+ 'n7100']:
+ return self.get_partition('CACHE')
+
+ @trace
+ def wait_for_ready(self):
+ self.host.run(['adb', 'wait-for-recovery'])
+
+ @trace
+ def is_remote(self):
+ if self.config['machine'] == None:
+ return False
+ if re.match('^ssh://', self.config['machine']):
+ return True
+ else:
+ return False
+
+ @trace
+ def run(self, commands):
+ result = None
+
+ if not self.quiet:
+ print('\tRunning \'{}\''.format(' '.join(commands)))
+
+ if type(commands) == type(list()):
+ commands = ' '.join(commands)
+
+ shell_command = 'cd {} && {}'.format('~',
+ 'adb shell '
+ + '"'
+ + commands.replace('"', '\\"')
+ + '"')
+
+ self.wait_for_ready()
+
+ if self.is_remote():
+ machine = re.sub('^ssh://', '', self.config['machine'])
+ result = sh.ssh(machine, shell_command)
+ else:
+ result = sh.bash('-c', shell_command)
+
+ return result
+
+ @trace
+ def mount_cache(self):
+ return self.run(['mount', self.get_cache_partition(), '/cache'])
+
+ # Documentation: bootable/recovery/recovery.cpp
+ @trace
+ def append_recovery_command(self, command):
+ self.wait_for_ready()
+ self.run(['echo', command, '>>', '/cache/recovery/command'])
+
+ @trace
+ def wipe_cache_and_data(self):
+ if not self.quiet:
+ print('Starting to wipe the data data and cache partition:')
+
+ self.mount_cache()
+
+ self.append_recovery_command('--wipe_data')
+
+ self.host.run(['adb', 'reboot', 'recovery'])
+
+ @trace
+ def install_zip(self, recovery_path):
+ filename = recovery_path.split(os.sep)[-1]
+ if not self.quiet:
+ print('Installing {} on {}'.format(filename, self.device))
+
+ if not self.quiet:
+ print('Starting to install {} on {}'.format(filename, self.device))
+
+ self.append_recovery_command('--sideload')
+ self.host.run(['adb', 'reboot', 'recovery'])
+
+def install_release(local_config, major_version, minor_version, device):
+ if not self.quiet:
+ print('Installing Replicant {} {} on {}'.format(filename,
+ major_version,
+ minor_version,
+ self.device))
+ host = lib.hosts.Host(local_config)
+ device = lib.devices.Device(host, 'i9300')
+
+ release = lib.releases.ReplicantRelease(local_config,
+ major_version, minor_version,
+ host, device)
+
+ runtime = lib.devices.DeviceRuntime(local_config, host, device, release)
+
+ runtime.install_recovery(release.get_recovery_path(), add_root=True)
+ runtime.wipe_cache_and_data()
+ runtime.install_zip(release.get_zip_path(), add_root=True)
+ runtime.boot('replicant')
diff --git a/tests/src/lib/replicant_releases.py b/tests/src/lib/replicant_releases.py
new file mode 100644
index 0000000..3da6e47
--- /dev/null
+++ b/tests/src/lib/replicant_releases.py
@@ -0,0 +1,260 @@
+#!/usr/bin/env python3
+# Copyright (C) 2020 Denis 'GNUtoo' Carikli <GNUtoo@cyberdimension.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+import os
+import re
+import sh
+
+import lib.devices
+import lib.hosts
+
+from lib.common import get_output
+from lib.common import trace
+
+class ReplicantRelease(object):
+ def __init__(self, config, major_version, minor_version, host, device,
+ quiet=False):
+ self.config = config
+ self.quiet = quiet
+
+ self.major_version = major_version
+ self.minor_version = minor_version
+ self.host = host
+ self.device = device
+
+ @trace
+ def get_recovery_path(self):
+ path = ""
+ path += os.sep.join([
+ self.config['releases_directory'],
+ 'replicant' + '-' + self.major_version,
+ self.minor_version])
+ path += os.sep + 'images' + os.sep + str(self.device)
+ path += os.sep + 'recovery-{}.img'.format(self.device)
+ return path
+
+ @trace
+ def get_zip_path(self):
+ filename = 'replicant-{}-{}.zip'.format(self.major_version,
+ self.device)
+ if self.major_version == '6.0':
+ if self.minor_version > '0003':
+ filename = 'replicant-{}-{}-{}.zip'.format(self.major_version,
+ self.minor_version,
+ self.device)
+ path = ""
+ path += os.sep.join([
+ self.config['releases_directory'],
+ 'replicant' + '-' + self.major_version,
+ self.minor_version])
+ path += os.sep + 'images' + os.sep + str(self.device)
+ path += os.sep + filename
+ return path
+
+ @trace
+ def get_supported_targets(self):
+ if self.major_version != '6.0':
+ assert(False)
+
+ return ['espresso3g', 'espressowifi', 'i9100', 'i9300', 'i9305',
+ 'maguro', 'n5100', 'n5110', 'n7000', 'n7100']
+
+class RecoveryRuntime(object):
+ def __init__(self, config, host, device, release, quiet=False):
+ self.config = config
+ self.quiet = quiet
+ self.host = host
+ self.device = device
+ self.release = release
+
+ @trace
+ def get_partition(self, partition):
+ if not self.release.major_version == '6.0':
+ assert(False)
+
+ if str(self.device) in ['i9100', 'i9300', 'n7100', 'n5100']:
+ return '/dev/block/platform/dw_mmc/by-name/' + str(partition)
+ elif str(self.device) in ['maguro']:
+ return '/dev/block/platform/omap/omap_hsmmc.0/by-name/' \
+ + str(partition)
+ elif str(self.device) in ['p3100']:
+ return '/dev/block/platform/omap/omap_hsmmc.1/by-name/' \
+ + str(partition)
+ else:
+ # TODO:
+ # - i9305
+ # - n5110
+ # - n7000
+ # - p3110
+ # - p311
+ # - p5100
+ # - p5510
+ assert(False)
+
+ @trace
+ def get_boot_partition(self):
+ if self.release.major_version != '6.0':
+ # TODO
+ assert(False)
+
+ if str(self.device) in ['i9300', 'i9305', 'n7100']:
+ return self.get_partition('BOOT')
+ else:
+ # TODO:
+ # - i9100
+ # - maguro
+ # - n5110
+ # - n7000
+ # - p3100
+ # - p3110
+ # - p3113
+ # - p5100
+ # - p5100
+ # - p5510
+ assert(False)
+
+ @trace
+ def get_cache_partition(self):
+ if self.release.major_version != '6.0':
+ # TODO
+ assert(False)
+
+ if str(self.device) in ['maguro']:
+ return self.get_partition('cache')
+ elif str(self.device) in ['espresso3g', 'espressowifi', 'i9100',
+ 'i9300', 'i9305', 'n5100', 'n5110', 'n7000',
+ 'n7100']:
+ return self.get_partition('CACHE')
+ else:
+ # TODO:
+ # - p3110
+ # - p3113
+ # - p5100
+ # - p5510
+ assert(False)
+
+ @trace
+ def wait_for_ready(self):
+ self.host.run(['adb', 'wait-for-recovery'])
+
+ @trace
+ def is_remote(self):
+ if self.config['machine'] == None:
+ return False
+ if re.match('^ssh://', self.config['machine']):
+ return True
+ else:
+ return False
+
+ @trace
+ def run(self, commands):
+ result = None
+
+ if not self.quiet:
+ print('\tRunning \'{}\''.format(' '.join(commands)))
+
+ if type(commands) == type(list()):
+ commands = ' '.join(commands)
+
+ shell_command = 'cd {} && {}'.format('~',
+ 'adb shell '
+ + '"'
+ + commands.replace('"', '\\"')
+ + '"')
+
+ self.wait_for_ready()
+
+ if self.is_remote():
+ machine = re.sub('^ssh://', '', self.config['machine'])
+ result = sh.ssh(machine, shell_command)
+ else:
+ result = sh.bash('-c', shell_command)
+
+ return result
+
+ @trace
+ def mount_cache(self):
+ return self.run(['mount', self.get_cache_partition(), '/cache'])
+
+ # Documentation: bootable/recovery/recovery.cpp
+ @trace
+ def append_recovery_command(self, command):
+ self.wait_for_ready()
+ self.run(['echo', command, '>>', '/cache/recovery/command'])
+
+ @trace
+ def wipe_cache_and_data(self):
+ if not self.quiet:
+ print('Starting to wipe the data data and cache partition:')
+
+ self.mount_cache()
+
+ self.append_recovery_command('--wipe_data')
+
+ self.host.run(['adb', 'reboot', 'recovery'])
+
+ @trace
+ def install_zip(self, recovery_path):
+ filename = recovery_path.split(os.sep)[-1]
+
+ if not self.quiet:
+ print('RecoveryRuntime.install_zip: '
+ + 'Starting to install {} on {}'.format(filename, self.device))
+
+ self.append_recovery_command('--sideload')
+ self.host.run(['adb', 'reboot', 'recovery'])
+
+ @trace
+ def install_boot_img(self, path, retries=100):
+ if not self.quiet:
+ print('Starting to install the {} boot image on {}'.format(
+ path, self.device))
+
+ filename = os.path.basename(path)
+ partition = self.get_boot_partition()
+
+ self.host.run(['adb', 'wait-for-recovery'])
+ self.host.run(['adb', 'push', path, '/' + filename])
+ self.host.run(['adb', 'shell', 'dd', 'if=/dev/zero', 'of=' + partition])
+ self.host.run(['adb', 'shell', 'dd', 'if=' + filename, 'of=' + partition])
+ self.host.run(['adb', 'shell', 'sync'])
+ self.host.run(['adb', 'shell', 'rm', '-f', '/' + filename])
+
+def install_replicant_release(local_config,
+ major_version, minor_version,
+ device, wipe_cache_and_data=True,
+ add_root=True,
+ disable_modem=False, quiet=False):
+ if not quiet:
+ print('RecoveryRuntime.install_replicant_release: '
+ + 'Installing Replicant {} release for the {}'.format(
+ minor_version, device))
+ host = lib.hosts.Host(local_config, quiet=quiet)
+ device = lib.devices.Device(host, device, quiet=quiet)
+
+ release = lib.replicant_releases.ReplicantRelease(local_config,
+ major_version,
+ minor_version,
+ host, device, quiet)
+
+ runtime = lib.devices.DeviceRuntime(local_config, host, device, release,
+ quiet=quiet)
+
+ runtime.install_recovery(release.get_recovery_path(), add_root)
+ if wipe_cache_and_data:
+ runtime.wipe_cache_and_data()
+ runtime.install_zip(release.get_zip_path(), add_root, disable_modem)
+ runtime.boot('replicant')
+ runtime.wait_for_boot_complete()
diff --git a/tests/src/lib/replicant_source.py b/tests/src/lib/replicant_source.py
new file mode 100644
index 0000000..80aa581
--- /dev/null
+++ b/tests/src/lib/replicant_source.py
@@ -0,0 +1,127 @@
+#!/usr/bin/env python3
+# Copyright (C) 2020 Denis 'GNUtoo' Carikli <GNUtoo@cyberdimension.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+import os
+import re
+import sh
+
+from lib.common import get_output
+from lib.common import trace
+
+class ReplicantSource(object):
+ def __init__(self, config, quiet=False):
+ self.config = config
+ self.quiet = quiet
+
+ @trace
+ def is_remote(self):
+ if self.config['machine'] == None:
+ return False
+ elif re.match('^ssh://', self.config['machine']):
+ return True
+ else:
+ return False
+
+ @trace
+ def run(self, commands):
+ result = None
+
+ if not self.quiet:
+ print('\tRunning \'{}\''.format(' '.join(commands)))
+
+
+ if type(commands) == type(list()):
+ commands = ' '.join(commands)
+ replicant_dir = self.config['replicant_directory']
+ if not re.match('^/', replicant_dir) and \
+ not re.match('^~', replicant_dir):
+ replicant_dir = '~' + os.sep + replicant_dir
+
+ shell_command = 'cd {} && {}'.format(replicant_dir, commands)
+
+ if self.is_remote():
+ machine = re.sub('^ssh://', '', self.config['machine'])
+ result = sh.ssh(machine, shell_command)
+ else:
+ result = sh.bash('-c', shell_command)
+
+ return result
+
+ @trace
+ def repo_init(self):
+ if not self.quiet:
+ print('Starting to repo init:')
+ for manifest in ['manifest.xml', 'manifests', 'manifests.git']:
+ self.run([
+ 'rm', '-rf', '.repo' + os.sep + manifest
+ ])
+ self.run(
+ [
+ 'repo', 'init',
+ '-u', self.config['manifest_url'],
+ '-b', self.config['manifest_branch']
+ ]
+ )
+
+ @trace
+ def repo_needs_init_again(self):
+ if not self.quiet:
+ print('Starting to check if repo init is needed:')
+ git_cmd = ['git', '-C', '.repo/manifests']
+
+ for manifest in ['manifest.xml', 'manifests', 'manifests.git']:
+ manifest_path = '.repo' + os.sep + manifest
+
+ try:
+ output = get_output(self.run(['ls', '-d', manifest_path]))
+ except sh.ErrorReturnCode_2:
+ return True
+
+ output = get_output(
+ self.run(git_cmd + ['remote', 'get-url', 'origin'])
+ )
+
+ if output != self.config['manifest_url']:
+ return True
+
+ output = get_output(self.run(git_cmd + ['diff']))
+ if len(output) > 0:
+ return True
+
+ # If the manifest was force pushed and that the git pull cannot
+ # be fast-foward, we need to detect that.
+ self.run(git_cmd + ['fetch', 'origin'])
+
+ try:
+ self.run(git_cmd + ['merge-base', '--is-ancestor',
+ 'HEAD',
+ 'origin' + '/'
+ + self.config['manifest_branch']])
+ except sh.ErrorReturnCode_2:
+ return True
+
+ return False
+
+ @trace
+ def sync(self):
+ if not self.quiet:
+ print('Starting repo sync:')
+ self.run(['repo', 'sync', '--force-sync'])
+
+ @trace
+ def build_images(self, device):
+ if not self.quiet:
+ print('Starting build for {}'.format(device))
+ self.run(['./vendor/replicant/build.sh', device])
diff --git a/tests/src/lib/test.py b/tests/src/lib/test.py
new file mode 100644
index 0000000..cd5172d
--- /dev/null
+++ b/tests/src/lib/test.py
@@ -0,0 +1,117 @@
+#!/usr/bin/env python3
+# Copyright (C) 2020 Denis 'GNUtoo' Carikli <GNUtoo@cyberdimension.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+import os
+import re
+import sh
+import time
+
+import lib.devices
+import lib.hosts
+import lib.releases
+
+from lib.common import trace
+from lib.common import get_output
+
+class ReplicantDevRelease(object):
+ def __init__(self, config, major_version, minor_version, host, device,
+ quiet=False):
+ self.config = config
+ self.quiet = quiet
+
+ self.major_version = major_version
+ self.minor_version = minor_version
+ self.host = host
+ self.device = device
+
+ self.dist_dirs = os.sep.join(['out', 'dist', 'i9300'])
+ self.tmpdir = None
+
+ @trace
+ def is_remote(self):
+ if self.config['machine'] == None:
+ return False
+ elif re.match('^ssh://', self.config['machine']):
+ return True
+ else:
+ return False
+
+ @trace
+ def sync(self, remote_path):
+ if not self.is_remote():
+ assert(False)
+
+ if not self.quiet:
+ print('Starting to sync {}:'.format(remote_path))
+
+ if not self.tmpdir:
+ self.tmpdir = get_output(self.host.run(['mktemp', '-d']))
+
+ remote_arg = \
+ re.sub('^ssh://', '', self.config['machine']) + ':' + remote_path
+
+ args = ['-av', '--progress', '--partial', remote_arg, self.tmpdir]
+ if not self.quiet:
+ print('\tRunning \'{}\''.format('rsync' + ' '.join(args)))
+
+ sh.rsync(args)
+
+ @trace
+ def get_path(self, filename):
+ path = self.config['replicant_directory'] \
+ + os.sep \
+ + self.dist_dirs \
+ + os.sep \
+ + filename
+
+ if self.is_remote():
+ self.sync(path)
+ return self.tmpdir + os.sep + os.path.basename(path)
+ else:
+ return path
+
+ @trace
+ def get_recovery_path(self):
+ return self.get_path('recovery-{}.img'.format(self.device))
+
+ @trace
+ def get_zip_path(self):
+ return self.get_path('replicant-{}-{}-{}.zip'.format(
+ self.major_version, self.minor_version, self.device))
+
+ @trace
+ def get_supported_targets(self):
+ if self.major_version != '6.0':
+ assert(False)
+
+ return ['espresso3g', 'espressowifi', 'i9100', 'i9300', 'i9305',
+ 'maguro', 'n5100', 'n5110', 'n7000', 'n7100']
+
+def build_replicant_6(device='i9300'):
+ replicant = ReplicantSource(remote_config)
+ if replicant.repo_needs_init_again():
+ replicant.repo_init()
+ replicant.sync()
+ replicant.build_images(device)
+
+def install_dev_image(device='i9300'):
+ host = lib.hosts.Host(local_config)
+ device = lib.devices.Device(host, 'i9300')
+ release = ReplicantDevRelease(remote_config, '6.0', 'dev', host,
+ device)
+ runtime = lib.devices.DeviceRuntime(local_config, host, device, release)
+
+ runtime.install_recovery(release.get_recovery_path(), add_root=True)
+ runtime.install_zip(release.get_zip_path())
diff --git a/tests/src/replicant_tests.conf b/tests/src/replicant_tests.conf
new file mode 100644
index 0000000..8df8381
--- /dev/null
+++ b/tests/src/replicant_tests.conf
@@ -0,0 +1,24 @@
+[DEFAULT]
+
+[replicant-builder]
+machine = ssh://lxc-f2a85m-pro-debian-local
+replicant_version = 6.0
+replicant_directory = replicant-6.0
+releases_directory = None
+manifest_url = git://git.replicant.us/GNUtoo/manifest.git
+manifest_branch = replicant-6.0-dev
+
+[replicant-installer]
+machine = None
+replicant_version = 6.0
+replicant_directory = ~/work/reference/replicant/replicant-6.0
+releases_directory = ~/work/releases/replicant/images
+manifest_url = git://git.replicant.us/GNUtoo/manifest.git
+manifest_branch = replicant-6.0-dev
+
+[fdroid-installer]
+machine = None
+replicant_version = 6.0
+releases_directory = ~/work/releases/f-droid
+manifest_url = git://git.replicant.us/GNUtoo/manifest.git
+manifest_branch = replicant-6.0-dev
diff --git a/tests/src/test-replicant-6.0-0004-rc5-migration.py b/tests/src/test-replicant-6.0-0004-rc5-migration.py
new file mode 100755
index 0000000..c9f0aac
--- /dev/null
+++ b/tests/src/test-replicant-6.0-0004-rc5-migration.py
@@ -0,0 +1,88 @@
+#!/usr/bin/env python3
+# Copyright (C) 2020 Denis 'GNUtoo' Carikli <GNUtoo@cyberdimension.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+import os, sys
+
+from lib.common import enable_trace
+# enable_trace(True)
+
+from lib.config import ReplicantConfig
+from lib.replicant_releases import *
+from lib.fdroid import *
+
+config = ReplicantConfig()
+
+def usage(progname):
+ print('Usage: {} <device>'.format(progname))
+ print()
+ print('Example: {} i9300'.format(progname))
+ sys.exit(1)
+
+if __name__ == '__main__':
+ if len(sys.argv) != 2:
+ usage(sys.argv[0])
+
+ device = sys.argv[1]
+
+ # Install Replicant 6.0 0003 with some APKs
+ apks = ['ch.blinkenlights.android.vanilla_10850.apk',
+ 'com.termoneplus_351.apk',
+ 'org.pocketworkstation.pckeyboard_1041001.apk',
+ 'org.smssecure.smssecure_145.apk']
+
+ major_version = '6.0'
+ minor_version = '0003'
+
+ install_replicant_release(config.get('replicant-installer'),
+ major_version,
+ minor_version,
+ device,
+ wipe_cache_and_data=True,
+ disable_modem=False,
+ quiet=True)
+
+ print("[ OK ] Installed Replicant {} {}".format(major_version,
+ minor_version))
+
+ for apk in apks:
+ install_fdroid_apk(config.get('fdroid-installer'), apk, quiet=True)
+
+ print("[ OK ] Installed APKs on top of Replicant {} {}".format(
+ major_version, minor_version))
+
+ # Install Replicant 6.0 0004 RC5
+ major_version = '6.0'
+ minor_version = '0004-rc5-transition'
+ install_replicant_release(config.get('replicant-installer'),
+ major_version,
+ minor_version,
+ device,
+ wipe_cache_and_data=False,
+ disable_modem=False,
+ quiet=True)
+ print("[ OK ] Installed Replicant {} {}".format(major_version,
+ minor_version))
+
+ major_version = '6.0'
+ minor_version = '0004-rc5'
+ install_replicant_release(config.get('replicant-installer'),
+ major_version,
+ minor_version,
+ device,
+ wipe_cache_and_data=False,
+ disable_modem=False,
+ quiet=True)
+ print("[ OK ] Installed Replicant {} {}".format(major_version,
+ minor_version))