Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DockerRunner: Enable background container to run commands against #51

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
99 changes: 70 additions & 29 deletions tests/integration/framework/docker_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you please bring this method back so that the tests don't have to change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to #51 (comment)

"""
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
22 changes: 16 additions & 6 deletions tests/integration/framework/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand All @@ -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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please restore this so that version test doesn't have to change

"""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"""
Expand All @@ -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)
16 changes: 8 additions & 8 deletions tests/integration/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"], [])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please revert all changes to this file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using self.docker_run would be fine for you ?
Also, methods TestRunner.docker_run and TestRunner.docker_exec return directly the output to avoid the use of .stdout access & encoding/decoding stuff. Error messages are directly handled on command execution (DockerRunner._shell) and create test failure, not used in tests (at least for now). You would like to restore this also ?

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

Expand All @@ -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 }"
Expand Down