Repository: XD-embedded/xd-docker

Issue:
client.container_stop()
Client API command to stop a container.

Discussion:

Patch:
diff --git a/xd/docker/client.py b/xd/docker/client.py
index ec42404..382c1ec 100644
--- a/xd/docker/client.py
+++ b/xd/docker/client.py
@@ -508,3 +508,35 @@ class DockerClient(object):
 
         r = self._post('/containers/{}/wait'.format(id_or_name))
         return r.json()['StatusCode']
+
+    def container_stop(self, container: Union[Container, ContainerName, str],
+                       timeout: Optional[int]=None):
+        """Stop container.
+
+        Stop the container, and optionally killing the container after a
+        timeout.
+
+        Arguments:
+          container: The container to remove (id or name).
+          timeout: Number of seconds to wait before killing the container.
+        """
+
+        # Handle convenience argument types
+        if isinstance(container, str):
+            id_or_name = container
+        elif isinstance(container, ContainerName):
+            id_or_name = container.name
+        else:
+            id_or_name = container.id or container.name
+
+        params = {}
+        if timeout is not None:
+            params['t'] = timeout
+
+        try:
+            self._post('/containers/{}/stop'.format(id_or_name), params=params)
+        except HTTPError as e:
+            if e.code == 304:
+                return False
+            raise e
+        return True


Test patch:
diff --git a/tests/integration/container_stop_test.py b/tests/integration/container_stop_test.py
new file mode 100644
index 0000000..e11e8f3
--- /dev/null
+++ b/tests/integration/container_stop_test.py
@@ -0,0 +1,27 @@
+import pytest
+import os
+import time
+
+from xd.docker.client import *
+
+
+def test_stop(docker, stdout):
+    os.system("docker run -d --name xd-docker-test busybox:latest sleep 10")
+    assert docker.container_stop('xd-docker-test') == True
+
+
+def test_already_stopped(docker, stdout):
+    os.system("docker run --name xd-docker-test busybox:latest true")
+    assert docker.container_stop('xd-docker-test') == False
+
+
+def test_not_started(docker, stdout):
+    os.system("docker create --name xd-docker-test busybox:latest true")
+    assert docker.container_stop('xd-docker-test') == False
+
+
+def test_no_such_container(docker, stdout):
+    with pytest.raises(ClientError) as clienterror:
+        with stdout.redirect():
+            docker.container_stop('xd-docker-test')
+    assert clienterror.value.code == 404
diff --git a/tests/integration/container_wait_test.py b/tests/integration/container_wait_test.py
new file mode 100644
index 0000000..dc5d44d
--- /dev/null
+++ b/tests/integration/container_wait_test.py
@@ -0,0 +1,27 @@
+import pytest
+import os
+import time
+
+from xd.docker.client import *
+
+
+def test_waiting(docker, stdout):
+    os.system("docker run -d --name xd-docker-test busybox:latest sleep 2")
+    assert docker.container_wait('xd-docker-test') == 0
+
+
+def test_already_stopped_0(docker, stdout):
+    os.system("docker run --name xd-docker-test busybox:latest true")
+    assert docker.container_wait('xd-docker-test') == 0
+
+
+def test_already_stopped_1(docker, stdout):
+    os.system("docker run --name xd-docker-test busybox:latest false")
+    assert docker.container_wait('xd-docker-test') == 1
+
+
+def test_no_such_container(docker, stdout):
+    with pytest.raises(ClientError) as clienterror:
+        with stdout.redirect():
+            docker.container_wait('xd-docker-test')
+    assert clienterror.value.code == 404
diff --git a/tests/unit/client_test.py b/tests/unit/client_test.py
index 0f1f130..6bf24f2 100644
--- a/tests/unit/client_test.py
+++ b/tests/unit/client_test.py
@@ -1263,10 +1263,7 @@ class container_wait_tests(ContextClientTestCase):
 
     @mock.patch('requests.post')
     def test_no_such_container(self, post_mock):
-        post_mock.return_value = requests_mock.Response(json.dumps(
-            {'StatusCode': 42}), 200)
-        post_mock.return_value = requests_mock.Response(
-            "No such container", 404)
+        post_mock.return_value = requests_mock.Response(None, 404)
         with pytest.raises(ClientError) as clienterror:
             self.client.container_wait('foobar')
         assert clienterror.value.code == 404
@@ -1283,3 +1280,43 @@ class container_wait_tests(ContextClientTestCase):
             {'StatusCode': 0}), 200)
         assert self.client.container_wait(Container(
             self.client,name="foobar")) == 0
+
+
+class container_stop_tests(ContextClientTestCase):
+
+    @mock.patch('requests.post')
+    def test_normal(self, post_mock):
+        post_mock.return_value = requests_mock.Response(None, 204)
+        assert self.client.container_stop("foobar") == True
+
+    @mock.patch('requests.post')
+    def test_already_stopped(self, post_mock):
+        post_mock.return_value = requests_mock.Response(None, 304)
+        assert self.client.container_stop("foobar") == False
+
+    @mock.patch('requests.post')
+    def test_no_such_container(self, post_mock):
+        post_mock.return_value = requests_mock.Response(None, 404)
+        with pytest.raises(ClientError) as clienterror:
+            self.client.container_stop('foobar')
+        assert clienterror.value.code == 404
+
+    @mock.patch('requests.post')
+    def test_containername(self, post_mock):
+        post_mock.return_value = requests_mock.Response(None, 204)
+        assert self.client.container_stop(ContainerName("foobar")) == True
+
+    @mock.patch('requests.post')
+    def test_container(self, post_mock):
+        post_mock.return_value = requests_mock.Response(None, 204)
+        assert self.client.container_stop(Container(
+            self.client,name="foobar")) == True
+
+    @mock.patch('requests.post')
+    def test_timeout(self, post_mock):
+        post_mock.return_value = requests_mock.Response(None, 204)
+        assert self.client.container_stop("foobar", timeout=42) == True
+        assert 'params' in post_mock.call_args[1]
+        params = post_mock.call_args[1]['params']
+        assert 't' in params
+        assert params['t'] == 42