diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 43f75edc..0b919a41 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -31,6 +31,17 @@ jobs: make cd - + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.8 + + - name: Install test dependencies + run: | + sudo apt-get install libgirepository1.0-dev python3-virtualenv + python -m pip install --upgrade pip + pip install -r test-requirements.txt + - name: Login to DockerHub uses: docker/login-action@v1 if: github.ref == 'refs/heads/master' @@ -46,7 +57,7 @@ jobs: - name: Run test suite run: | ./test/wait-for-hawkbit-online - cd test && ./rauc-hawkbit-updater.t -v + dbus-run-session -- pytest -v docs: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 4022e10b..02fa8d86 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,7 @@ build/ # Backup files done by uncrustify .uncrustify/ *~ + +# tests +venv/ +test/__pycache__/ diff --git a/README.md b/README.md index 301de9c5..8944397d 100644 --- a/README.md +++ b/README.md @@ -81,8 +81,35 @@ Compile cd build cmake .. make + cd .. ``` +Test Suite +---------- + +Prepare test suite: + +```shell +virtualenv -p python3 venv +source venv/bin/activate +python -m pip install --upgrade pip +pip install -r test-requirements.txt +``` + +Run hawkBit docker container: + +```shell +docker pull hawkbit/hawkbit-update-server +docker run -d --name hawkbit -p 8080:8080 hawkbit/hawkbit-update-server +``` + +Run test suite: + +```shell +./test/wait-for-hawkbit-online && dbus-run-session -- pytest -v +``` + +Pass `-o log_cli=true` to pytest in order to enable live logging for all test cases. Usage / options --------------- diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..a1c8600c --- /dev/null +++ b/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +log_file_level = INFO +log_format = %(levelname)s %(name)s %(message)s +addopts = --show-capture=log -rs diff --git a/test-requirements.txt b/test-requirements.txt index 19adc5db..a8a29cc8 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,3 +1,4 @@ +pytest attrs requests pydbus diff --git a/test/conftest.py b/test/conftest.py new file mode 100644 index 00000000..adadc425 --- /dev/null +++ b/test/conftest.py @@ -0,0 +1,105 @@ +# SPDX-License-Identifier: LGPL-2.1-only +# SPDX-FileCopyrightText: 2021 Enrico Jörns , Pengutronix +# SPDX-FileCopyrightText: 2021 Bastian Krause , Pengutronix + +import os +from configparser import ConfigParser + +import pytest + +from hawkbit_mgmt import HawkbitMgmtTestClient, HawkbitError + +def pytest_addoption(parser): + """Register custom argparse-style options.""" + parser.addoption( + '--hawkbit-instance', + help='HOST:PORT of hawkBit instance to use (default: %(default)s)', + default='localhost:8080') + +@pytest.fixture(scope='session') +def hawkbit(pytestconfig): + """Instance of HawkbitMgmtTestClient connecting to a hawkBit instance.""" + from uuid import uuid4 + + host, port = pytestconfig.option.hawkbit_instance.split(':') + client = HawkbitMgmtTestClient(host, int(port)) + + client.set_config('pollingTime', '00:00:30') + client.set_config('pollingOverdueTime', '00:03:00') + client.set_config('authentication.targettoken.enabled', True) + client.set_config('authentication.gatewaytoken.enabled', True) + client.set_config('authentication.gatewaytoken.key', uuid4().hex) + + return client + +@pytest.fixture +def hawkbit_target_added(hawkbit): + """Creates a hawkBit target.""" + target = hawkbit.add_target() + yield target + + hawkbit.delete_target(target) + +@pytest.fixture +def config(tmp_path, hawkbit, hawkbit_target_added): + """ + Creates a temporary rauc-hawkbit-updater configuration matching the hawkBit (target) + configuration of the hawkbit and hawkbit_target_added fixtures. + """ + target = hawkbit.get_target() + target_token = target.get('securityToken') + target_name = target.get('name') + bundle_location = tmp_path / 'bundle.raucb' + + hawkbit_config = ConfigParser() + hawkbit_config['client'] = { + 'hawkbit_server': f'{hawkbit.host}:{hawkbit.port}', + 'ssl': 'false', + 'ssl_verify': 'false', + 'tenant_id': 'DEFAULT', + 'target_name': target_name, + 'auth_token': target_token, + 'bundle_download_location': str(bundle_location), + 'retry_wait': '60', + 'connect_timeout': '20', + 'timeout': '60', + 'log_level': 'debug', + } + hawkbit_config['device'] = { + 'product': 'Terminator', + 'model': 'T-1000', + 'serialnumber': '8922673153', + 'hw_revision': '2', + 'mac_address': 'ff:ff:ff:ff:ff:ff', + } + + tmp_config = tmp_path / 'rauc-hawkbit-updater.conf' + with tmp_config.open('w') as f: + hawkbit_config.write(f) + return tmp_config + +@pytest.fixture +def adjust_config(config): + """ + Adjusts the rauc-hawkbit-updater configuration created by the config fixture by + adding/overwriting or removing options. + """ + config_files = [] + def _adjust_config(options={'client': {}}, remove={}): + adjusted_config = ConfigParser() + adjusted_config.read(config) + + # update + for section, option in options.items(): + for key, value in option.items(): + adjusted_config.set(section, key, value) + + # remove + for section, option in remove.items(): + adjusted_config.remove_option(section, option) + + with config.open('w') as f: + adjusted_config.write(f) + return config + + return _adjust_config diff --git a/test/hawkbit_mgmt.py b/test/hawkbit_mgmt.py new file mode 120000 index 00000000..f1952a9c --- /dev/null +++ b/test/hawkbit_mgmt.py @@ -0,0 +1 @@ +../script/hawkbit_mgmt.py \ No newline at end of file diff --git a/test/helper.py b/test/helper.py new file mode 100644 index 00000000..19b1ca55 --- /dev/null +++ b/test/helper.py @@ -0,0 +1,38 @@ +# SPDX-License-Identifier: LGPL-2.1-only +# SPDX-FileCopyrightText: 2021 Enrico Jörns , Pengutronix +# SPDX-FileCopyrightText: 2021 Bastian Krause , Pengutronix + +import os +import subprocess +import shlex +import logging + + +def run(command, *, timeout=30): + """ + Runs given command as subprocess with DBUS_STARTER_BUS_TYPE=session and PATH+=./build. Blocks + until command terminates. Logs command (with updated env) and its stdout/stderr/exit code. + Returns tuple (stdout, stderr, exit code). + """ + env = os.environ.copy() + env.update({'DBUS_STARTER_BUS_TYPE': 'session'}) + env['PATH'] += f':{os.path.dirname(os.path.abspath(__file__))}/../build' + + logger = logging.getLogger(command.split()[0]) + + log_env = [ f'{key}={value}' for key, value in set(env.items()) - set(os.environ.items()) ] + logger.info('running: %s %s', ' '.join(log_env), command) + + proc = subprocess.run(shlex.split(command), capture_output=True, text=True, check=False, + env=env, timeout=timeout) + + for line in proc.stdout.splitlines(): + if line: + logger.info('stdout: %s', line) + for line in proc.stderr.splitlines(): + if line: + logger.warning('stderr: %s', line) + + logger.info('exitcode: %d', proc.returncode) + + return proc.stdout, proc.stderr, proc.returncode diff --git a/test/test_basics.py b/test/test_basics.py new file mode 100644 index 00000000..08256a94 --- /dev/null +++ b/test/test_basics.py @@ -0,0 +1,122 @@ +# SPDX-License-Identifier: LGPL-2.1-only +# SPDX-FileCopyrightText: 2021 Enrico Jörns , Pengutronix +# SPDX-FileCopyrightText: 2021 Bastian Krause , Pengutronix + +from configparser import ConfigParser + +from helper import run + +def test_version(): + """Test version argument.""" + out, err, exitcode = run('rauc-hawkbit-updater -v') + + assert exitcode == 0 + assert out.startswith('Version ') + assert err == '' + +def test_invalid_arg(): + """Test invalid argument.""" + out, err, exitcode = run('rauc-hawkbit-updater --invalidarg') + + assert exitcode == 1 + assert out == '' + assert err.strip() == 'option parsing failed: Unknown option --invalidarg' + +def test_config_unspecified(): + """Test call without config argument.""" + out, err, exitcode = run('rauc-hawkbit-updater') + + assert exitcode == 2 + assert out == '' + assert err.strip() == 'No configuration file given' + +def test_config_file_non_existent(): + """Test call with inexistent config file.""" + out, err, exitcode = run('rauc-hawkbit-updater -c does-not-exist.conf') + + assert exitcode == 3 + assert out == '' + assert err.strip() == 'No such configuration file: does-not-exist.conf' + +def test_config_no_auth_token(adjust_config): + """Test config without auth_token option in client section.""" + config = adjust_config(remove={'client': 'auth_token'}) + + out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r') + + assert exitcode == 4 + assert out == '' + assert err.strip() == \ + 'Loading config file failed: Neither auth_token nor gateway_token is set in the config.' + +def test_config_multiple_auth_methods(adjust_config): + """Test config with auth_token and gateway_token options in client section.""" + config = adjust_config({'client': {'gateway_token': 'wrong-gateway-token'}}) + + out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r') + + assert exitcode == 4 + assert out == '' + assert err.strip() == \ + 'Loading config file failed: Both auth_token and gateway_token are set in the config.' + +def test_register_and_check_invalid_gateway_token(adjust_config): + """Test config with invalid gateway_token.""" + config = adjust_config( + {'client': {'gateway_token': 'wrong-gateway-token'}}, + remove={'client': 'auth_token'} + ) + + out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r') + + assert exitcode == 1 + assert 'MESSAGE: Checking for new software...' in out + assert err.strip() == 'WARNING: Failed to authenticate. Check if gateway_token is correct?' + +def test_register_and_check_valid_gateway_token(hawkbit, adjust_config): + """Test config with valid gateway_token.""" + gateway_token = hawkbit.get_config('authentication.gatewaytoken.key') + config = adjust_config( + {'client': {'gateway_token': gateway_token}}, + remove={'client': 'auth_token'} + ) + + out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r') + + assert exitcode == 0 + assert 'MESSAGE: Checking for new software...' in out + assert err == '' + +def test_register_and_check_invalid_auth_token(adjust_config): + """Test config with invalid auth_token.""" + config = adjust_config({'client': {'auth_token': 'wrong-auth-token'}}) + + out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r') + + assert exitcode == 1 + assert 'MESSAGE: Checking for new software...' in out + assert err.strip() == 'WARNING: Failed to authenticate. Check if auth_token is correct?' + +def test_register_and_check_valid_auth_token(config): + """Test config with valid auth_token.""" + out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r') + + assert exitcode == 0 + assert 'MESSAGE: Checking for new software...' in out + assert err == '' + +def test_identify(hawkbit, config): + """ + Test that supplying target meta information works and information are received correctly by + hawkBit. + """ + out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r') + + assert exitcode == 0 + assert 'Providing meta information to hawkbit server' in out + assert err == '' + + ref_config = ConfigParser() + ref_config.read(config) + + assert dict(ref_config.items('device')) == hawkbit.get_attributes()