From 02a90745619db181a8d22c328dfec7508dce024b Mon Sep 17 00:00:00 2001 From: AbcSxyZ Date: Fri, 17 Dec 2021 15:08:23 +0100 Subject: [PATCH 1/9] DockerRunner: Enable background container to run commands against --- tests/integration/framework/docker_runner.py | 90 ++++++++++++++------ tests/integration/framework/test_runner.py | 10 +-- tests/integration/version.py | 10 +-- 3 files changed, 72 insertions(+), 38 deletions(-) diff --git a/tests/integration/framework/docker_runner.py b/tests/integration/framework/docker_runner.py index c46a535..1b6fee9 100644 --- a/tests/integration/framework/docker_runner.py +++ b/tests/integration/framework/docker_runner.py @@ -8,49 +8,83 @@ import sys class DockerRunner: - """Run docker containers for testing""" + """Run a docker container in background to execute testing commands""" 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 + self.container_id = None - def construct_docker_command(self, envs, args): + # Launch the container + self._start() + + def execute(self, envs, args): """ - Construct a docker command with env and args + Run our target docker image with a list of + environment variables and a list of arguments """ - command = ["docker", "run", "--platform", self.platform] - - for env in envs: - command.append("-e") - command.append(env) - - command.append(self.image) - - for arg in args: - command.append(arg) + command = self._construct_command(envs, args) + return self._shell(command) - return command + def __del__(self): + """Remove test container""" + stop_command = ["docker", "rm", "-f", self.container_id] + self._shell(stop_command, silent=True) - def run_interactive_command(self, envs, args): + def _start(self): """ - Run our target docker image with a list of - environment variables and a list of arguments + Launch a docker container. Done in 2 step using create + start. + + Keep the container running with a shell open thanks to `--interactive`, + having stdin open keep the process running. """ - command = self.construct_docker_command(envs, args) + create_command = [ + "docker", "create", + "--platform", self.platform, + "--interactive", + self.image, + "/bin/bash", + ] + self.container_id = self._shell(create_command, silent=True) + + start_command = [ + "docker", "start", self.container_id + ] + self._shell(start_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 + + return result.stdout.decode("utf-8").strip() + + def _construct_command(self, envs, args): + """Construct a docker command with env and args""" + command = ["docker", "exec"] + + for env in envs: + command.append("-e") + command.append(env) + + command.append(self.container_id) - raise docker_err + # Launch all shell commands using entrypoint.py only + command.append("entrypoint.py") - return output + # Need to add a default executables added by CMD normally + if len(args) == 0 or args[0].startswith("-"): + command.append("dogecoind") + + command.extend(args) + + return command diff --git a/tests/integration/framework/test_runner.py b/tests/integration/framework/test_runner.py index daa2906..d45f072 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.container = None def add_options(self, parser): """Allow adding options in tests""" @@ -30,10 +30,7 @@ def run_command(self, envs, args): 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 runner.run_interactive_command(envs, args) + return self.container.execute(envs, args) def main(self): """main loop""" @@ -48,6 +45,9 @@ def main(self): self.add_options(parser) self.options = parser.parse_args() + self.container = 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..59b24a5 100644 --- a/tests/integration/version.py +++ b/tests/integration/version.py @@ -28,20 +28,20 @@ def run_test(self): # check dogecoind with only env dogecoind = self.run_command(["VERSION=1"], []) - self.ensure_version_on_first_line(dogecoind.stdout) + self.ensure_version_on_first_line(dogecoind) # check dogecoin-cli dogecoincli = self.run_command([], ["dogecoin-cli", "-?"]) - self.ensure_version_on_first_line(dogecoincli.stdout) + self.ensure_version_on_first_line(dogecoincli) # check dogecoin-tx dogecointx = self.run_command([], ["dogecoin-tx", "-?"]) - self.ensure_version_on_first_line(dogecointx.stdout) + 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 }" From 84b4f913cfeadd6ddce19d0d4ad14e1d380a2b97 Mon Sep 17 00:00:00 2001 From: AbcSxyZ Date: Sat, 18 Dec 2021 20:23:17 +0100 Subject: [PATCH 2/9] Restore run commands to DockerRunner --- tests/integration/framework/docker_runner.py | 14 +++++++++----- tests/integration/framework/test_runner.py | 14 ++++++++++++-- tests/integration/version.py | 6 +++--- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/tests/integration/framework/docker_runner.py b/tests/integration/framework/docker_runner.py index 1b6fee9..a4e36fe 100644 --- a/tests/integration/framework/docker_runner.py +++ b/tests/integration/framework/docker_runner.py @@ -21,11 +21,15 @@ def __init__(self, platform, image, verbose): self._start() def execute(self, envs, args): + """Launch `docker exec` commands inside the background container""" + command = self._construct_command("exec", envs, args) + return self._shell(command) + + def run(self, envs, args): """ - Run our target docker image with a list of - environment variables and a list of arguments + Launch `docker run` commands to create a new container for each command """ - command = self._construct_command(envs, args) + command = self._construct_command("run", envs, args) return self._shell(command) def __del__(self): @@ -68,9 +72,9 @@ def _shell(self, command, silent=False): return result.stdout.decode("utf-8").strip() - def _construct_command(self, envs, args): + def _construct_command(self, docker_cmd, envs, args): """Construct a docker command with env and args""" - command = ["docker", "exec"] + command = ["docker", docker_cmd] for env in envs: command.append("-e") diff --git a/tests/integration/framework/test_runner.py b/tests/integration/framework/test_runner.py index d45f072..3bd3206 100644 --- a/tests/integration/framework/test_runner.py +++ b/tests/integration/framework/test_runner.py @@ -25,13 +25,23 @@ 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 return self.container.execute(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.container.run(envs, args) + def main(self): """main loop""" parser = argparse.ArgumentParser() diff --git a/tests/integration/version.py b/tests/integration/version.py index 59b24a5..68e86c6 100644 --- a/tests/integration/version.py +++ b/tests/integration/version.py @@ -27,15 +27,15 @@ def run_test(self): self.version_expr = re.compile(f".*{ self.options.version }.*") # check dogecoind with only env - dogecoind = self.run_command(["VERSION=1"], []) + dogecoind = self.docker_exec(["VERSION=1"], []) self.ensure_version_on_first_line(dogecoind) # check dogecoin-cli - dogecoincli = self.run_command([], ["dogecoin-cli", "-?"]) + dogecoincli = self.docker_exec([], ["dogecoin-cli", "-?"]) self.ensure_version_on_first_line(dogecoincli) # check dogecoin-tx - dogecointx = self.run_command([], ["dogecoin-tx", "-?"]) + dogecointx = self.docker_exec([], ["dogecoin-tx", "-?"]) self.ensure_version_on_first_line(dogecointx) # make sure that we find version errors From eba0eb9be6bee3a8c61a2a6f01da7a138530509f Mon Sep 17 00:00:00 2001 From: AbcSxyZ Date: Sun, 19 Dec 2021 00:24:37 +0100 Subject: [PATCH 3/9] test: Fix docker run commands for DockerRunner --- tests/integration/framework/docker_runner.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/integration/framework/docker_runner.py b/tests/integration/framework/docker_runner.py index a4e36fe..e448a35 100644 --- a/tests/integration/framework/docker_runner.py +++ b/tests/integration/framework/docker_runner.py @@ -80,7 +80,13 @@ def _construct_command(self, docker_cmd, envs, args): command.append("-e") command.append(env) - command.append(self.container_id) + # Use container or image depending on command type + if docker_cmd == "exec": + tag = self.container_id + elif docker_cmd == "run": + tag = self.image + + command.append(tag) # Launch all shell commands using entrypoint.py only command.append("entrypoint.py") From b8d21714ede9e2b1ef61c908c3b9f226daf17cf3 Mon Sep 17 00:00:00 2001 From: AbcSxyZ Date: Sun, 19 Dec 2021 17:34:52 +0100 Subject: [PATCH 4/9] test: update background container to be optional --- tests/integration/framework/docker_runner.py | 31 ++++++++++++++------ tests/integration/framework/test_runner.py | 8 ++--- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/tests/integration/framework/docker_runner.py b/tests/integration/framework/docker_runner.py index e448a35..9e31371 100644 --- a/tests/integration/framework/docker_runner.py +++ b/tests/integration/framework/docker_runner.py @@ -8,20 +8,28 @@ import sys class DockerRunner: - """Run a docker container in background to execute testing commands""" - + """ + 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 - # Launch the container - self._start() - 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() + command = self._construct_command("exec", envs, args) return self._shell(command) @@ -33,11 +41,10 @@ def run(self, envs, args): return self._shell(command) def __del__(self): - """Remove test container""" - stop_command = ["docker", "rm", "-f", self.container_id] - self._shell(stop_command, silent=True) + """Clean up background container if enabled""" + self._stop_background() - def _start(self): + def _start_background(self): """ Launch a docker container. Done in 2 step using create + start. @@ -58,6 +65,12 @@ def _start(self): ] self._shell(start_command, silent=True) + 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) + def _shell(self, command, silent=False): """Run an arbitrary shell command and return its output""" if self.verbose and not silent: diff --git a/tests/integration/framework/test_runner.py b/tests/integration/framework/test_runner.py index 3bd3206..01d0931 100644 --- a/tests/integration/framework/test_runner.py +++ b/tests/integration/framework/test_runner.py @@ -16,7 +16,7 @@ class TestRunner: """Base class to define and run Dogecoin Core Docker tests with""" def __init__(self): self.options = {} - self.container = None + self.docker_cli = None def add_options(self, parser): """Allow adding options in tests""" @@ -33,14 +33,14 @@ def docker_exec(self, envs, args): assert self.options.platform is not None assert self.options.image is not None - return self.container.execute(envs, args) + return self.docker_cli.execute(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.container.run(envs, args) + return self.docker_cli.run(envs, args) def main(self): """main loop""" @@ -55,7 +55,7 @@ def main(self): self.add_options(parser) self.options = parser.parse_args() - self.container = DockerRunner(self.options.platform, + self.docker_cli = DockerRunner(self.options.platform, self.options.image, self.options.verbose) self.run_test() From 9b3ac80def363e1279cc141f8b212fcd59d57b6a Mon Sep 17 00:00:00 2001 From: AbcSxyZ Date: Sun, 19 Dec 2021 17:35:20 +0100 Subject: [PATCH 5/9] update version tests to docker_run --- tests/integration/version.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integration/version.py b/tests/integration/version.py index 68e86c6..aa2d52e 100644 --- a/tests/integration/version.py +++ b/tests/integration/version.py @@ -27,15 +27,15 @@ def run_test(self): self.version_expr = re.compile(f".*{ self.options.version }.*") # check dogecoind with only env - dogecoind = self.docker_exec(["VERSION=1"], []) + dogecoind = self.docker_run(["VERSION=1"], []) self.ensure_version_on_first_line(dogecoind) # check dogecoin-cli - dogecoincli = self.docker_exec([], ["dogecoin-cli", "-?"]) + dogecoincli = self.docker_run([], ["dogecoin-cli", "-?"]) self.ensure_version_on_first_line(dogecoincli) # check dogecoin-tx - dogecointx = self.docker_exec([], ["dogecoin-tx", "-?"]) + dogecointx = self.docker_run([], ["dogecoin-tx", "-?"]) self.ensure_version_on_first_line(dogecointx) # make sure that we find version errors From eb89b6c2bf58de9cecd5593376c1003529ef9afd Mon Sep 17 00:00:00 2001 From: AbcSxyZ Date: Sun, 19 Dec 2021 17:44:14 +0100 Subject: [PATCH 6/9] Use sleep infinity to start background container --- tests/integration/framework/docker_runner.py | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/tests/integration/framework/docker_runner.py b/tests/integration/framework/docker_runner.py index 9e31371..5d74f84 100644 --- a/tests/integration/framework/docker_runner.py +++ b/tests/integration/framework/docker_runner.py @@ -45,26 +45,15 @@ def __del__(self): self._stop_background() def _start_background(self): - """ - Launch a docker container. Done in 2 step using create + start. - - Keep the container running with a shell open thanks to `--interactive`, - having stdin open keep the process running. - """ + """Start a background container to run `exec` commands inside""" create_command = [ - "docker", "create", + "docker", "run", "-d", "--platform", self.platform, - "--interactive", self.image, - "/bin/bash", + "sleep", "infinity", ] self.container_id = self._shell(create_command, silent=True) - start_command = [ - "docker", "start", self.container_id - ] - self._shell(start_command, silent=True) - def _stop_background(self): """Remove background test container if used""" stop_command = ["docker", "rm", "-f", self.container_id] From 62a40d5a230f8a69bf66c886d3130c7989e31b2e Mon Sep 17 00:00:00 2001 From: AbcSxyZ Date: Sun, 19 Dec 2021 17:44:52 +0100 Subject: [PATCH 7/9] improve doc --- tests/integration/framework/docker_runner.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/integration/framework/docker_runner.py b/tests/integration/framework/docker_runner.py index 5d74f84..49e02f0 100644 --- a/tests/integration/framework/docker_runner.py +++ b/tests/integration/framework/docker_runner.py @@ -34,9 +34,7 @@ def execute(self, envs, args): return self._shell(command) def run(self, envs, args): - """ - Launch `docker run` commands to create a new container for each command - """ + """Launch `docker run` commands, create a new container each time""" command = self._construct_command("run", envs, args) return self._shell(command) From 775eec6c5442538bc901fa2058db694c7133e31b Mon Sep 17 00:00:00 2001 From: AbcSxyZ Date: Sun, 19 Dec 2021 19:18:51 +0100 Subject: [PATCH 8/9] fix platform for docker run + change run commands to match real behavior --- tests/integration/framework/docker_runner.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/tests/integration/framework/docker_runner.py b/tests/integration/framework/docker_runner.py index 49e02f0..19966cf 100644 --- a/tests/integration/framework/docker_runner.py +++ b/tests/integration/framework/docker_runner.py @@ -82,19 +82,16 @@ def _construct_command(self, docker_cmd, envs, args): # Use container or image depending on command type if docker_cmd == "exec": - tag = self.container_id - elif docker_cmd == "run": - tag = self.image - - command.append(tag) + # Use entrypoint.py to enter `docker exec` + command.extend([self.container_id, "entrypoint.py"]) - # Launch all shell commands using entrypoint.py only - command.append("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") - # Need to add a default executables added by CMD normally - 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 From 407dbac2f239753055fb5547fdc0bd4ce33cfe00 Mon Sep 17 00:00:00 2001 From: AbcSxyZ Date: Sun, 19 Dec 2021 19:19:08 +0100 Subject: [PATCH 9/9] reduce hash size of container_id --- tests/integration/framework/docker_runner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/framework/docker_runner.py b/tests/integration/framework/docker_runner.py index 19966cf..784d879 100644 --- a/tests/integration/framework/docker_runner.py +++ b/tests/integration/framework/docker_runner.py @@ -50,7 +50,7 @@ def _start_background(self): self.image, "sleep", "infinity", ] - self.container_id = self._shell(create_command, silent=True) + self.container_id = self._shell(create_command, silent=True)[:16] def _stop_background(self): """Remove background test container if used"""