diff --git a/README.md b/README.md index 6f73515..451cc52 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -[![GitHub Actions CI/CD build status — Collection test suite](https://github.com/ansible-collection-migration/hetzner.hcloud/workflows/Collection%20test%20suite/badge.svg?branch=master)](https://github.com/ansible-collection-migration/hetzner.hcloud/actions?query=workflow%3A%22Collection%20test%20suite%22) +[![Run Status](https://api.shippable.com/projects/5e66776c8b17a60007e4c277/badge?branch=master)]() Ansible Collection: hetzner.hcloud -================================================= \ No newline at end of file +================================================= + +Ansible Hetzner Cloud Collection for controlling your Hetzner Cloud Resources. diff --git a/plugins/modules/hcloud_route.py b/plugins/modules/hcloud_route.py index b5fe3b2..25ede26 100644 --- a/plugins/modules/hcloud_route.py +++ b/plugins/modules/hcloud_route.py @@ -172,8 +172,8 @@ class AnsibleHcloudRoute(Hcloud): return AnsibleModule( argument_spec=dict( network={"type": "str", "required": True}, - destination={"type": "str", "required": True}, gateway={"type": "str", "required": True}, + destination={"type": "str", "required": True}, state={ "choices": ["absent", "present"], "default": "present", diff --git a/shippable.yml b/shippable.yml new file mode 100644 index 0000000..9e8572e --- /dev/null +++ b/shippable.yml @@ -0,0 +1,35 @@ +language: python + +env: + matrix: + - T=none + +matrix: + exclude: + - env: T=none + include: + - env: T=sanity/1 + - env: T=sanity/2 + - env: T=sanity/3 + - env: T=sanity/4 + + - env: T=hcloud/3.8/1 + - env: T=hcloud/3.8/2 + +branches: + except: + - "*-patch-*" + - "revert-*-*" + +build: + ci: + - tests/utils/shippable/timing.sh tests/utils/shippable/shippable.sh $T + +integrations: + notifications: + - integrationName: email + type: email + on_success: never + on_failure: never + on_start: never + on_pull_request: never diff --git a/tests/integration/targets/hcloud_datacenter_info/meta/main.yml b/tests/integration/targets/hcloud_datacenter_info/meta/main.yml new file mode 100644 index 0000000..407c901 --- /dev/null +++ b/tests/integration/targets/hcloud_datacenter_info/meta/main.yml @@ -0,0 +1,3 @@ +collections: + - community.general.ipfilter + - hetzner.cloud diff --git a/tests/integration/targets/hcloud_floating_ip/meta/main.yml b/tests/integration/targets/hcloud_floating_ip/meta/main.yml new file mode 100644 index 0000000..407c901 --- /dev/null +++ b/tests/integration/targets/hcloud_floating_ip/meta/main.yml @@ -0,0 +1,3 @@ +collections: + - community.general.ipfilter + - hetzner.cloud diff --git a/tests/integration/targets/hcloud_floating_ip/tasks/main.yml b/tests/integration/targets/hcloud_floating_ip/tasks/main.yml index 7b1590a..eb48ab4 100644 --- a/tests/integration/targets/hcloud_floating_ip/tasks/main.yml +++ b/tests/integration/targets/hcloud_floating_ip/tasks/main.yml @@ -381,4 +381,4 @@ - name: verify cleanup another server assert: that: - - result is changed \ No newline at end of file + - result is changed diff --git a/tests/integration/targets/hcloud_floating_ip_info/meta/main.yml b/tests/integration/targets/hcloud_floating_ip_info/meta/main.yml new file mode 100644 index 0000000..407c901 --- /dev/null +++ b/tests/integration/targets/hcloud_floating_ip_info/meta/main.yml @@ -0,0 +1,3 @@ +collections: + - community.general.ipfilter + - hetzner.cloud diff --git a/tests/integration/targets/hcloud_image_info/meta/main.yml b/tests/integration/targets/hcloud_image_info/meta/main.yml new file mode 100644 index 0000000..407c901 --- /dev/null +++ b/tests/integration/targets/hcloud_image_info/meta/main.yml @@ -0,0 +1,3 @@ +collections: + - community.general.ipfilter + - hetzner.cloud diff --git a/tests/integration/targets/hcloud_location_info/meta/main.yml b/tests/integration/targets/hcloud_location_info/meta/main.yml new file mode 100644 index 0000000..407c901 --- /dev/null +++ b/tests/integration/targets/hcloud_location_info/meta/main.yml @@ -0,0 +1,3 @@ +collections: + - community.general.ipfilter + - hetzner.cloud diff --git a/tests/integration/targets/hcloud_network/meta/main.yml b/tests/integration/targets/hcloud_network/meta/main.yml new file mode 100644 index 0000000..407c901 --- /dev/null +++ b/tests/integration/targets/hcloud_network/meta/main.yml @@ -0,0 +1,3 @@ +collections: + - community.general.ipfilter + - hetzner.cloud diff --git a/tests/integration/targets/hcloud_network_info/meta/main.yml b/tests/integration/targets/hcloud_network_info/meta/main.yml new file mode 100644 index 0000000..407c901 --- /dev/null +++ b/tests/integration/targets/hcloud_network_info/meta/main.yml @@ -0,0 +1,3 @@ +collections: + - community.general.ipfilter + - hetzner.cloud diff --git a/tests/integration/targets/hcloud_rdns/meta/main.yml b/tests/integration/targets/hcloud_rdns/meta/main.yml new file mode 100644 index 0000000..67d54d7 --- /dev/null +++ b/tests/integration/targets/hcloud_rdns/meta/main.yml @@ -0,0 +1,3 @@ +collections: + - ansible.netcommon + - hetzner.cloud diff --git a/tests/integration/targets/hcloud_rdns/tasks/main.yml b/tests/integration/targets/hcloud_rdns/tasks/main.yml index b3b7e4e..23b6842 100644 --- a/tests/integration/targets/hcloud_rdns/tasks/main.yml +++ b/tests/integration/targets/hcloud_rdns/tasks/main.yml @@ -26,23 +26,10 @@ - result is failed - 'result.msg == "missing required arguments: ip_address, server" or result.msg == "missing required arguments: server, ip_address"' -- name: test missing required parameters on create - hcloud_rdns: - server: "{{ hcloud_server_name }}" - ip_address: "{{ setup.hcloud_server.ipv6 | ipaddr('next_usable') }}" - state: present - register: result - ignore_errors: yes -- name: verify fail test missing required parameters on create - assert: - that: - - result is failed - - 'result.msg == "missing required arguments: dns_ptr"' - - name: test create rdns with checkmode hcloud_rdns: server: "{{ hcloud_server_name }}" - ip_address: "{{ setup.hcloud_server.ipv6 | ipaddr('next_usable') }}" + ip_address: "{{ setup.hcloud_server.ipv6 | ansible.netcommon.ipaddr('next_usable') }}" dns_ptr: "example.com" state: present register: result @@ -55,7 +42,7 @@ - name: test create rdns hcloud_rdns: server: "{{ hcloud_server_name }}" - ip_address: "{{ setup.hcloud_server.ipv6 | ipaddr('next_usable') }}" + ip_address: "{{ setup.hcloud_server.ipv6 | ansible.netcommon.ipaddr('next_usable') }}" dns_ptr: "example.com" state: present register: rdns @@ -64,13 +51,13 @@ that: - rdns is changed - rdns.hcloud_rdns.server == "{{ hcloud_server_name }}" - - rdns.hcloud_rdns.ip_address == "{{ setup.hcloud_server.ipv6 | ipaddr('next_usable') }}" + - rdns.hcloud_rdns.ip_address == "{{ setup.hcloud_server.ipv6 | ansible.netcommon.ipaddr('next_usable') }}" - rdns.hcloud_rdns.dns_ptr == "example.com" - name: test create rdns idempotency hcloud_rdns: server: "{{ hcloud_server_name }}" - ip_address: "{{ setup.hcloud_server.ipv6 | ipaddr('next_usable') }}" + ip_address: "{{ setup.hcloud_server.ipv6 | ansible.netcommon.ipaddr('next_usable') }}" dns_ptr: "example.com" state: present register: result @@ -82,7 +69,7 @@ - name: test absent rdns hcloud_rdns: server: "{{ hcloud_server_name }}" - ip_address: "{{ setup.hcloud_server.ipv6 | ipaddr('next_usable') }}" + ip_address: "{{ setup.hcloud_server.ipv6 | ansible.netcommon.ipaddr('next_usable') }}" state: absent register: result - name: verify test absent rdns diff --git a/tests/integration/targets/hcloud_route/meta/main.yml b/tests/integration/targets/hcloud_route/meta/main.yml new file mode 100644 index 0000000..67d54d7 --- /dev/null +++ b/tests/integration/targets/hcloud_route/meta/main.yml @@ -0,0 +1,3 @@ +collections: + - ansible.netcommon + - hetzner.cloud diff --git a/tests/integration/targets/hcloud_route/tasks/main.yml b/tests/integration/targets/hcloud_route/tasks/main.yml index 1894d67..7d816bf 100644 --- a/tests/integration/targets/hcloud_route/tasks/main.yml +++ b/tests/integration/targets/hcloud_route/tasks/main.yml @@ -21,7 +21,7 @@ assert: that: - result is failed - - 'result.msg == "missing required arguments: network, destination, gateway"' + - 'result.msg == "missing required arguments: destination, gateway, network"' - name: test create route with checkmode hcloud_route: diff --git a/tests/integration/targets/hcloud_server/meta/main.yml b/tests/integration/targets/hcloud_server/meta/main.yml new file mode 100644 index 0000000..407c901 --- /dev/null +++ b/tests/integration/targets/hcloud_server/meta/main.yml @@ -0,0 +1,3 @@ +collections: + - community.general.ipfilter + - hetzner.cloud diff --git a/tests/integration/targets/hcloud_server_info/meta/main.yml b/tests/integration/targets/hcloud_server_info/meta/main.yml new file mode 100644 index 0000000..407c901 --- /dev/null +++ b/tests/integration/targets/hcloud_server_info/meta/main.yml @@ -0,0 +1,3 @@ +collections: + - community.general.ipfilter + - hetzner.cloud diff --git a/tests/integration/targets/hcloud_server_network/meta/main.yml b/tests/integration/targets/hcloud_server_network/meta/main.yml new file mode 100644 index 0000000..407c901 --- /dev/null +++ b/tests/integration/targets/hcloud_server_network/meta/main.yml @@ -0,0 +1,3 @@ +collections: + - community.general.ipfilter + - hetzner.cloud diff --git a/tests/integration/targets/hcloud_server_type_info/meta/main.yml b/tests/integration/targets/hcloud_server_type_info/meta/main.yml new file mode 100644 index 0000000..407c901 --- /dev/null +++ b/tests/integration/targets/hcloud_server_type_info/meta/main.yml @@ -0,0 +1,3 @@ +collections: + - community.general.ipfilter + - hetzner.cloud diff --git a/tests/integration/targets/hcloud_ssh_key/meta/main.yml b/tests/integration/targets/hcloud_ssh_key/meta/main.yml index 7e4c5b7..5dcc072 100644 --- a/tests/integration/targets/hcloud_ssh_key/meta/main.yml +++ b/tests/integration/targets/hcloud_ssh_key/meta/main.yml @@ -1,2 +1,5 @@ dependencies: - setup_sshkey +collections: + - community.general.ipfilter + - hetzner.cloud diff --git a/tests/integration/targets/hcloud_ssh_key_info/meta/main.yml b/tests/integration/targets/hcloud_ssh_key_info/meta/main.yml index 7e4c5b7..5dcc072 100644 --- a/tests/integration/targets/hcloud_ssh_key_info/meta/main.yml +++ b/tests/integration/targets/hcloud_ssh_key_info/meta/main.yml @@ -1,2 +1,5 @@ dependencies: - setup_sshkey +collections: + - community.general.ipfilter + - hetzner.cloud diff --git a/tests/integration/targets/hcloud_subnetwork/meta/main.yml b/tests/integration/targets/hcloud_subnetwork/meta/main.yml new file mode 100644 index 0000000..407c901 --- /dev/null +++ b/tests/integration/targets/hcloud_subnetwork/meta/main.yml @@ -0,0 +1,3 @@ +collections: + - community.general.ipfilter + - hetzner.cloud diff --git a/tests/integration/targets/hcloud_subnetwork/tasks/main.yml b/tests/integration/targets/hcloud_subnetwork/tasks/main.yml index f973471..06bcf83 100644 --- a/tests/integration/targets/hcloud_subnetwork/tasks/main.yml +++ b/tests/integration/targets/hcloud_subnetwork/tasks/main.yml @@ -22,7 +22,7 @@ assert: that: - result is failed - - 'result.msg == "missing required arguments: network_zone, type, ip_range"' + - 'result.msg == "missing required arguments: ip_range, network_zone, type"' - name: test create subnetwork with checkmode hcloud_subnetwork: diff --git a/tests/integration/targets/hcloud_volume/meta/main.yml b/tests/integration/targets/hcloud_volume/meta/main.yml new file mode 100644 index 0000000..407c901 --- /dev/null +++ b/tests/integration/targets/hcloud_volume/meta/main.yml @@ -0,0 +1,3 @@ +collections: + - community.general.ipfilter + - hetzner.cloud diff --git a/tests/integration/targets/hcloud_volume_info/meta/main.yml b/tests/integration/targets/hcloud_volume_info/meta/main.yml new file mode 100644 index 0000000..407c901 --- /dev/null +++ b/tests/integration/targets/hcloud_volume_info/meta/main.yml @@ -0,0 +1,3 @@ +collections: + - community.general.ipfilter + - hetzner.cloud diff --git a/tests/requirements.yml b/tests/requirements.yml new file mode 100644 index 0000000..6c49d0d --- /dev/null +++ b/tests/requirements.yml @@ -0,0 +1,3 @@ +integration_tests_dependencies: +- community.general +- ansible.netcommon diff --git a/tests/utils/shippable/check_matrix.py b/tests/utils/shippable/check_matrix.py new file mode 100755 index 0000000..dfcca3e --- /dev/null +++ b/tests/utils/shippable/check_matrix.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python +"""Verify the currently executing Shippable test matrix matches the one defined in the "shippable.yml" file.""" +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import datetime +import json +import os +import re +import sys +import time + +try: + from typing import NoReturn +except ImportError: + NoReturn = None + +try: + # noinspection PyCompatibility + from urllib2 import urlopen # pylint: disable=ansible-bad-import-from +except ImportError: + # noinspection PyCompatibility + from urllib.request import urlopen + + +def main(): # type: () -> None + """Main entry point.""" + repo_full_name = os.environ['REPO_FULL_NAME'] + required_repo_full_name = 'ansible-collections/hetzner.hcloud' + + if repo_full_name != required_repo_full_name: + sys.stderr.write('Skipping matrix check on repo "%s" which is not "%s".\n' % (repo_full_name, required_repo_full_name)) + return + + with open('shippable.yml', 'rb') as yaml_file: + yaml = yaml_file.read().decode('utf-8').splitlines() + + defined_matrix = [match.group(1) for match in [re.search(r'^ *- env: T=(.*)$', line) for line in yaml] if match and match.group(1) != 'none'] + + if not defined_matrix: + fail('No matrix entries found in the "shippable.yml" file.', + 'Did you modify the "shippable.yml" file?') + + run_id = os.environ['SHIPPABLE_BUILD_ID'] + sleep = 1 + jobs = [] + + for attempts_remaining in range(4, -1, -1): + try: + jobs = json.loads(urlopen('https://api.shippable.com/jobs?runIds=%s' % run_id).read()) + + if not isinstance(jobs, list): + raise Exception('Shippable run %s data is not a list.' % run_id) + + break + except Exception as ex: + if not attempts_remaining: + fail('Unable to retrieve Shippable run %s matrix.' % run_id, + str(ex)) + + sys.stderr.write('Unable to retrieve Shippable run %s matrix: %s\n' % (run_id, ex)) + sys.stderr.write('Trying again in %d seconds...\n' % sleep) + time.sleep(sleep) + sleep *= 2 + + if len(jobs) != len(defined_matrix): + if len(jobs) == 1: + hint = '\n\nMake sure you do not use the "Rebuild with SSH" option.' + else: + hint = '' + + fail('Shippable run %s has %d jobs instead of the expected %d jobs.' % (run_id, len(jobs), len(defined_matrix)), + 'Try re-running the entire matrix.%s' % hint) + + actual_matrix = dict((job.get('jobNumber'), dict(tuple(line.split('=', 1)) for line in job.get('env', [])).get('T', '')) for job in jobs) + errors = [(job_number, test, actual_matrix.get(job_number)) for job_number, test in enumerate(defined_matrix, 1) if actual_matrix.get(job_number) != test] + + if len(errors): + error_summary = '\n'.join('Job %s expected "%s" but found "%s" instead.' % (job_number, expected, actual) for job_number, expected, actual in errors) + + fail('Shippable run %s has a job matrix mismatch.' % run_id, + 'Try re-running the entire matrix.\n\n%s' % error_summary) + + +def fail(message, output): # type: (str, str) -> NoReturn + # Include a leading newline to improve readability on Shippable "Tests" tab. + # Without this, the first line becomes indented. + output = '\n' + output.strip() + + timestamp = datetime.datetime.utcnow().replace(microsecond=0).isoformat() + + # hack to avoid requiring junit-xml, which isn't pre-installed on Shippable outside our test containers + xml = ''' + + +\t +\t\t +\t\t\t%s +\t\t +\t + +''' % (timestamp, message, output) + + path = 'shippable/testresults/check-matrix.xml' + dir_path = os.path.dirname(path) + + if not os.path.exists(dir_path): + os.makedirs(dir_path) + + with open(path, 'w') as junit_fd: + junit_fd.write(xml.lstrip()) + + sys.stderr.write(message + '\n') + sys.stderr.write(output + '\n') + + sys.exit(1) + + +if __name__ == '__main__': + main() diff --git a/tests/utils/shippable/hcloud.sh b/tests/utils/shippable/hcloud.sh new file mode 100755 index 0000000..da037e0 --- /dev/null +++ b/tests/utils/shippable/hcloud.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +set -o pipefail -eux + +declare -a args +IFS='/:' read -ra args <<< "$1" + +cloud="${args[0]}" +python="${args[1]}" +group="${args[2]}" + +target="shippable/${cloud}/group${group}/" + +stage="${S:-prod}" + +changed_all_target="shippable/${cloud}/smoketest/" + +if ! ansible-test integration "${changed_all_target}" --list-targets > /dev/null 2>&1; then + # no smoketest tests are available for this cloud + changed_all_target="none" +fi + +if [ "${group}" == "1" ]; then + # only run smoketest tests for group1 + changed_all_mode="include" +else + # smoketest tests already covered by group1 + changed_all_mode="exclude" +fi + +# shellcheck disable=SC2086 +ansible-test integration --color -v --retry-on-error "${target}" ${COVERAGE:+"$COVERAGE"} ${CHANGED:+"$CHANGED"} ${UNSTABLE:+"$UNSTABLE"} \ + --remote-terminate always --remote-stage "${stage}" \ + --docker --python "${python}" --changed-all-target "${changed_all_target}" --changed-all-mode "${changed_all_mode}" diff --git a/tests/utils/shippable/sanity.sh b/tests/utils/shippable/sanity.sh new file mode 100755 index 0000000..a52030c --- /dev/null +++ b/tests/utils/shippable/sanity.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +set -o pipefail -eux + +declare -a args +IFS='/:' read -ra args <<< "$1" + +group="${args[1]}" + +if [ "${BASE_BRANCH:-}" ]; then + base_branch="origin/${BASE_BRANCH}" +else + base_branch="" +fi + +case "${group}" in + 1) options=(--skip-test pylint --skip-test ansible-doc --skip-test validate-modules) ;; + 2) options=( --test ansible-doc --test validate-modules) ;; + 3) options=(--test pylint plugins/modules/) ;; + 4) options=(--test pylint --exclude plugins/modules/) ;; +esac + +# allow collection migration sanity tests for groups 3 and 4 to pass without updating this script during migration +network_path="lib/ansible/modules/network/" + +if [ -d "${network_path}" ]; then + if [ "${group}" -eq 3 ]; then + options+=(--exclude "${network_path}") + elif [ "${group}" -eq 4 ]; then + options+=("${network_path}") + fi +fi + +# shellcheck disable=SC2086 +ansible-test sanity --color -v --junit ${COVERAGE:+"$COVERAGE"} ${CHANGED:+"$CHANGED"} \ + --docker --base-branch "${base_branch}" \ + --exclude shippable.yml --exclude tests/utils/ \ + "${options[@]}" --allow-disabled diff --git a/tests/utils/shippable/shippable.sh b/tests/utils/shippable/shippable.sh new file mode 100755 index 0000000..866fd6c --- /dev/null +++ b/tests/utils/shippable/shippable.sh @@ -0,0 +1,113 @@ +#!/usr/bin/env bash + +set -o pipefail -eux + +declare -a args +IFS='/:' read -ra args <<< "$1" + +script="${args[0]}" + +test="$1" + +docker images ansible/ansible +docker images quay.io/ansible/* +docker ps + +for container in $(docker ps --format '{{.Image}} {{.ID}}' | grep -v '^drydock/' | sed 's/^.* //'); do + docker rm -f "${container}" || true # ignore errors +done + +docker ps + +if [ -d /home/shippable/cache/ ]; then + ls -la /home/shippable/cache/ +fi + +command -v python +python -V + +function retry +{ + for repetition in 1 2 3; do + set +e + "$@" + result=$? + set -e + if [ ${result} == 0 ]; then + return ${result} + fi + echo "$@ -> ${result}" + done + echo "Command '$@' failed 3 times!" + exit -1 +} + +command -v pip +pip --version +pip list --disable-pip-version-check +retry pip install https://github.com/ansible/ansible/archive/devel.tar.gz --disable-pip-version-check + +export ANSIBLE_COLLECTIONS_PATHS="${HOME}/.ansible" +SHIPPABLE_RESULT_DIR="$(pwd)/shippable" +TEST_DIR="${ANSIBLE_COLLECTIONS_PATHS}/ansible_collections/hetzner/hcloud" +mkdir -p "${TEST_DIR}" +cp -aT "${SHIPPABLE_BUILD_DIR}" "${TEST_DIR}" +cd "${TEST_DIR}" + +# STAR: HACK install dependencies +retry ansible-galaxy -vvv collection install community.general +retry ansible-galaxy -vvv collection install ansible.netcommon + +retry pip install hcloud +# END: HACK + +export PYTHONIOENCODING='utf-8' + +if [ "${JOB_TRIGGERED_BY_NAME:-}" == "nightly-trigger" ]; then + COMPLETE=yes +fi + + +if [ -n "${COMPLETE:-}" ]; then + # disable change detection triggered by setting the COMPLETE environment variable to a non-empty value + export CHANGED="" +elif [[ "${COMMIT_MESSAGE}" =~ ci_complete ]]; then + # disable change detection triggered by having 'ci_complete' in the latest commit message + export CHANGED="" +else + # enable change detection (default behavior) + export CHANGED="--changed" +fi + +if [ "${IS_PULL_REQUEST:-}" == "true" ]; then + # run unstable tests which are targeted by focused changes on PRs + export UNSTABLE="--allow-unstable-changed" +else + # do not run unstable tests outside PRs + export UNSTABLE="" +fi + +# remove empty core/extras module directories from PRs created prior to the repo-merge +find plugins -type d -empty -print -delete + +function cleanup +{ + if [ -d tests/output/junit/ ]; then + cp -aT tests/output/junit/ "$SHIPPABLE_RESULT_DIR/testresults/" + fi + + if [ -d tests/output/data/ ]; then + cp -a tests/output/data/ "$SHIPPABLE_RESULT_DIR/testresults/" + fi + + if [ -d tests/output/bot/ ]; then + cp -aT tests/output/bot/ "$SHIPPABLE_RESULT_DIR/testresults/" + fi +} + +trap cleanup EXIT + +ansible-test env --dump --show --timeout "50" --color -v + +"tests/utils/shippable/check_matrix.py" +"tests/utils/shippable/${script}.sh" "${test}" diff --git a/tests/utils/shippable/timing.py b/tests/utils/shippable/timing.py new file mode 100755 index 0000000..fb53827 --- /dev/null +++ b/tests/utils/shippable/timing.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3.7 +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import sys +import time + +start = time.time() + +sys.stdin.reconfigure(errors='surrogateescape') +sys.stdout.reconfigure(errors='surrogateescape') + +for line in sys.stdin: + seconds = time.time() - start + sys.stdout.write('%02d:%02d %s' % (seconds // 60, seconds % 60, line)) + sys.stdout.flush() diff --git a/tests/utils/shippable/timing.sh b/tests/utils/shippable/timing.sh new file mode 100755 index 0000000..77e2578 --- /dev/null +++ b/tests/utils/shippable/timing.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +set -o pipefail -eu + +"$@" 2>&1 | "$(dirname "$0")/timing.py"