cli_test.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. import os
  2. import shutil
  3. import tempfile
  4. from io import StringIO
  5. import docker
  6. import py
  7. import pytest
  8. from docker.constants import DEFAULT_DOCKER_API_VERSION
  9. from .. import mock
  10. from .. import unittest
  11. from ..helpers import build_config
  12. from compose.cli.command import get_project
  13. from compose.cli.command import get_project_name
  14. from compose.cli.docopt_command import NoSuchCommand
  15. from compose.cli.errors import UserError
  16. from compose.cli.main import TopLevelCommand
  17. from compose.config.environment import Environment
  18. from compose.const import IS_WINDOWS_PLATFORM
  19. from compose.const import LABEL_SERVICE
  20. from compose.container import Container
  21. from compose.project import Project
  22. class CLITestCase(unittest.TestCase):
  23. def test_default_project_name(self):
  24. test_dir = py._path.local.LocalPath('tests/fixtures/simple-composefile')
  25. with test_dir.as_cwd():
  26. project_name = get_project_name('.')
  27. assert 'simple-composefile' == project_name
  28. def test_project_name_with_explicit_base_dir(self):
  29. base_dir = 'tests/fixtures/simple-composefile'
  30. project_name = get_project_name(base_dir)
  31. assert 'simple-composefile' == project_name
  32. def test_project_name_with_explicit_uppercase_base_dir(self):
  33. base_dir = 'tests/fixtures/UpperCaseDir'
  34. project_name = get_project_name(base_dir)
  35. assert 'uppercasedir' == project_name
  36. def test_project_name_with_explicit_project_name(self):
  37. name = 'explicit-project-name'
  38. project_name = get_project_name(None, project_name=name)
  39. assert 'explicit-project-name' == project_name
  40. @mock.patch.dict(os.environ)
  41. def test_project_name_from_environment_new_var(self):
  42. name = 'namefromenv'
  43. os.environ['COMPOSE_PROJECT_NAME'] = name
  44. project_name = get_project_name(None)
  45. assert project_name == name
  46. def test_project_name_with_empty_environment_var(self):
  47. base_dir = 'tests/fixtures/simple-composefile'
  48. with mock.patch.dict(os.environ):
  49. os.environ['COMPOSE_PROJECT_NAME'] = ''
  50. project_name = get_project_name(base_dir)
  51. assert 'simple-composefile' == project_name
  52. @mock.patch.dict(os.environ)
  53. def test_project_name_with_environment_file(self):
  54. base_dir = tempfile.mkdtemp()
  55. try:
  56. name = 'namefromenvfile'
  57. with open(os.path.join(base_dir, '.env'), 'w') as f:
  58. f.write('COMPOSE_PROJECT_NAME={}'.format(name))
  59. project_name = get_project_name(base_dir)
  60. assert project_name == name
  61. # Environment has priority over .env file
  62. os.environ['COMPOSE_PROJECT_NAME'] = 'namefromenv'
  63. assert get_project_name(base_dir) == os.environ['COMPOSE_PROJECT_NAME']
  64. finally:
  65. shutil.rmtree(base_dir)
  66. def test_get_project(self):
  67. base_dir = 'tests/fixtures/longer-filename-composefile'
  68. env = Environment.from_env_file(base_dir)
  69. env['COMPOSE_API_VERSION'] = DEFAULT_DOCKER_API_VERSION
  70. project = get_project(base_dir, environment=env)
  71. assert project.name == 'longer-filename-composefile'
  72. assert project.client
  73. assert project.services
  74. def test_command_help(self):
  75. with mock.patch('sys.stdout', new=StringIO()) as fake_stdout:
  76. TopLevelCommand.help({'COMMAND': 'up'})
  77. assert "Usage: up" in fake_stdout.getvalue()
  78. def test_command_help_nonexistent(self):
  79. with pytest.raises(NoSuchCommand):
  80. TopLevelCommand.help({'COMMAND': 'nonexistent'})
  81. @pytest.mark.xfail(IS_WINDOWS_PLATFORM, reason="requires dockerpty")
  82. @mock.patch('compose.cli.main.RunOperation', autospec=True)
  83. @mock.patch('compose.cli.main.PseudoTerminal', autospec=True)
  84. @mock.patch('compose.service.Container.create')
  85. @mock.patch.dict(os.environ)
  86. def test_run_interactive_passes_logs_false(
  87. self,
  88. mock_container_create,
  89. mock_pseudo_terminal,
  90. mock_run_operation,
  91. ):
  92. os.environ['COMPOSE_INTERACTIVE_NO_CLI'] = 'true'
  93. mock_client = mock.create_autospec(docker.APIClient)
  94. mock_client.api_version = DEFAULT_DOCKER_API_VERSION
  95. mock_client._general_configs = {}
  96. mock_container_create.return_value = Container(mock_client, {
  97. 'Id': '37b35e0ba80d91009d37e16f249b32b84f72bda269985578ed6c75a0a13fcaa8',
  98. 'Config': {
  99. 'Labels': {
  100. LABEL_SERVICE: 'service',
  101. }
  102. },
  103. }, has_been_inspected=True)
  104. project = Project.from_config(
  105. name='composetest',
  106. client=mock_client,
  107. config_data=build_config({
  108. 'service': {'image': 'busybox'}
  109. }),
  110. )
  111. command = TopLevelCommand(project)
  112. with pytest.raises(SystemExit):
  113. command.run({
  114. 'SERVICE': 'service',
  115. 'COMMAND': None,
  116. '-e': [],
  117. '--label': [],
  118. '--user': None,
  119. '--no-deps': None,
  120. '--detach': False,
  121. '-T': None,
  122. '--entrypoint': None,
  123. '--service-ports': None,
  124. '--use-aliases': None,
  125. '--publish': [],
  126. '--volume': [],
  127. '--rm': None,
  128. '--name': None,
  129. '--workdir': None,
  130. })
  131. _, _, call_kwargs = mock_run_operation.mock_calls[0]
  132. assert call_kwargs['logs'] is False
  133. @mock.patch('compose.service.Container.create')
  134. def test_run_service_with_restart_always(self, mock_container_create):
  135. mock_client = mock.create_autospec(docker.APIClient)
  136. mock_client.api_version = DEFAULT_DOCKER_API_VERSION
  137. mock_client._general_configs = {}
  138. mock_container_create.return_value = Container(mock_client, {
  139. 'Id': '37b35e0ba80d91009d37e16f249b32b84f72bda269985578ed6c75a0a13fcaa8',
  140. 'Name': 'composetest_service_37b35',
  141. 'Config': {
  142. 'Labels': {
  143. LABEL_SERVICE: 'service',
  144. }
  145. },
  146. }, has_been_inspected=True)
  147. project = Project.from_config(
  148. name='composetest',
  149. client=mock_client,
  150. config_data=build_config({
  151. 'service': {
  152. 'image': 'busybox',
  153. 'restart': 'always',
  154. }
  155. }),
  156. )
  157. command = TopLevelCommand(project)
  158. command.run({
  159. 'SERVICE': 'service',
  160. 'COMMAND': None,
  161. '-e': [],
  162. '--label': [],
  163. '--user': None,
  164. '--no-deps': None,
  165. '--detach': True,
  166. '-T': None,
  167. '--entrypoint': None,
  168. '--service-ports': None,
  169. '--use-aliases': None,
  170. '--publish': [],
  171. '--volume': [],
  172. '--rm': None,
  173. '--name': None,
  174. '--workdir': None,
  175. })
  176. # NOTE: The "run" command is supposed to be a one-off tool; therefore restart policy "no"
  177. # (the default) is enforced despite explicit wish for "always" in the project
  178. # configuration file
  179. assert not mock_client.create_host_config.call_args[1].get('restart_policy')
  180. command = TopLevelCommand(project)
  181. command.run({
  182. 'SERVICE': 'service',
  183. 'COMMAND': None,
  184. '-e': [],
  185. '--label': [],
  186. '--user': None,
  187. '--no-deps': None,
  188. '--detach': True,
  189. '-T': None,
  190. '--entrypoint': None,
  191. '--service-ports': None,
  192. '--use-aliases': None,
  193. '--publish': [],
  194. '--volume': [],
  195. '--rm': True,
  196. '--name': None,
  197. '--workdir': None,
  198. })
  199. assert not mock_client.create_host_config.call_args[1].get('restart_policy')
  200. @mock.patch('compose.project.Project.up')
  201. @mock.patch.dict(os.environ)
  202. def test_run_up_with_docker_cli_build(self, mock_project_up):
  203. os.environ['COMPOSE_DOCKER_CLI_BUILD'] = '1'
  204. mock_client = mock.create_autospec(docker.APIClient)
  205. mock_client.api_version = DEFAULT_DOCKER_API_VERSION
  206. mock_client._general_configs = {}
  207. container = Container(mock_client, {
  208. 'Id': '37b35e0ba80d91009d37e16f249b32b84f72bda269985578ed6c75a0a13fcaa8',
  209. 'Name': 'composetest_service_37b35',
  210. 'Config': {
  211. 'Labels': {
  212. LABEL_SERVICE: 'service',
  213. }
  214. },
  215. }, has_been_inspected=True)
  216. mock_project_up.return_value = [container]
  217. project = Project.from_config(
  218. name='composetest',
  219. config_data=build_config({
  220. 'service': {'image': 'busybox'}
  221. }),
  222. client=mock_client,
  223. )
  224. command = TopLevelCommand(project)
  225. command.run({
  226. 'SERVICE': 'service',
  227. 'COMMAND': None,
  228. '-e': [],
  229. '--label': [],
  230. '--user': None,
  231. '--no-deps': None,
  232. '--detach': True,
  233. '-T': None,
  234. '--entrypoint': None,
  235. '--service-ports': None,
  236. '--use-aliases': None,
  237. '--publish': [],
  238. '--volume': [],
  239. '--rm': None,
  240. '--name': None,
  241. '--workdir': None,
  242. })
  243. _, _, call_kwargs = mock_project_up.mock_calls[0]
  244. assert call_kwargs.get('cli')
  245. def test_command_manual_and_service_ports_together(self):
  246. project = Project.from_config(
  247. name='composetest',
  248. client=None,
  249. config_data=build_config({
  250. 'service': {'image': 'busybox'},
  251. }),
  252. )
  253. command = TopLevelCommand(project)
  254. with pytest.raises(UserError):
  255. command.run({
  256. 'SERVICE': 'service',
  257. 'COMMAND': None,
  258. '-e': [],
  259. '--label': [],
  260. '--user': None,
  261. '--no-deps': None,
  262. '--detach': True,
  263. '-T': None,
  264. '--entrypoint': None,
  265. '--service-ports': True,
  266. '--use-aliases': None,
  267. '--publish': ['80:80'],
  268. '--rm': None,
  269. '--name': None,
  270. })