diff --git a/.gitignore b/.gitignore index 80db74b..26848f9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.pyc .idea -/volume/deluge* \ No newline at end of file +/volume/deluge* +*.timestamp \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..537c5e1 --- /dev/null +++ b/Makefile @@ -0,0 +1,62 @@ +SHELL := bash + +.SHELLFLAGS := -eu -o pipefail -c + +.PHONY: test-unit-local \ + start-local-integration-harness \ + stop-local-integration-harness \ + test-integration-local \ + test-all-local \ + test-unit-docker \ + test-integration-docker \ + clean + +# Runs the unit tests locally. +unit: + poetry run pytest -m "not integration" + +# Starts the local integration harness. This is required for running pytest with the "integration" marker. +harness-start: + rm -rf ${PWD}/volume/deluge-{1,2,3} + docker compose -f docker-compose.local.yaml up + +# Stops the local integration harness. +harness-stop: + docker compose -f docker-compose.local.yaml down --volumes --remove-orphans + +# Runs the integration tests locally. This requires the integration harness to be running. +integration: + echo "NOTE: Make sure to have started the integration harness or this will not work" + poetry run pytest -m "integration" + +tests: unit integration + +docker/.lastbuilt-test.timestamp: docker/bittorrent-benchmarks.Dockerfile + docker build -t bittorrent-benchmarks:test -f ./docker/bittorrent-benchmarks.Dockerfile . + touch docker/.lastbuilt-test.timestamp + +docker/.lastbuilt-release.timestamp: docker/bittorrent-benchmarks.Dockerfile + docker build -t bittorrent-benchmarks:test --build-arg BUILD_TYPE="release" \ + -f ./docker/bittorrent-benchmarks.Dockerfile . + touch docker/.lastbuilt-release.timestamp + +# Builds the test image required for local dockerized integration tests. +image-test: docker/.lastbuilt-test.timestamp +image-release: docker/.lastbuilt-release.timestamp + +# Runs the unit tests in a docker container. +unit-docker: image-test + docker run --entrypoint poetry --rm bittorrent-benchmarks:test run pytest -m "not integration" + +# Runs the integration tests in a docker container. +integration-docker: image-test + docker compose -f docker-compose.local.yaml -f docker-compose.ci.yaml down --volumes --remove-orphans + docker compose -f docker-compose.local.yaml -f docker-compose.ci.yaml up \ + --abort-on-container-exit --exit-code-from test-runner + +tests-docker: unit-docker integration-docker + +clean: + rm -rf docker/.lastbuilt* + rm -rf volume/deluge-{1,2,3} + docker compose -f docker-compose.local.yaml -f docker-compose.ci.yaml down --volumes --rmi all --remove-orphans diff --git a/benchmarks/core/experiments/experiments.py b/benchmarks/core/experiments/experiments.py index 62487aa..5de9ab1 100644 --- a/benchmarks/core/experiments/experiments.py +++ b/benchmarks/core/experiments/experiments.py @@ -3,7 +3,8 @@ import logging from abc import ABC, abstractmethod from collections.abc import Iterable -from typing import Optional +from time import time, sleep +from typing import Optional, List from typing_extensions import Generic, TypeVar @@ -44,17 +45,30 @@ def __init__(self, components: Iterable[ExperimentComponent], polling_interval: def await_ready(self, timeout: float = 0) -> bool: """Awaits for all components to be ready, or until a timeout is reached.""" - # TODO we should probably have per-component timeouts, or at least provide feedback - # as to what was the completion state of each component. - if not await_predicate( - lambda: all(component.is_ready() for component in self.components), - timeout=timeout, - polling_interval=self.polling_interval, - ): - return False + + start_time = time() + not_ready = [component for component in self.components] + + logging.info(f'Awaiting for components to be ready: {self._component_names(not_ready)}') + while len(not_ready) != 0: + for component in not_ready: + if component.is_ready(): + logger.info(f'Component {str(component)} is ready.') + not_ready.remove(component) + + sleep(self.polling_interval) + + if (timeout != 0) and (time() - start_time > timeout): + print("timeout") + logger.info(f'Some components timed out: {self._component_names(not_ready)}') + return False return True + @staticmethod + def _component_names(components: List[ExperimentComponent]) -> str: + return ', '.join(str(component) for component in components) + def run(self, experiment: Experiment): """Runs the :class:`Experiment` within this :class:`ExperimentEnvironment`.""" if not self.await_ready(): diff --git a/benchmarks/core/experiments/tests/test_experiments.py b/benchmarks/core/experiments/tests/test_experiments.py index 3126c00..a3deba3 100644 --- a/benchmarks/core/experiments/tests/test_experiments.py +++ b/benchmarks/core/experiments/tests/test_experiments.py @@ -44,10 +44,12 @@ def test_should_timeout_if_component_takes_too_long(): ] environment = ExperimentEnvironment(components, polling_interval=0) - assert not environment.await_ready(0.1) + assert not environment.await_ready(0.09) - assert components[0].iteration == 5 - assert components[1].iteration < 3 + # Because ExperimentEnvironment sweeps through the components, it will + # iterate exactly once before timing out. + assert components[0].iteration == 1 + assert components[1].iteration == 1 class ExperimentThatReliesOnComponents(Experiment): diff --git a/docker-compose-up.sh b/docker-compose-up.sh deleted file mode 100755 index fa6e680..0000000 --- a/docker-compose-up.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash -set -e - -# These have to be wiped out before we boot the containers. Note that this will only work -# if you've set up rootless Docker. -rm -rf ./volume/{deluge-1,deluge-2,deluge-3} -docker compose up \ No newline at end of file diff --git a/docker/bin/run-tests.sh b/docker/bin/run-tests.sh index 5b1ce99..3fdbf65 100755 --- a/docker/bin/run-tests.sh +++ b/docker/bin/run-tests.sh @@ -14,4 +14,3 @@ touch /opt/bittorrent-benchmarks/volume/.initialized echo "Launching tests." cd /opt/bittorrent-benchmarks poetry run pytest --exitfirst - diff --git a/docker/bittorrent-benchmarks.Dockerfile b/docker/bittorrent-benchmarks.Dockerfile index 5048530..f242a4d 100644 --- a/docker/bittorrent-benchmarks.Dockerfile +++ b/docker/bittorrent-benchmarks.Dockerfile @@ -1,27 +1,22 @@ FROM python:3.12-slim -ARG UID=1000 -ARG GID=1000 ARG BUILD_TYPE="test" -RUN groupadd -g ${GID} runner \ - && useradd -u ${UID} -g ${GID} -s /bin/bash -m runner -RUN mkdir /opt/bittorrent-benchmarks && chown -R runner:runner /opt/bittorrent-benchmarks RUN pip install poetry -USER runner +RUN mkdir /opt/bittorrent-benchmarks WORKDIR /opt/bittorrent-benchmarks -COPY --chown=runner:runner pyproject.toml poetry.lock ./ -RUN if [ "$BUILD_TYPE" = "production" ]; then \ - echo "Image is a production build"; \ +COPY pyproject.toml poetry.lock ./ +RUN if [ "$BUILD_TYPE" = "release" ]; then \ + echo "Image is a release build"; \ poetry install --only main --no-root; \ else \ echo "Image is a test build"; \ poetry install --no-root; \ fi -COPY --chown=runner:runner . . +COPY . . RUN poetry install --only main ENTRYPOINT ["poetry", "run", "bittorrent-benchmarks", "/opt/bittorrent-benchmarks/experiments.yaml"]