diff --git a/tests/integration/framework/docker_runner.py b/tests/integration/framework/docker_runner.py index c46a535..784d879 100644 --- a/tests/integration/framework/docker_runner.py +++ b/tests/integration/framework/docker_runner.py @@ -8,49 +8,90 @@ import sys class DockerRunner: - """Run docker containers for testing""" + """ + Interface with the Docker cli to build & launch `docker run` and + `docker exec` commands. + - `docker run` is used for a one shot container. + - `docker exec` enable a test to use the same background container for + multiples commands, launched automatically at first exec instruction. + """ def __init__(self, platform, image, verbose): """Sets platform and image for all tests ran with this instance""" self.platform = platform self.image = image self.verbose = verbose + #Background container for `docker exec` + self.container_id = None - def construct_docker_command(self, envs, args): - """ - Construct a docker command with env and args - """ - command = ["docker", "run", "--platform", self.platform] + def execute(self, envs, args): + """Launch `docker exec` commands inside the background container""" + # Create background container if not existing on first exec command + if self.container_id is None: + self._start_background() - for env in envs: - command.append("-e") - command.append(env) + command = self._construct_command("exec", envs, args) + return self._shell(command) - command.append(self.image) + def run(self, envs, args): + """Launch `docker run` commands, create a new container each time""" + command = self._construct_command("run", envs, args) + return self._shell(command) - for arg in args: - command.append(arg) + def __del__(self): + """Clean up background container if enabled""" + self._stop_background() - return command + def _start_background(self): + """Start a background container to run `exec` commands inside""" + create_command = [ + "docker", "run", "-d", + "--platform", self.platform, + self.image, + "sleep", "infinity", + ] + self.container_id = self._shell(create_command, silent=True)[:16] - def run_interactive_command(self, envs, args): - """ - Run our target docker image with a list of - environment variables and a list of arguments - """ - command = self.construct_docker_command(envs, args) + def _stop_background(self): + """Remove background test container if used""" + stop_command = ["docker", "rm", "-f", self.container_id] + if self.container_id: + self._shell(stop_command, silent=True) - if self.verbose: - print(f"Running command: { ' '.join(command) }") + def _shell(self, command, silent=False): + """Run an arbitrary shell command and return its output""" + if self.verbose and not silent: + print(f"$ { ' '.join(command) }") try: - output = subprocess.run(command, capture_output=True, check=True) - except subprocess.CalledProcessError as docker_err: - print(f"Error while running command: { ' '.join(command) }", file=sys.stderr) - print(docker_err, file=sys.stderr) - print(docker_err.stderr.decode("utf-8"), file=sys.stderr) - print(docker_err.stdout.decode("utf-8"), file=sys.stdout) + result = subprocess.run(command, capture_output=True, check=True) + except subprocess.CalledProcessError as command_err: + print(command_err.stdout.decode("utf-8"), file=sys.stdout) + print(command_err.stderr.decode("utf-8"), file=sys.stderr) + raise command_err - raise docker_err + return result.stdout.decode("utf-8").strip() - return output + def _construct_command(self, docker_cmd, envs, args): + """Construct a docker command with env and args""" + command = ["docker", docker_cmd] + + for env in envs: + command.append("-e") + command.append(env) + + # Use container or image depending on command type + if docker_cmd == "exec": + # Use entrypoint.py to enter `docker exec` + command.extend([self.container_id, "entrypoint.py"]) + + # Need to add the default executable added by CMD not given by + # exec, entrypoint will crash without CMD default argument. + if len(args) == 0 or args[0].startswith("-"): + command.append("dogecoind") + + elif docker_cmd == "run": + command.extend(["--platform", self.platform, self.image]) + + command.extend(args) + return command diff --git a/tests/integration/framework/test_runner.py b/tests/integration/framework/test_runner.py index daa2906..01d0931 100644 --- a/tests/integration/framework/test_runner.py +++ b/tests/integration/framework/test_runner.py @@ -15,8 +15,8 @@ class TestConfigurationError(Exception): class TestRunner: """Base class to define and run Dogecoin Core Docker tests with""" def __init__(self): - """Make sure there is an options object""" self.options = {} + self.docker_cli = None def add_options(self, parser): """Allow adding options in tests""" @@ -25,15 +25,22 @@ def run_test(self): """Actual test, must be implemented by the final class""" raise NotImplementedError - def run_command(self, envs, args): - """Run a docker command with env and args""" + def docker_exec(self, envs, args): + """ + Launch `docker exec` command, run command inside a background container. + Let execute mutliple instructions in the same container. + """ assert self.options.platform is not None assert self.options.image is not None - runner = DockerRunner(self.options.platform, - self.options.image, self.options.verbose) + return self.docker_cli.execute(envs, args) - return runner.run_interactive_command(envs, args) + def docker_run(self, envs, args): + """Launch `docker run` command, create a new container for each run""" + assert self.options.platform is not None + assert self.options.image is not None + + return self.docker_cli.run(envs, args) def main(self): """main loop""" @@ -48,6 +55,9 @@ def main(self): self.add_options(parser) self.options = parser.parse_args() + self.docker_cli = DockerRunner(self.options.platform, + self.options.image, self.options.verbose) + self.run_test() print("Tests successful") sys.exit(0) diff --git a/tests/integration/version.py b/tests/integration/version.py index b8be289..aa2d52e 100644 --- a/tests/integration/version.py +++ b/tests/integration/version.py @@ -27,21 +27,21 @@ def run_test(self): self.version_expr = re.compile(f".*{ self.options.version }.*") # check dogecoind with only env - dogecoind = self.run_command(["VERSION=1"], []) - self.ensure_version_on_first_line(dogecoind.stdout) + dogecoind = self.docker_run(["VERSION=1"], []) + self.ensure_version_on_first_line(dogecoind) # check dogecoin-cli - dogecoincli = self.run_command([], ["dogecoin-cli", "-?"]) - self.ensure_version_on_first_line(dogecoincli.stdout) + dogecoincli = self.docker_run([], ["dogecoin-cli", "-?"]) + self.ensure_version_on_first_line(dogecoincli) # check dogecoin-tx - dogecointx = self.run_command([], ["dogecoin-tx", "-?"]) - self.ensure_version_on_first_line(dogecointx.stdout) + dogecointx = self.docker_run([], ["dogecoin-tx", "-?"]) + self.ensure_version_on_first_line(dogecointx) # make sure that we find version errors caught_error = False try: - self.ensure_version_on_first_line("no version here".encode('utf-8')) + self.ensure_version_on_first_line("no version here") except AssertionError: caught_error = True @@ -50,7 +50,7 @@ def run_test(self): def ensure_version_on_first_line(self, cmd_output): """Assert that the version is contained in the first line of output string""" - first_line = cmd_output.decode("utf-8").split("\n")[0] + first_line = cmd_output.split("\n")[0] if re.match(self.version_expr, first_line) is None: text = f"Could not find version { self.options.version } in { first_line }"