repo2docker/tests/conftest.py

284 wiersze
8.8 KiB
Python

"""
Custom test collector for our integration tests.
Test lifecycle:
- Find all directories that contain `verify` or `*.repos.yaml`
- If `verify` is found:
- Run `jupyter-repo2docker` on the test directory.
- Extra arguments may be added as YAML list of strings in `extra-args.yaml`.
- Run `./verify` inside the built container.
- It should return a non-zero exit code for the test to be considered a
successful.
- If a `*.repos.yaml` is found:
- For each entry of the form `{name, url, ref, verify}`
- Run `jupyter-repo2docker` with the `url` and `ref`
- Run the `verify` inside the built container
"""
import os
import pipes
import shlex
import subprocess
import time
from tempfile import TemporaryDirectory
import escapism
import pytest
import requests
import yaml
from repo2docker.__main__ import make_r2d
TESTS_DIR = os.path.abspath(os.path.dirname(__file__))
def pytest_collect_file(parent, file_path):
if file_path.name == "verify":
return LocalRepo.from_parent(parent, path=file_path)
elif file_path.name.endswith(".repos.yaml"):
return RemoteRepoList.from_parent(parent, path=file_path)
def make_test_func(args, skip_build=False, extra_run_kwargs=None):
"""Generate a test function that runs repo2docker"""
def test():
app = make_r2d(args)
app.initialize()
if extra_run_kwargs:
app.extra_run_kwargs.update(extra_run_kwargs)
if skip_build:
def build_noop():
print("Skipping build")
app.skip_build = build_noop
if app.run_cmd:
# verify test, run it
app.start()
return
# no run_cmd given, starting notebook server
app.run = False
app.start() # This just build the image and does not run it.
container = app.start_container()
port = app.port
# wait a bit for the container to be ready
container_url = f"http://localhost:{port}/api"
# give the container a chance to start
time.sleep(1)
try:
# try a few times to connect
success = False
for i in range(1, 4):
container.reload()
assert container.status == "running"
try:
info = requests.get(container_url).json()
except Exception as e:
print(f"Error: {e}")
time.sleep(i * 3)
else:
print(info)
success = True
break
assert success, f"Notebook never started in {container}"
finally:
# stop the container
container.stop()
app.wait_for_container(container)
return test
# Provide a fixture for testing in .py files
@pytest.fixture()
def run_repo2docker():
def run_test(args):
return make_test_func(args)()
return run_test
@pytest.fixture()
def base_image():
"""
Base ubuntu image to use when testing specific BuildPacks
"""
return "buildpack-deps:jammy"
def _add_content_to_git(repo_dir):
"""Add content to file 'test' in git repository and commit."""
# use append mode so this can be called multiple times
with open(os.path.join(repo_dir, "test"), "a") as f:
f.write("Hello")
subprocess.check_call(["git", "add", "test"], cwd=repo_dir)
subprocess.check_call(["git", "commit", "-m", "Test commit"], cwd=repo_dir)
def _get_sha1(repo_dir):
"""Get repository's current commit SHA1."""
sha1 = subprocess.Popen(
["git", "rev-parse", "HEAD"], stdout=subprocess.PIPE, cwd=repo_dir
)
return sha1.stdout.read().decode().strip()
@pytest.fixture()
def git_repo():
"""
Make a dummy git repo in which user can perform git operations
Should be used as a contextmanager, it will delete directory when done
"""
with TemporaryDirectory() as gitdir:
subprocess.check_call(["git", "init"], cwd=gitdir)
yield gitdir
@pytest.fixture()
def repo_with_content(git_repo):
"""Create a git repository with content"""
_add_content_to_git(git_repo)
sha1 = _get_sha1(git_repo)
yield git_repo, sha1
@pytest.fixture
def repo_with_submodule():
"""Create a git repository with a git submodule in a non-master branch.
Provides parent repository directory, current parent commit SHA1, and
the submodule's current commit. The submodule will be added under the
name "submod" in the parent repository.
Creating the submodule in a separate branch is useful to prove that
submodules are initialized properly when the master branch doesn't have
the submodule yet.
"""
submodule_repo = "https://github.com/binderhub-ci-repos/requirements"
submod_sha1_b = "20c4fe55a9b2c5011d228545e821b1c7b1723652"
with TemporaryDirectory() as git_a_dir:
# create "parent" repository
subprocess.check_call(["git", "init"], cwd=git_a_dir)
_add_content_to_git(git_a_dir)
# create a new branch in the parent without any submodule
subprocess.check_call(
["git", "checkout", "-b", "branch-without-submod"], cwd=git_a_dir
)
# create a new branch in the parent to add the submodule
subprocess.check_call(
["git", "checkout", "-b", "branch-with-submod"], cwd=git_a_dir
)
subprocess.check_call(
[
"git",
"submodule",
"add",
submodule_repo,
"submod",
],
cwd=git_a_dir,
)
# checkout the first commit for the submod, not the latest
subprocess.check_call(
["git", "checkout", submod_sha1_b], cwd=os.path.join(git_a_dir, "submod")
)
subprocess.check_call(["git", "add", git_a_dir, ".gitmodules"], cwd=git_a_dir)
subprocess.check_call(
["git", "commit", "-m", "Add B repos submod"], cwd=git_a_dir
)
sha1_a = _get_sha1(git_a_dir)
yield git_a_dir, sha1_a, submod_sha1_b
class Repo2DockerTest(pytest.Function):
"""A pytest.Item for running repo2docker"""
def __init__(
self, name, parent, args=None, skip_build=False, extra_run_kwargs=None
):
self.args = args
self.save_cwd = os.getcwd()
f = parent.obj = make_test_func(
args, skip_build=skip_build, extra_run_kwargs=extra_run_kwargs
)
super().__init__(name, parent, callobj=f)
def reportinfo(self):
return (self.parent.path, None, "")
def repr_failure(self, excinfo):
err = excinfo.value
if isinstance(err, SystemExit):
cmd = f'jupyter-repo2docker {" ".join(map(pipes.quote, self.args))}'
return f"{cmd} | exited with status={err.code}"
else:
return super().repr_failure(excinfo)
def teardown(self):
super().teardown()
os.chdir(self.save_cwd)
class LocalRepo(pytest.File):
def collect(self):
args = ["--appendix", 'RUN echo "appendix" > /tmp/appendix']
# If there's an extra-args.yaml file in a test dir, assume it contains
# a yaml list with extra arguments to be passed to repo2docker
extra_args_path = self.path.parent / "test-extra-args.yaml"
if extra_args_path.exists():
extra_args = yaml.safe_load(extra_args_path.read_text())
args += extra_args
print(self.path.name, self.path.parent, str(self.path))
# re-use image name for multiple tests of the same image
# so we don't run through the build twice
rel_repo_dir = os.path.relpath(self.path.parent, TESTS_DIR)
image_name = f"r2d-tests-{escapism.escape(rel_repo_dir, escape_char='-').lower()}-{int(time.time())}"
args.append(f"--image-name={image_name}")
args.append(str(self.path.parent))
yield Repo2DockerTest.from_parent(self, name="build", args=args)
yield Repo2DockerTest.from_parent(
self,
name=self.path.name,
args=args + ["./verify"],
skip_build=True,
)
# mount the tests dir as a volume
check_tmp_args = (
args[:-1]
+ ["--volume", f"{TESTS_DIR}:/io/tests"]
+ [args[-1], "/io/tests/check-tmp"]
)
yield Repo2DockerTest.from_parent(
self,
name="check-tmp",
args=check_tmp_args,
skip_build=True,
extra_run_kwargs={"user": "root"},
)
class RemoteRepoList(pytest.File):
def collect(self):
with self.path.open() as f:
repos = yaml.safe_load(f)
for repo in repos:
args = []
if "ref" in repo:
args += ["--ref", repo["ref"]]
args += [repo["url"], "--"] + shlex.split(repo["verify"])
yield Repo2DockerTest.from_parent(self, name=repo["name"], args=args)