Skip to content

Commit 9f8c3ae

Browse files
Test Command (#11)
* Updated _image.py to a new version * Added the "Test" command * Fixed most of the test failures, probably not a final fix * Documentation fixes per PR + Exposed `default_user` to the CLI for `dropin` command ~ Changed `test` to mount to the parent of the given logfile instead of a hard-coded location ~ Changed `logfile` under `test` to `output_xml` ~ Changed some imports and type annotations to fit `__future__.annotations` * Updated lockfiles to avoid hdf5 1.14.2 dependency * Added the test test, fixed test, tested test (success!) * Testing container mount moved from `/temporary/Testing` to `/scratch/Testing`. --------- Co-authored-by: tyler-g-hudson <[email protected]>
1 parent 71b98e3 commit 9f8c3ae

File tree

7 files changed

+412
-237
lines changed

7 files changed

+412
-237
lines changed

env_files/lock-dev.txt

Lines changed: 119 additions & 103 deletions
Large diffs are not rendered by default.

env_files/lock-runtime.txt

Lines changed: 118 additions & 102 deletions
Large diffs are not rendered by default.

test/test_docker_cmake.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import re
22
from pathlib import Path
33
from subprocess import PIPE
4+
from tempfile import TemporaryDirectory
45
from typing import Dict, Iterator, Tuple
56

67
from pytest import fixture, mark
@@ -11,6 +12,7 @@
1112
cmake_config_dockerfile,
1213
cmake_install_dockerfile,
1314
)
15+
from wigwam.commands import test as command_test
1416

