from typing import Iterator, Optional, Dict, Any, List, Union
from ipaddress import IPv4Address, IPv6Address
import logging
from .api import APIClient
from .. import exceptions
from ..compiler import CompilationOutcome
from ..core.tool import Tool
from ..core.patch import Patch
from ..core.fileline import FileLineSet
from ..core.bug import Bug
from ..core.container import Container
from ..core.coverage import TestSuiteCoverage
from ..core.test import TestCase, TestOutcome
from ..cmd import ExecResponse
logger = logging.getLogger(__name__) # type: logging.Logger
logger.setLevel(logging.DEBUG)
__all__ = ('ContainerManager',)
[docs]class ContainerManager(object):
def __init__(self, api: APIClient) -> None:
self.__api = api
[docs] def __getitem__(self, uid: str) -> Container:
"""Fetches a container by its ID.
Parameters:
uid: the ID of the container.
Returns:
the container with the given ID.
Raises:
KeyError: if no container is found with the given ID.
"""
with self.__api.get('containers/{}'.format(uid)) as r:
if r.status_code == 200:
return Container.from_dict(r.json())
if r.status_code == 404:
m = "no container found with given UID: {}".format(uid)
raise KeyError(m)
self.__api.handle_erroneous_response(r)
[docs] def __delitem__(self, uid: str) -> None:
"""Deletes a given container.
Parameters:
uid: the ID of the container.
Raises:
KeyError: if no container is found with the given ID, or the
container has already been destroyed.
"""
with self.__api.delete('containers/{}'.format(uid)) as r:
if r.status_code == 204:
return
if r.status_code == 404:
m = "no container found with given UID: {}".format(uid)
raise KeyError(m)
self.__api.handle_erroneous_response(r)
[docs] def __contains__(self, uid: str) -> bool:
"""Checks whether a container with a given ID exists.
Parameter:
uid: the ID of the container.
Returns:
True if the container exists; false if not.
"""
try:
self[uid]
return True
except KeyError:
return False
def clear(self) -> None:
"""Destroys all running containers."""
with self.__api.delete('containers') as r:
if r.status_code != 204:
self.__api.handle_erroneous_response(r)
[docs] def __iter__(self) -> Iterator[str]:
"""
Returns an iterator over the identifiers of all of the containers that
are currently running on the server.
"""
with self.__api.get('containers') as r:
if r.status_code == 200:
ids = r.json()
assert isinstance(ids , list)
assert all(isinstance(n, str) for n in ids)
return ids.__iter__()
self.__api.handle_erroneous_response(r)
[docs] def provision(self,
bug: Bug,
*,
plugins: Optional[List[Tool]] = None
) -> Container:
"""Provisions a container for a given bug."""
if plugins is None:
plugins = []
logger.info("provisioning container for bug: %s", bug.name)
endpoint = 'bugs/{}/provision'.format(bug.name)
payload = {
'plugins': [p.to_dict() for p in plugins]
} # type: Dict[str, Any]
with self.__api.post(endpoint, json=payload) as r:
if r.status_code == 200:
container = Container.from_dict(r.json())
logger.info("provisioned container (id: %s) for bug: %s",
container.uid,
bug.name)
return container
if r.status_code == 404:
m = "no bug registered with given name: {}".format(bug.name)
raise KeyError(m)
self.__api.handle_erroneous_response(r)
def mktemp(self, container: Container) -> str:
"""Generates a temporary file for a given container.
Returns:
the path to the temporary file inside the given container.
"""
with self.__api.post('containers/{}/tempfile'.format(container.uid)) as r:
if r.status_code == 200:
return r.json()
self.__api.handle_erroneous_response(r)
def ip_address(self,
container: Container
) -> Union[IPv4Address, IPv6Address]:
"""
The IP address used by a given container, or None if no IP address has
been assigned to that container.
"""
with self.__api.get('containers/{}/ip'.format(container.uid)) as r:
if r.status_code == 200:
return r.json()
self.__api.handle_erroneous_response(r)
[docs] def is_alive(self, container: Container) -> bool:
"""Determines whether or not a given container is still alive."""
uid = container.uid
with self.__api.get('containers/{}/alive'.format(uid)) as r:
if r.status_code == 200:
return r.json()
if r.status_code == 404:
m = "no container found with given UID: {}".format(uid)
raise KeyError(m)
self.__api.handle_erroneous_response(r)
def extract_coverage(self, container: Container) -> FileLineSet:
"""
Extracts a report of the lines that have been executed since the last
time that a coverage report was extracted.
"""
uid = container.uid
with self.__api.post('containers/{}/read-coverage'.format(uid)) as r:
if r.status_code == 200:
return FileLineSet.from_dict(r.json())
self.__api.handle_erroneous_response(r)
def instrument(self,
container: Container
) -> None:
"""
Instruments the program inside the container for computing test suite
coverage.
Params:
container: the container that should be instrumented.
"""
path = "containers/{}/instrument".format(container.uid)
with self.__api.post(path) as r:
if r.status_code != 204:
logger.info("failed to instrument container: %s", container.uid)
self.__api.handle_erroneous_response(r)
def compile(self,
container: Container,
verbose: bool = False
) -> CompilationOutcome:
"""Attempts to compile the program inside a given container.
Params:
container: the container whose program should be compiled.
verbose: specifies whether to print the stdout and stderr produced
by the compilation command to the stdout. If `True`, then the
stdout and stderr will be printed.
Returns:
a summary of the outcome of the attempted compilation.
Raises:
KeyError: if the container no longer exists.
"""
path = "containers/{}/build".format(container.uid)
params = {}
if verbose:
params['verbose'] = 'yes'
with self.__api.post(path, params=params) as r:
if r.status_code == 200:
return CompilationOutcome.from_dict(r.json())
self.__api.handle_erroneous_response(r)
build = compile
def test(self,
container: Container,
test: TestCase
) -> TestOutcome:
"""Executes a given test inside a container.
Parameters:
container: the container in which the test should be conducted.
test: the test that should be executed.
Returns:
a summary of the outcome of the test execution.
Raises:
KeyError: if the container no longer exists, or the given test
doesn't exist.
"""
path = "containers/{}/test/{}".format(container.uid, test.name)
with self.__api.post(path) as r:
if r.status_code == 200:
return TestOutcome.from_dict(r.json())
self.__api.handle_erroneous_response(r)
def coverage(self,
container: Container,
*,
instrument: bool = True
) -> TestSuiteCoverage:
"""Computes complete test suite coverage for a given container.
Parameters:
container: the container for which coverage should be computed.
rebuild: if set to True, the program will be rebuilt before
coverage is computed.
"""
uid = container.uid
logger.info("Fetching coverage information for container: %s",
uid)
uri = 'containers/{}/coverage'.format(uid)
params = {'instrument': 'yes' if instrument else 'no'}
with self.__api.post(uri, params=params) as r:
if r.status_code == 200:
jsn = r.json()
coverage = TestSuiteCoverage.from_dict(jsn) # type: ignore
logger.info("Fetched coverage information for container: %s",
uid)
return coverage
try:
self.__api.handle_erroneous_response(r)
except exceptions.BugZooException as err:
logger.exception("Failed to fetch coverage information for container %s: %s", uid, err.message) # noqa: pycodestyle
raise
except Exception as err:
logger.exception("Failed to fetch coverage information for container %s due to unexpected failure: %s", uid, err) # noqa: pycodestyle
raise
[docs] def exec(self,
container: Container,
command: str,
context: Optional[str] = None,
stdout: bool = True,
stderr: bool = False,
time_limit: Optional[int] = None
) -> ExecResponse:
"""Executes a given command inside a provided container.
Parameters:
container: the container to which the command should be issued.
command: the command that should be executed.
context: the working directory that should be used to perform the
execution. If no context is provided, then the command will be
executed at the root of the container.
stdout: specifies whether or not output to the stdout should be
included in the execution summary.
stderr: specifies whether or not output to the stderr should be
included in the execution summary.
time_limit: an optional time limit that is applied to the
execution. If the command fails to execute within the time
limit, the command will be aborted and treated as a failure.
Returns:
a summary of the outcome of the execution.
Raises:
KeyError: if the container no longer exists on the server.
"""
# FIXME perhaps these should be encoded as path variables?
payload = {
'command': command,
'context': context,
'stdout': stdout,
'stderr': stderr,
'time-limit': time_limit
}
path = "containers/{}/exec".format(container.uid)
with self.__api.post(path, json=payload) as r:
if r.status_code == 200:
return ExecResponse.from_dict(r.json())
if r.status_code == 404:
raise KeyError("no container found with given UID: {}".format(container.uid))
self.__api.handle_erroneous_response(r)
command = exec
def patch(self, container: Container, patch: Patch) -> bool:
"""
Attempts to apply a given patch to the source code for a program inside
a given container. All patch applications are guaranteed to be atomic;
if the patch fails to apply, no changes will be made to the relevant
source code files.
Returns:
true if patch application was successful, and false if the attempt
was unsuccessful.
"""
path = "containers/{}".format(container.uid)
payload = str(patch)
with self.__api.patch(path, payload) as r:
return r.status_code == 204
def persist(self, container: Container, image_name: str) -> None:
"""Persists the state of a given container as a Docker image.
Parameters:
container: the container that should be persisted.
image_name: the name of the Docker image that should be created.
Raises:
ContainerNotFound: if the given container does not exist on the
server.
ImageAlreadyExists: if the given image name is already in use by
another Docker image on the server.
"""
logger.debug("attempting to persist container (%s) to image (%s).",
container.id,
image_name)
path = "containers/{}/persist/{}".format(container.id, image_name)
with self.__api.put(path) as r:
if r.status_code == 204:
logger.debug("persisted container (%s) to image (%s).",
container.id,
image_name)
return
try:
self.__api.handle_erroneous_response(r)
except Exception:
logger.exception("failed to persist container (%s) to image (%s).", # noqa: pycodestyle
container.id,
image_name)
raise