1517
from .utils import (
1618
determine_scope,
@@ -289,3 +291,20 @@ def test_cmake_install_image(
289291
"""
290292
isce3_cmake_install_image.run(command='python -c "import isce3"')
291293
isce3_cmake_install_image.run(command='python -c "import nisar"')
294+
295+
@mark.isce3
296+
@mark.slow
297+
def test_test_command(
298+
self,
299+
isce3_cmake_install_tag: str,
300+
isce3_cmake_install_image: Image, # type: ignore
301+
):
302+
with TemporaryDirectory() as temp_dir:
303+
output_xml = Path(temp_dir) / "temp.xml"
304+
command_test(
305+
tag=isce3_cmake_install_tag,
306+
output_xml=output_xml,
307+
compress_output=False,
308+
quiet_fail=False,
309+
)
310+
assert output_xml.is_file()

wigwam/_docker_cmake.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,10 @@ def cmake_config_dockerfile(base: str, build_type: str, with_cuda: bool = True)
3838
cmake_extra_args = " ".join(additional_args)
3939

4040
# Begin constructing the dockerfile with the initial FROM line.
41-
dockerfile: str = f"FROM {base}\n\n"
41+
dockerfile: str = f"FROM {base}"
4242

4343
# Activate the micromamba user and environment.
44-
dockerfile += micromamba_docker_lines() + "\n\n"
44+
dockerfile += f"\n\n{micromamba_docker_lines()}\n\n"
4545
dockerfile += dedent(
4646
f"""
4747
ENV INSTALL_PREFIX {str(install_prefix())}
@@ -59,6 +59,7 @@ def cmake_config_dockerfile(base: str, build_type: str, with_cuda: bool = True)
5959
"""
6060
).strip()
6161

62+
dockerfile += "\n"
6263
return dockerfile
6364

6465

@@ -77,14 +78,19 @@ def cmake_build_dockerfile(base: str) -> str:
7778
The generated Dockerfile.
7879
"""
7980
# Begin constructing the dockerfile with the initial FROM line.
80-
dockerfile = f"FROM {base}\n\n"
81+
dockerfile = f"FROM {base}"
8182

8283
# Run as the $MAMBA_USER and activate the micromamba environment.
83-
dockerfile += f"{micromamba_docker_lines()}\n\n"
84+
dockerfile += f"\n\n{micromamba_docker_lines()}"
8485

8586
# Build the project.
86-
dockerfile += "RUN cmake --build $BUILD_PREFIX --parallel"
87+
dockerfile += "\n\nRUN cmake --build $BUILD_PREFIX --parallel"
8788

89+
# Add permissions to the testing subdirectory under the build prefix.
90+
# This step is necessary to enable testing on the image.
91+
dockerfile += "\n\nRUN chmod -R 777 $BUILD_PREFIX"
92+
93+
dockerfile += "\n"
8894
return dockerfile
8995

9096

@@ -103,10 +109,10 @@ def cmake_install_dockerfile(base: str) -> str:
103109
The generated Dockerfile.
104110
"""
105111
# Begin constructing the dockerfile with the initial FROM line.
106-
dockerfile = f"FROM {base}\n\n"
112+
dockerfile = f"FROM {base}"
107113

108114
# Run as the $MAMBA_USER and activate the micromamba environment.
109-
dockerfile += f"{micromamba_docker_lines()}\n\n"
115+
dockerfile += f"\n\n{micromamba_docker_lines()}\n\n"
110116

111117
# Install the project and set the appropriate permissions at the target directory.
112118
dockerfile += dedent(
@@ -129,4 +135,5 @@ def cmake_install_dockerfile(base: str) -> str:
129135
"""
130136
).strip()
131137

138+
dockerfile += "\n"
132139
return dockerfile

wigwam/_image.py

Lines changed: 37 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1+
from __future__ import annotations
2+
13
import io
24
import os
5+
from collections.abc import Iterable
36
from shlex import split
47
from subprocess import DEVNULL, CalledProcessError, run
58
from sys import stdin
6-
from typing import Any, List, Optional, Type, TypeVar, Union, overload
9+
from typing import Any, Type, TypeVar, overload
710

11+
from ._bind_mount import BindMount
812
from ._exceptions import CommandNotFoundError, DockerBuildError, ImageNotFoundError
913

1014

@@ -16,7 +20,7 @@ class Image:
1620
an interface by which to interact with that image.
1721
1822
Capabilities include:
19-
- Building Docker images from dockerfiles or Dockerfile-formatted strings
23+
- Building Docker images from Dockerfiles or Dockerfile-formatted strings
2024
via :func:`~wigwam.Image.build`.
2125
- Running commands in containers built from the image using
2226
:func:`~wigwam.Image.run`.
@@ -42,8 +46,8 @@ def build(
4246
cls: Type[Self],
4347
tag: str,
4448
*,
45-
dockerfile: Union[str, os.PathLike[str]],
46-
context: Union[str, os.PathLike[str]] = ...,
49+
dockerfile: os.PathLike[str] | str,
50+
context: os.PathLike[str] | str = ...,
4751
stdout: Any = ...,
4852
stderr: Any = ...,
4953
network: str = ...,
@@ -60,8 +64,7 @@ def build(
6064
tag : str
6165
A name for the image.
6266
dockerfile : os.PathLike
63-
The path of the Dockerfile to build
64-
directory.
67+
The path of the Dockerfile to build directory.
6568
context : os.PathLike, optional
6669
The build context. Defaults to ".".
6770
stdout : io.TextIOBase or special value, optional
@@ -95,7 +98,7 @@ def build(
9598
tag: str,
9699
*,
97100
dockerfile_string: str,
98-
context: Union[str, os.PathLike[str]] = ...,
101+
context: os.PathLike[str] | str = ...,
99102
stdout: Any = ...,
100103
stderr: Any = ...,
101104
network: str = ...,
@@ -132,7 +135,7 @@ def build(
132135
DockerBuildError
133136
If the Docker build command fails.
134137
ValueError
135-
If both `Dockerfile` and `dockerfile_string` are defined.
138+
If both `dockerfile` and `dockerfile_string` are defined.
136139
"""
137140
...
138141

@@ -194,14 +197,14 @@ def build(
194197

195198
return cls(tag)
196199

197-
def _inspect(self, format: Optional[str] = None) -> str:
200+
def _inspect(self, format: str | None = None) -> str:
198201
"""
199202
Use 'docker inspect' to retrieve a piece of information about the
200203
image.
201204
202205
Parameters
203206
----------
204-
format : str, optional
207+
format : str or None, optional
205208
The value to be requested by the --format argument, or None.
206209
Defaults to None.
207210
@@ -224,11 +227,13 @@ def run(
224227
self,
225228
command: str,
226229
*,
227-
stdout: Optional[Union[io.TextIOBase, int]] = None,
228-
stderr: Optional[Union[io.TextIOBase, int]] = None,
230+
stdout: io.TextIOBase | int | None = None,
231+
stderr: io.TextIOBase | int | None = None,
229232
interactive: bool = False,
230233
network: str = "host",
231234
check: bool = True,
235+
host_user: bool = False,
236+
bind_mounts: Iterable[BindMount] | None = None,
232237
) -> str:
233238
"""
234239
Run the given command on a container.
@@ -253,6 +258,11 @@ def run(
253258
check: bool, optional
254259
If True, check for CalledProcessErrors on non-zero return codes. Defualts
255260
to True.
261+
host_user: bool, optional
262+
If True, run the command as the user on the host machine, else run as the
263+
default user. Defaults to False.
264+
bind_mounts : Iterable[BindMount], optional
265+
A list of bind mount descriptions to apply to the run command.
256266
257267
Returns
258268
-------
@@ -264,7 +274,13 @@ def run(
264274
CommandNotFoundError:
265275
When a command is attempted that is not recognized on the image.
266276
"""
277+
267278
cmd = ["docker", "run", f"--network={network}", "--rm"]
279+
if host_user:
280+
cmd += ["-u", f"{os.getuid()}:{os.getgid()}"]
281+
if bind_mounts is not None:
282+
for mount in bind_mounts:
283+
cmd += ["-v", f"{mount.mount_string()}"]
268284
if interactive:
269285
cmd += ["-i"]
270286
if stdin.isatty():
@@ -288,7 +304,7 @@ def run(
288304
raise
289305
return result.stdout
290306

291-
def drop_in(self, network: str = "host") -> None:
307+
def drop_in(self, network: str = "host", host_user: bool = True) -> None:
292308
"""
293309
Start a drop-in session on a disposable container.
294310
@@ -300,15 +316,18 @@ def drop_in(self, network: str = "host") -> None:
300316
----------
301317
network : str, optional
302318
The name of the network. Defaults to "host".
319+
host_user: bool, optional
320+
If True, run as the current user on the host machine. Else, run as the
321+
default user in the image. Defaults to True.
303322
304323
Raises
305324
-------
306325
CommandNotFoundError:
307326
When bash is not recognized on the image.
308327
"""
309-
self.run(
310-
"bash", interactive=True, network=network, check=False
311-
) # pragma: no cover
328+
self.run( # pragma: no cover
329+
"bash", interactive=True, network=network, check=False, host_user=host_user
330+
)
312331

313332
def has_command(self, command: str) -> bool:
314333
"""
@@ -339,8 +358,8 @@ def has_command(self, command: str) -> bool:
339358
return True
340359

341360
@property
342-
def tags(self) -> List[str]:
343-
"""List[str]: The Repo Tags held on this Docker image."""
361+
def tags(self) -> list[str]:
362+
"""list[str]: The Repo Tags held on this Docker image."""
344363
return self._inspect(format="{{.RepoTags}}").strip("][\n").split(", ")
345364

346365
@property

wigwam/cli/util_commands.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import argparse
44

5-
from ..commands import dropin, make_lockfile, remove
5+
from ..commands import dropin, make_lockfile, remove, test
66
from ._utils import help_formatter
77

88

@@ -18,12 +18,38 @@ def init_util_parsers(subparsers: argparse._SubParsersAction, prefix: str) -> No
1818
The image tag prefix.
1919
"""
2020

21+
test_parser = subparsers.add_parser(
22+
"test", help="Run unit tests on an image.", formatter_class=help_formatter
23+
)
24+
test_parser.add_argument(
25+
"tag", metavar="IMAGE_TAG", type=str, help="The tag or ID of the test image."
26+
)
27+
test_parser.add_argument(
28+
"--output-xml",
29+
"-o",
30+
type=str,
31+
default="Test.xml",
32+
help="The output XML file to write test results to.",
33+
)
34+
test_parser.add_argument(
35+
"--compress-output", action="store_true", help="Compress ctest output."
36+
)
37+
test_parser.add_argument(
38+
"--quiet-fail", action="store_true", help="Less verbose output on test failure."
39+
)
40+
2141
dropin_parser = subparsers.add_parser(
2242
"dropin", help="Start a drop-in session.", formatter_class=help_formatter
2343
)
2444
dropin_parser.add_argument(
2545
"tag", metavar="IMAGE_TAG", type=str, help="The tag or ID of the desired image."
2646
)
47+
test_parser.add_argument(
48+
"--default-user",
49+
action="store_true",
50+
help="Run as the default user on the image. If not used, will run as the "
51+
"current user on the host machine.",
52+
)
2753

2854
remove_parser = subparsers.add_parser(
2955
"remove",
@@ -93,3 +119,5 @@ def run_util(args: argparse.Namespace, command: str) -> None:
93119
remove(**vars(args))
94120
elif command == "lockfile":
95121
make_lockfile(**vars(args))
122+
elif command == "test":
123+
test(**vars(args))

0 commit comments

Comments
 (0)