class TestDockerBuildCommand(TestCase): """Test docker build commands""" def setUp(self): self.mocks = EnvironmentMockGroup() self.mocks.start() def tearDown(self): self.mocks.stop() def test_wrapped_command(self): """Test shell wrapping for Docker chdir""" cmd = DockerBuildCommand(["pip", "install", "requests"], cwd="/tmp/foobar") self.assertEqual(cmd.get_wrapped_command(), ("/bin/sh -c " "'cd /tmp/foobar && " "pip install requests'")) cmd = DockerBuildCommand( ["python", "/tmp/foo/pip", "install", "Django>1.7"], cwd="/tmp/foobar", bin_path="/tmp/foo" ) self.assertEqual( cmd.get_wrapped_command(), ("/bin/sh -c " "'cd /tmp/foobar && PATH=/tmp/foo:$PATH " "python /tmp/foo/pip install Django\>1.7'"), ) def test_unicode_output(self): """Unicode output from command""" self.mocks.configure_mock( "docker_client", { "exec_create.return_value": {"Id": "container-foobar"}, "exec_start.return_value": b"HérÉ îß sömê ünïçó∂é", "exec_inspect.return_value": {"ExitCode": 0}, }, )
class TestDockerBuildCommand(TestCase): '''Test docker build commands''' def setUp(self): self.mocks = EnvironmentMockGroup() self.mocks.start() def tearDown(self): self.mocks.stop() def test_wrapped_command(self): '''Test shell wrapping for Docker chdir''' cmd = DockerBuildCommand(['pip', 'install', 'requests'], cwd='/tmp/foobar') self.assertEqual( cmd.get_wrapped_command(), ("/bin/sh -c " "'cd /tmp/foobar && " "pip install requests'")) cmd = DockerBuildCommand(['pip', 'install', 'Django>1.7'], cwd='/tmp/foobar') self.assertEqual( cmd.get_wrapped_command(), ("/bin/sh -c " "'cd /tmp/foobar && " "pip install Django\>1.7'")) def test_unicode_output(self): '''Unicode output from command''' self.mocks.configure_mock('docker_client', { 'exec_create.return_value': {'Id': 'container-foobar'}, 'exec_start.return_value': b'HérÉ îß sömê ünïçó∂é', 'exec_inspect.return_value': {'ExitCode': 0}, })
class TestDockerBuildCommand(TestCase): '''Test docker build commands''' def setUp(self): self.mocks = EnvironmentMockGroup() self.mocks.start() def tearDown(self): self.mocks.stop() def test_wrapped_command(self): '''Test shell wrapping for Docker chdir''' cmd = DockerBuildCommand(['pip', 'install', 'requests'], cwd='/tmp/foobar') self.assertEqual(cmd.get_wrapped_command(), ("/bin/sh -c " "'cd /tmp/foobar && " "pip install requests'")) cmd = DockerBuildCommand( ['python', '/tmp/foo/pip', 'install', 'Django>1.7'], cwd='/tmp/foobar', bin_path='/tmp/foo') self.assertEqual(cmd.get_wrapped_command(), ("/bin/sh -c " "'cd /tmp/foobar && PATH=/tmp/foo:$PATH " "python /tmp/foo/pip install Django\>1.7'")) def test_unicode_output(self): '''Unicode output from command''' self.mocks.configure_mock( 'docker_client', { 'exec_create.return_value': { 'Id': b'container-foobar' }, 'exec_start.return_value': SAMPLE_UTF8_BYTES, 'exec_inspect.return_value': { 'ExitCode': 0 }, }) cmd = DockerBuildCommand(['echo', 'test'], cwd='/tmp/foobar') cmd.build_env = Mock() cmd.build_env.get_client.return_value = self.mocks.docker_client type(cmd.build_env).container_id = PropertyMock(return_value='foo') cmd.run() self.assertEqual( cmd.output, u'H\xe9r\xc9 \xee\xdf s\xf6m\xea \xfcn\xef\xe7\xf3\u2202\xe9') self.assertEqual(self.mocks.docker_client.exec_start.call_count, 1) self.assertEqual(self.mocks.docker_client.exec_create.call_count, 1) self.assertEqual(self.mocks.docker_client.exec_inspect.call_count, 1) def test_command_oom_kill(self): '''Command is OOM killed''' self.mocks.configure_mock( 'docker_client', { 'exec_create.return_value': { 'Id': b'container-foobar' }, 'exec_start.return_value': b'Killed\n', 'exec_inspect.return_value': { 'ExitCode': 137 }, }) cmd = DockerBuildCommand(['echo', 'test'], cwd='/tmp/foobar') cmd.build_env = Mock() cmd.build_env.get_client.return_value = self.mocks.docker_client type(cmd.build_env).container_id = PropertyMock(return_value='foo') cmd.run() self.assertEqual( str(cmd.output), u'Command killed due to excessive memory consumption\n')
class TestLocalEnvironment(TestCase): '''Test execution and exception handling in environment''' fixtures = ['test_data'] def setUp(self): self.project = Project.objects.get(slug='pip') self.version = Version(slug='foo', verbose_name='foobar') self.project.versions.add(self.version, bulk=False) self.mocks = EnvironmentMockGroup() self.mocks.start() def tearDown(self): self.mocks.stop() def test_normal_execution(self): '''Normal build in passing state''' self.mocks.configure_mock( 'process', {'communicate.return_value': (b'This is okay', '')}) type(self.mocks.process).returncode = PropertyMock(return_value=0) build_env = LocalEnvironment(version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}) with build_env: build_env.run('echo', 'test') self.assertTrue(self.mocks.process.communicate.called) self.assertTrue(build_env.done) self.assertTrue(build_env.successful) self.assertEqual(len(build_env.commands), 1) self.assertEqual(build_env.commands[0].output, u'This is okay') self.assertFalse(self.mocks.api()(DUMMY_BUILD_ID).put.called) def test_failing_execution(self): '''Build in failing state''' self.mocks.configure_mock( 'process', {'communicate.return_value': (b'This is not okay', '')}) type(self.mocks.process).returncode = PropertyMock(return_value=1) build_env = LocalEnvironment(version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}) with build_env: build_env.run('echo', 'test') self.fail('This should be unreachable') self.assertTrue(self.mocks.process.communicate.called) self.assertTrue(build_env.done) self.assertTrue(build_env.failed) self.assertEqual(len(build_env.commands), 1) self.assertEqual(build_env.commands[0].output, u'This is not okay') self.assertFalse(self.mocks.api()(DUMMY_BUILD_ID).put.called) def test_failing_execution_with_caught_exception(self): '''Build in failing state with BuildEnvironmentError exception''' build_env = LocalEnvironment(version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}) with build_env: raise BuildEnvironmentError('Foobar') self.assertFalse(self.mocks.process.communicate.called) self.assertEqual(len(build_env.commands), 0) self.assertTrue(build_env.done) self.assertTrue(build_env.failed) self.assertFalse(self.mocks.api()(DUMMY_BUILD_ID).put.called) def test_failing_execution_with_unexpected_exception(self): '''Build in failing state with exception from code''' build_env = LocalEnvironment(version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}) with build_env: raise ValueError('uncaught') self.assertFalse(self.mocks.process.communicate.called) self.assertTrue(build_env.done) self.assertTrue(build_env.failed) self.assertFalse(self.mocks.api()(DUMMY_BUILD_ID).put.called)
class TestDockerEnvironment(TestCase): '''Test docker build environment''' fixtures = ['test_data'] def setUp(self): self.project = Project.objects.get(slug='pip') self.version = Version(slug='foo', verbose_name='foobar') self.project.versions.add(self.version, bulk=False) self.mocks = EnvironmentMockGroup() self.mocks.start() def tearDown(self): self.mocks.stop() def test_container_id(self): '''Test docker build command''' docker = DockerEnvironment(version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}) self.assertEqual(docker.container_id, 'build-123-project-6-pip') def test_connection_failure(self): '''Connection failure on to docker socket should raise exception''' self.mocks.configure_mock('docker', {'side_effect': DockerException}) build_env = DockerEnvironment(version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}) def _inner(): with build_env: self.fail('Should not hit this') self.assertRaises(BuildEnvironmentError, _inner) self.assertFalse(self.mocks.api()(DUMMY_BUILD_ID).put.called) def test_api_failure(self): '''Failing API error response from docker should raise exception''' response = Mock(status_code=500, reason='Because') self.mocks.configure_mock( 'docker_client', { 'create_container.side_effect': DockerAPIError('Failure creating container', response, 'Failure creating container') }) build_env = DockerEnvironment(version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}) def _inner(): with build_env: self.fail('Should not hit this') self.assertRaises(BuildEnvironmentError, _inner) self.assertFalse(self.mocks.api()(DUMMY_BUILD_ID).put.called) def test_command_execution(self): '''Command execution through Docker''' self.mocks.configure_mock( 'docker_client', { 'exec_create.return_value': { 'Id': b'container-foobar' }, 'exec_start.return_value': b'This is the return', 'exec_inspect.return_value': { 'ExitCode': 1 }, }) build_env = DockerEnvironment(version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}) with build_env: build_env.run('echo test', cwd='/tmp') self.mocks.docker_client.exec_create.assert_called_with( container='build-123-project-6-pip', cmd="/bin/sh -c 'cd /tmp && echo\\ test'", stderr=True, stdout=True) self.assertEqual(build_env.commands[0].exit_code, 1) self.assertEqual(build_env.commands[0].output, u'This is the return') self.assertEqual(build_env.commands[0].error, None) self.assertTrue(build_env.failed) self.assertFalse(self.mocks.api()(DUMMY_BUILD_ID).put.called) def test_command_execution_cleanup_exception(self): '''Command execution through Docker, catch exception during cleanup''' response = Mock(status_code=500, reason='Because') self.mocks.configure_mock( 'docker_client', { 'exec_create.return_value': { 'Id': b'container-foobar' }, 'exec_start.return_value': b'This is the return', 'exec_inspect.return_value': { 'ExitCode': 0 }, 'kill.side_effect': DockerAPIError('Failure killing container', response, 'Failure killing container') }) build_env = DockerEnvironment(version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}) with build_env: build_env.run('echo', 'test', cwd='/tmp') self.mocks.docker_client.kill.assert_called_with( 'build-123-project-6-pip') self.assertTrue(build_env.successful) self.assertFalse(self.mocks.api()(DUMMY_BUILD_ID).put.called) def test_container_already_exists(self): '''Docker container already exists''' self.mocks.configure_mock( 'docker_client', { 'inspect_container.return_value': { 'State': { 'Running': True } }, 'exec_create.return_value': { 'Id': b'container-foobar' }, 'exec_start.return_value': b'This is the return', 'exec_inspect.return_value': { 'ExitCode': 0 }, }) build_env = DockerEnvironment(version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}) def _inner(): with build_env: build_env.run('echo', 'test', cwd='/tmp') self.assertRaises(BuildEnvironmentError, _inner) self.assertEqual( str(build_env.failure), 'A build environment is currently running for this version') self.assertEqual(self.mocks.docker_client.exec_create.call_count, 0) self.assertTrue(build_env.failed) self.assertFalse(self.mocks.api()(DUMMY_BUILD_ID).put.called) def test_container_timeout(self): '''Docker container timeout and command failure''' response = Mock(status_code=404, reason='Container not found') self.mocks.configure_mock( 'docker_client', { 'inspect_container.side_effect': [ DockerAPIError( 'No container found', response, 'No container found', ), { 'State': { 'Running': False, 'ExitCode': 42 } }, ], 'exec_create.return_value': { 'Id': b'container-foobar' }, 'exec_start.return_value': b'This is the return', 'exec_inspect.return_value': { 'ExitCode': 0 }, }) build_env = DockerEnvironment(version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}) with build_env: build_env.run('echo', 'test', cwd='/tmp') self.assertEqual(str(build_env.failure), 'Build exited due to time out') self.assertEqual(self.mocks.docker_client.exec_create.call_count, 1) self.assertTrue(build_env.failed) self.assertFalse(self.mocks.api()(DUMMY_BUILD_ID).put.called)
class TestLocalEnvironment(TestCase): '''Test execution and exception handling in environment''' fixtures = ['test_data'] def setUp(self): self.project = Project.objects.get(slug='pip') self.version = Version(slug='foo', verbose_name='foobar') self.project.versions.add(self.version) self.mocks = EnvironmentMockGroup() self.mocks.start() def tearDown(self): self.mocks.stop() def test_normal_execution(self): '''Normal build in passing state''' self.mocks.configure_mock('process', { 'communicate.return_value': ('This is okay', '')}) type(self.mocks.process).returncode = PropertyMock(return_value=0) build_env = LocalEnvironment(version=self.version, project=self.project, build={}) with build_env: build_env.run('echo', 'test') self.assertTrue(self.mocks.process.communicate.called) self.assertTrue(build_env.done) self.assertTrue(build_env.successful) self.assertEqual(len(build_env.commands), 1) self.assertEqual(build_env.commands[0].output, u'This is okay') def test_failing_execution(self): '''Build in failing state''' self.mocks.configure_mock('process', { 'communicate.return_value': ('This is not okay', '')}) type(self.mocks.process).returncode = PropertyMock(return_value=1) build_env = LocalEnvironment(version=self.version, project=self.project, build={}) with build_env: build_env.run('echo', 'test') self.fail('This should be unreachable') self.assertTrue(self.mocks.process.communicate.called) self.assertTrue(build_env.done) self.assertTrue(build_env.failed) self.assertEqual(len(build_env.commands), 1) self.assertEqual(build_env.commands[0].output, u'This is not okay') def test_failing_execution_with_caught_exception(self): '''Build in failing state with BuildEnvironmentError exception''' build_env = LocalEnvironment(version=self.version, project=self.project, build={}) with build_env: raise BuildEnvironmentError('Foobar') self.assertFalse(self.mocks.process.communicate.called) self.assertEqual(len(build_env.commands), 0) self.assertTrue(build_env.done) self.assertTrue(build_env.failed) def test_failing_execution_with_uncaught_exception(self): '''Build in failing state with exception from code''' build_env = LocalEnvironment(version=self.version, project=self.project, build={}) def _inner(): with build_env: raise Exception() self.assertRaises(Exception, _inner) self.assertFalse(self.mocks.process.communicate.called) self.assertTrue(build_env.done) self.assertTrue(build_env.failed)
class TestDockerEnvironment(TestCase): '''Test docker build environment''' fixtures = ['test_data'] def setUp(self): self.project = Project.objects.get(slug='pip') self.version = Version(slug='foo', verbose_name='foobar') self.project.versions.add(self.version) self.mocks = EnvironmentMockGroup() self.mocks.start() def tearDown(self): self.mocks.stop() def test_container_id(self): '''Test docker build command''' docker = DockerEnvironment(version=self.version, project=self.project, build={'id': 123}) self.assertEqual(docker.container_id, 'build-123-project-6-pip') def test_connection_failure(self): '''Connection failure on to docker socket should raise exception''' self.mocks.configure_mock('docker', { 'side_effect': DockerException }) build_env = DockerEnvironment(version=self.version, project=self.project, build={}) def _inner(): with build_env: self.fail('Should not hit this') self.assertRaises(BuildEnvironmentError, _inner) def test_api_failure(self): '''Failing API error response from docker should raise exception''' response = Mock(status_code=500, reason='Because') self.mocks.configure_mock('docker_client', { 'create_container.side_effect': DockerAPIError( 'Failure creating container', response, 'Failure creating container' ) }) build_env = DockerEnvironment(version=self.version, project=self.project, build={}) def _inner(): with build_env: self.fail('Should not hit this') self.assertRaises(BuildEnvironmentError, _inner) def test_command_execution(self): '''Command execution through Docker''' self.mocks.configure_mock('docker_client', { 'exec_create.return_value': {'Id': 'container-foobar'}, 'exec_start.return_value': 'This is the return', 'exec_inspect.return_value': {'ExitCode': 1}, }) build_env = DockerEnvironment(version=self.version, project=self.project, build={'id': 123}) with build_env: build_env.run('echo test', cwd='/tmp') self.mocks.docker_client.exec_create.assert_called_with( container='build-123-project-6-pip', cmd="/bin/sh -c 'cd /tmp && echo\\ test'", stderr=True, stdout=True ) self.assertEqual(build_env.commands[0].exit_code, 1) self.assertEqual(build_env.commands[0].output, 'This is the return') self.assertEqual(build_env.commands[0].error, None) self.assertTrue(build_env.failed) def test_command_execution_cleanup_exception(self): '''Command execution through Docker, catch exception during cleanup''' response = Mock(status_code=500, reason='Because') self.mocks.configure_mock('docker_client', { 'exec_create.return_value': {'Id': 'container-foobar'}, 'exec_start.return_value': 'This is the return', 'exec_inspect.return_value': {'ExitCode': 0}, 'kill.side_effect': DockerAPIError( 'Failure killing container', response, 'Failure killing container' ) }) build_env = DockerEnvironment(version=self.version, project=self.project, build={'id': 123}) with build_env: build_env.run('echo', 'test', cwd='/tmp') self.mocks.docker_client.kill.assert_called_with( 'build-123-project-6-pip') self.assertTrue(build_env.successful) def test_container_already_exists(self): '''Docker container already exists''' self.mocks.configure_mock('docker_client', { 'inspect_container.return_value': {'State': {'Running': True}}, 'exec_create.return_value': {'Id': 'container-foobar'}, 'exec_start.return_value': 'This is the return', 'exec_inspect.return_value': {'ExitCode': 0}, }) build_env = DockerEnvironment(version=self.version, project=self.project, build={}) def _inner(): with build_env: build_env.run('echo', 'test', cwd='/tmp') self.assertRaises(BuildEnvironmentError, _inner) self.assertEqual( str(build_env.failure), 'A build environment is currently running for this version') self.assertEqual(self.mocks.docker_client.exec_create.call_count, 0) self.assertTrue(build_env.failed) def test_container_timeout(self): '''Docker container timeout and command failure''' response = Mock(status_code=404, reason='Container not found') self.mocks.configure_mock('docker_client', { 'inspect_container.side_effect': [ DockerAPIError( 'No container found', response, 'No container found', ), {'State': {'Running': False, 'ExitCode': 42}}, ], 'exec_create.return_value': {'Id': 'container-foobar'}, 'exec_start.return_value': 'This is the return', 'exec_inspect.return_value': {'ExitCode': 0}, }) build_env = DockerEnvironment(version=self.version, project=self.project, build={}) with build_env: build_env.run('echo', 'test', cwd='/tmp') self.assertEqual( str(build_env.failure), 'Build exited due to time out') self.assertEqual(self.mocks.docker_client.exec_create.call_count, 1) self.assertTrue(build_env.failed)
class TestLocalBuildEnvironment(TestCase): """Test execution and exception handling in environment.""" fixtures = ['test_data'] def setUp(self): self.project = Project.objects.get(slug='pip') self.version = Version(slug='foo', verbose_name='foobar') self.project.versions.add(self.version, bulk=False) self.mocks = EnvironmentMockGroup() self.mocks.start() def tearDown(self): self.mocks.stop() def test_normal_execution(self): """Normal build in passing state.""" self.mocks.configure_mock('process', { 'communicate.return_value': (b'This is okay', '') }) type(self.mocks.process).returncode = PropertyMock(return_value=0) build_env = LocalBuildEnvironment( version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}, ) with build_env: build_env.run('echo', 'test') self.assertTrue(self.mocks.process.communicate.called) self.assertTrue(build_env.done) self.assertTrue(build_env.successful) self.assertEqual(len(build_env.commands), 1) self.assertEqual(build_env.commands[0].output, u'This is okay') # api() is not called anymore, we use api_v2 instead self.assertFalse(self.mocks.api()(DUMMY_BUILD_ID).put.called) self.mocks.mocks['api_v2.build']().put.assert_called_with({ 'id': DUMMY_BUILD_ID, 'version': self.version.pk, 'success': True, 'project': self.project.pk, 'setup_error': u'', 'length': mock.ANY, 'error': '', 'setup': u'', 'output': u'', 'state': u'finished', 'builder': mock.ANY, 'exit_code': 0, }) def test_incremental_state_update_with_no_update(self): """Build updates to a non-finished state when update_on_success=True.""" build_envs = [ LocalBuildEnvironment( version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}, ), LocalBuildEnvironment( version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}, update_on_success=False, ), ] for build_env in build_envs: with build_env: build_env.update_build(BUILD_STATE_CLONING) self.mocks.mocks['api_v2.build']().put.assert_called_with({ 'id': DUMMY_BUILD_ID, 'version': self.version.pk, 'success': True, 'project': self.project.pk, 'setup_error': u'', 'length': mock.ANY, 'error': '', 'setup': u'', 'output': u'', 'state': BUILD_STATE_CLONING, 'builder': mock.ANY, 'exit_code': 0, }) def test_failing_execution(self): """Build in failing state.""" self.mocks.configure_mock('process', { 'communicate.return_value': (b'This is not okay', '') }) type(self.mocks.process).returncode = PropertyMock(return_value=1) build_env = LocalBuildEnvironment( version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}, ) with build_env: build_env.run('echo', 'test') self.fail('This should be unreachable') self.assertTrue(self.mocks.process.communicate.called) self.assertTrue(build_env.done) self.assertTrue(build_env.failed) self.assertEqual(len(build_env.commands), 1) self.assertEqual(build_env.commands[0].output, u'This is not okay') # api() is not called anymore, we use api_v2 instead self.assertFalse(self.mocks.api()(DUMMY_BUILD_ID).put.called) self.mocks.mocks['api_v2.build']().put.assert_called_with({ 'id': DUMMY_BUILD_ID, 'version': self.version.pk, 'success': False, 'project': self.project.pk, 'setup_error': u'', 'length': mock.ANY, 'error': '', 'setup': u'', 'output': u'', 'state': u'finished', 'builder': mock.ANY, 'exit_code': 1, }) def test_failing_execution_with_caught_exception(self): """Build in failing state with BuildEnvironmentError exception.""" build_env = LocalBuildEnvironment( version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}, ) with build_env: raise BuildEnvironmentError('Foobar') self.assertFalse(self.mocks.process.communicate.called) self.assertEqual(len(build_env.commands), 0) self.assertTrue(build_env.done) self.assertTrue(build_env.failed) # api() is not called anymore, we use api_v2 instead self.assertFalse(self.mocks.api()(DUMMY_BUILD_ID).put.called) self.mocks.mocks['api_v2.build']().put.assert_called_with({ 'id': DUMMY_BUILD_ID, 'version': self.version.pk, 'success': False, 'project': self.project.pk, 'setup_error': u'', 'length': mock.ANY, 'error': 'Foobar', 'setup': u'', 'output': u'', 'state': u'finished', 'builder': mock.ANY, 'exit_code': 1, }) def test_failing_execution_with_unexpected_exception(self): """Build in failing state with exception from code.""" build_env = LocalBuildEnvironment( version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}, ) with build_env: raise ValueError('uncaught') self.assertFalse(self.mocks.process.communicate.called) self.assertTrue(build_env.done) self.assertTrue(build_env.failed) # api() is not called anymore, we use api_v2 instead self.assertFalse(self.mocks.api()(DUMMY_BUILD_ID).put.called) self.mocks.mocks['api_v2.build']().put.assert_called_with({ 'id': DUMMY_BUILD_ID, 'version': self.version.pk, 'success': False, 'project': self.project.pk, 'setup_error': u'', 'length': mock.ANY, 'error': ( 'There was a problem with Read the Docs while building your ' 'documentation. Please report this to us with your build id (123).' ), 'setup': u'', 'output': u'', 'state': u'finished', 'builder': mock.ANY, })
class TestDockerBuildEnvironment(TestCase): """Test docker build environment.""" fixtures = ['test_data'] def setUp(self): self.project = Project.objects.get(slug='pip') self.version = Version(slug='foo', verbose_name='foobar') self.project.versions.add(self.version, bulk=False) self.mocks = EnvironmentMockGroup() self.mocks.start() def tearDown(self): self.mocks.stop() def test_container_id(self): """Test docker build command.""" docker = DockerBuildEnvironment( version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}, ) self.assertEqual(docker.container_id, 'build-123-project-6-pip') def test_environment_successful_build(self): """A successful build exits cleanly and reports the build output.""" build_env = DockerBuildEnvironment( version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}, ) with build_env: pass self.assertTrue(build_env.successful) # api() is not called anymore, we use api_v2 instead self.assertFalse(self.mocks.api()(DUMMY_BUILD_ID).put.called) self.mocks.mocks['api_v2.build']().put.assert_called_with({ 'id': DUMMY_BUILD_ID, 'version': self.version.pk, 'success': True, 'project': self.project.pk, 'setup_error': u'', 'length': 0, 'error': '', 'setup': u'', 'output': u'', 'state': u'finished', 'builder': mock.ANY, }) def test_environment_successful_build_without_update(self): """A successful build exits cleanly and doesn't update build.""" build_env = DockerBuildEnvironment( version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}, update_on_success=False, ) with build_env: pass self.assertTrue(build_env.successful) # api() is not called anymore, we use api_v2 instead self.assertFalse(self.mocks.api()(DUMMY_BUILD_ID).put.called) self.assertFalse(self.mocks.mocks['api_v2.build']().put.called) def test_environment_failed_build_without_update_but_with_error(self): """A failed build exits cleanly and doesn't update build.""" build_env = DockerBuildEnvironment( version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}, update_on_success=False, ) with build_env: raise BuildEnvironmentError('Test') self.assertFalse(build_env.successful) # api() is not called anymore, we use api_v2 instead self.assertFalse(self.mocks.api()(DUMMY_BUILD_ID).put.called) self.mocks.mocks['api_v2.build']().put.assert_called_with({ 'id': DUMMY_BUILD_ID, 'version': self.version.pk, 'success': False, 'project': self.project.pk, 'setup_error': u'', 'exit_code': 1, 'length': 0, 'error': 'Test', 'setup': u'', 'output': u'', 'state': u'finished', 'builder': mock.ANY, }) def test_connection_failure(self): """Connection failure on to docker socket should raise exception.""" self.mocks.configure_mock('docker', {'side_effect': DockerException}) build_env = DockerBuildEnvironment( version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}, ) def _inner(): with build_env: self.fail('Should not hit this') self.assertRaises(BuildEnvironmentError, _inner) # api() is not called anymore, we use api_v2 instead self.assertFalse(self.mocks.api()(DUMMY_BUILD_ID).put.called) self.mocks.mocks['api_v2.build']().put.assert_called_with({ 'id': DUMMY_BUILD_ID, 'version': self.version.pk, 'success': False, 'project': self.project.pk, 'setup_error': u'', 'exit_code': 1, 'length': 0, 'error': ( "There was a problem with Read the Docs while building your " "documentation. Please report this to us with your build id " "(123)." ), 'setup': u'', 'output': u'', 'state': u'finished', 'builder': mock.ANY, }) def test_api_failure(self): """Failing API error response from docker should raise exception.""" response = Mock(status_code=500, reason='Because') self.mocks.configure_mock( 'docker_client', { 'create_container.side_effect': DockerAPIError( 'Failure creating container', response, 'Failure creating container') }) build_env = DockerBuildEnvironment( version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}, ) def _inner(): with build_env: self.fail('Should not hit this') self.assertRaises(BuildEnvironmentError, _inner) # api() is not called anymore, we use api_v2 instead self.assertFalse(self.mocks.api()(DUMMY_BUILD_ID).put.called) self.mocks.mocks['api_v2.build']().put.assert_called_with({ 'id': DUMMY_BUILD_ID, 'version': self.version.pk, 'success': False, 'project': self.project.pk, 'setup_error': u'', 'exit_code': 1, 'length': mock.ANY, 'error': 'Build environment creation failed', 'setup': u'', 'output': u'', 'state': u'finished', 'builder': mock.ANY, }) def test_api_failure_on_docker_memory_limit(self): """Docker exec_create raised memory issue on `exec`""" response = Mock(status_code=500, reason='Internal Server Error') self.mocks.configure_mock( 'docker_client', { 'exec_create.side_effect': DockerAPIError( 'Failure creating container', response, 'Failure creating container'), }) build_env = DockerBuildEnvironment( version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}, ) with build_env: build_env.run('echo test', cwd='/tmp') self.assertEqual(build_env.commands[0].exit_code, -1) self.assertEqual(build_env.commands[0].error, None) self.assertTrue(build_env.failed) # api() is not called anymore, we use api_v2 instead self.assertFalse(self.mocks.api()(DUMMY_BUILD_ID).put.called) self.mocks.mocks['api_v2.build']().put.assert_called_with({ 'id': DUMMY_BUILD_ID, 'version': self.version.pk, 'success': False, 'project': self.project.pk, 'setup_error': u'', 'exit_code': -1, 'length': mock.ANY, 'error': '', 'setup': u'', 'output': u'', 'state': u'finished', 'builder': mock.ANY, }) def test_api_failure_on_error_in_exit(self): response = Mock(status_code=500, reason='Internal Server Error') self.mocks.configure_mock('docker_client', { 'kill.side_effect': BuildEnvironmentError('Failed') }) build_env = DockerBuildEnvironment( version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}, ) with build_env: pass # api() is not called anymore, we use api_v2 instead self.assertFalse(self.mocks.api()(DUMMY_BUILD_ID).put.called) self.mocks.mocks['api_v2.build']().put.assert_called_with({ 'id': DUMMY_BUILD_ID, 'version': self.version.pk, 'success': False, 'project': self.project.pk, 'setup_error': u'', 'exit_code': 1, 'length': 0, 'error': 'Failed', 'setup': u'', 'output': u'', 'state': u'finished', 'builder': mock.ANY, }) def test_api_failure_returns_previous_error_on_error_in_exit(self): """ Treat previously raised errors with more priority. Don't report a connection problem to Docker on cleanup if we have a more usable error to show the user. """ response = Mock(status_code=500, reason='Internal Server Error') self.mocks.configure_mock('docker_client', { 'kill.side_effect': BuildEnvironmentError('Outer failed') }) build_env = DockerBuildEnvironment( version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}, ) with build_env: raise BuildEnvironmentError('Inner failed') # api() is not called anymore, we use api_v2 instead self.assertFalse(self.mocks.api()(DUMMY_BUILD_ID).put.called) self.mocks.mocks['api_v2.build']().put.assert_called_with({ 'id': DUMMY_BUILD_ID, 'version': self.version.pk, 'success': False, 'project': self.project.pk, 'setup_error': u'', 'exit_code': 1, 'length': 0, 'error': 'Inner failed', 'setup': u'', 'output': u'', 'state': u'finished', 'builder': mock.ANY, }) def test_command_execution(self): """Command execution through Docker.""" self.mocks.configure_mock( 'docker_client', { 'exec_create.return_value': {'Id': b'container-foobar'}, 'exec_start.return_value': b'This is the return', 'exec_inspect.return_value': {'ExitCode': 1}, }) build_env = DockerBuildEnvironment( version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}, ) with build_env: build_env.run('echo test', cwd='/tmp') self.mocks.docker_client.exec_create.assert_called_with( container='build-123-project-6-pip', cmd="/bin/sh -c 'cd /tmp && echo\\ test'", stderr=True, stdout=True) self.assertEqual(build_env.commands[0].exit_code, 1) self.assertEqual(build_env.commands[0].output, u'This is the return') self.assertEqual(build_env.commands[0].error, None) self.assertTrue(build_env.failed) # api() is not called anymore, we use api_v2 instead self.assertFalse(self.mocks.api()(DUMMY_BUILD_ID).put.called) self.mocks.mocks['api_v2.build']().put.assert_called_with({ 'id': DUMMY_BUILD_ID, 'version': self.version.pk, 'success': False, 'project': self.project.pk, 'setup_error': u'', 'exit_code': 1, 'length': 0, 'error': '', 'setup': u'', 'output': u'', 'state': u'finished', 'builder': mock.ANY, }) def test_command_execution_cleanup_exception(self): """Command execution through Docker, catch exception during cleanup.""" response = Mock(status_code=500, reason='Because') self.mocks.configure_mock( 'docker_client', { 'exec_create.return_value': {'Id': b'container-foobar'}, 'exec_start.return_value': b'This is the return', 'exec_inspect.return_value': {'ExitCode': 0}, 'kill.side_effect': DockerAPIError( 'Failure killing container', response, 'Failure killing container', ) }) build_env = DockerBuildEnvironment( version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}, ) with build_env: build_env.run('echo', 'test', cwd='/tmp') self.mocks.docker_client.kill.assert_called_with( 'build-123-project-6-pip') self.assertTrue(build_env.successful) # api() is not called anymore, we use api_v2 instead self.assertFalse(self.mocks.api()(DUMMY_BUILD_ID).put.called) self.mocks.mocks['api_v2.build']().put.assert_called_with({ 'id': DUMMY_BUILD_ID, 'version': self.version.pk, 'error': '', 'success': True, 'project': self.project.pk, 'setup_error': u'', 'exit_code': 0, 'length': 0, 'setup': u'', 'output': u'', 'state': u'finished', 'builder': mock.ANY, }) def test_container_already_exists(self): """Docker container already exists.""" self.mocks.configure_mock( 'docker_client', { 'inspect_container.return_value': {'State': {'Running': True}}, 'exec_create.return_value': {'Id': b'container-foobar'}, 'exec_start.return_value': b'This is the return', 'exec_inspect.return_value': {'ExitCode': 0}, }) build_env = DockerBuildEnvironment( version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}, ) def _inner(): with build_env: build_env.run('echo', 'test', cwd='/tmp') self.assertRaises(BuildEnvironmentError, _inner) self.assertEqual( str(build_env.failure), 'A build environment is currently running for this version') self.assertEqual(self.mocks.docker_client.exec_create.call_count, 0) self.assertTrue(build_env.failed) # api() is not called anymore, we use api_v2 instead self.assertFalse(self.mocks.api()(DUMMY_BUILD_ID).put.called) self.mocks.mocks['api_v2.build']().put.assert_called_with({ 'id': DUMMY_BUILD_ID, 'version': self.version.pk, 'success': False, 'project': self.project.pk, 'setup_error': u'', 'exit_code': 1, 'length': 0, 'error': 'A build environment is currently running for this version', 'setup': u'', 'output': u'', 'state': u'finished', 'builder': mock.ANY, }) def test_container_timeout(self): """Docker container timeout and command failure.""" response = Mock(status_code=404, reason='Container not found') self.mocks.configure_mock( 'docker_client', { 'inspect_container.side_effect': [ DockerAPIError( 'No container found', response, 'No container found', ), {'State': {'Running': False, 'ExitCode': 42}}, ], 'exec_create.return_value': {'Id': b'container-foobar'}, 'exec_start.return_value': b'This is the return', 'exec_inspect.return_value': {'ExitCode': 0}, }) build_env = DockerBuildEnvironment( version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}, ) with build_env: build_env.run('echo', 'test', cwd='/tmp') self.assertEqual(str(build_env.failure), 'Build exited due to time out') self.assertEqual(self.mocks.docker_client.exec_create.call_count, 1) self.assertTrue(build_env.failed) # api() is not called anymore, we use api_v2 instead self.assertFalse(self.mocks.api()(DUMMY_BUILD_ID).put.called) self.mocks.mocks['api_v2.build']().put.assert_called_with({ 'id': DUMMY_BUILD_ID, 'version': self.version.pk, 'success': False, 'project': self.project.pk, 'setup_error': u'', 'exit_code': 1, 'length': 0, 'error': 'Build exited due to time out', 'setup': u'', 'output': u'', 'state': u'finished', 'builder': mock.ANY, })
class TestDockerBuildCommand(TestCase): '''Test docker build commands''' def setUp(self): self.mocks = EnvironmentMockGroup() self.mocks.start() def tearDown(self): self.mocks.stop() def test_wrapped_command(self): '''Test shell wrapping for Docker chdir''' cmd = DockerBuildCommand(['pip', 'install', 'requests'], cwd='/tmp/foobar') self.assertEqual( cmd.get_wrapped_command(), ("/bin/sh -c " "'cd /tmp/foobar && " "pip install requests'")) cmd = DockerBuildCommand(['python', '/tmp/foo/pip', 'install', 'Django>1.7'], cwd='/tmp/foobar', bin_path='/tmp/foo') self.assertEqual( cmd.get_wrapped_command(), ("/bin/sh -c " "'cd /tmp/foobar && PATH=/tmp/foo:$PATH " "python /tmp/foo/pip install Django\>1.7'")) def test_unicode_output(self): '''Unicode output from command''' self.mocks.configure_mock('docker_client', { 'exec_create.return_value': {'Id': b'container-foobar'}, 'exec_start.return_value': SAMPLE_UTF8_BYTES, 'exec_inspect.return_value': {'ExitCode': 0}, }) cmd = DockerBuildCommand(['echo', 'test'], cwd='/tmp/foobar') cmd.build_env = Mock() cmd.build_env.get_client.return_value = self.mocks.docker_client type(cmd.build_env).container_id = PropertyMock(return_value='foo') cmd.run() self.assertEqual( cmd.output, u'H\xe9r\xc9 \xee\xdf s\xf6m\xea \xfcn\xef\xe7\xf3\u2202\xe9') self.assertEqual(self.mocks.docker_client.exec_start.call_count, 1) self.assertEqual(self.mocks.docker_client.exec_create.call_count, 1) self.assertEqual(self.mocks.docker_client.exec_inspect.call_count, 1) def test_command_oom_kill(self): '''Command is OOM killed''' self.mocks.configure_mock('docker_client', { 'exec_create.return_value': {'Id': b'container-foobar'}, 'exec_start.return_value': b'Killed\n', 'exec_inspect.return_value': {'ExitCode': 137}, }) cmd = DockerBuildCommand(['echo', 'test'], cwd='/tmp/foobar') cmd.build_env = Mock() cmd.build_env.get_client.return_value = self.mocks.docker_client type(cmd.build_env).container_id = PropertyMock(return_value='foo') cmd.run() self.assertEqual( str(cmd.output), u'Command killed due to excessive memory consumption\n')
class TestDockerEnvironment(TestCase): """Test docker build environment""" fixtures = ["test_data"] def setUp(self): self.project = Project.objects.get(slug="pip") self.version = Version(slug="foo", verbose_name="foobar") self.project.versions.add(self.version) self.mocks = EnvironmentMockGroup() self.mocks.start() def tearDown(self): self.mocks.stop() def test_container_id(self): """Test docker build command""" docker = DockerEnvironment(version=self.version, project=self.project, build={}) self.assertEqual(docker.container_id, "version-foobar-of-pip-20") def test_connection_failure(self): """Connection failure on to docker socket should raise exception""" self.mocks.configure_mock("docker", {"side_effect": DockerException}) build_env = DockerEnvironment(version=self.version, project=self.project, build={}) def _inner(): with build_env: self.fail("Should not hit this") self.assertRaises(BuildEnvironmentError, _inner) def test_api_failure(self): """Failing API error response from docker should raise exception""" response = Mock(status_code=500, reason="Because") self.mocks.configure_mock( "docker_client", { "create_container.side_effect": DockerAPIError( "Failure creating container", response, "Failure creating container" ) }, ) build_env = DockerEnvironment(version=self.version, project=self.project, build={}) def _inner(): with build_env: self.fail("Should not hit this") self.assertRaises(BuildEnvironmentError, _inner) def test_command_execution(self): """Command execution through Docker""" self.mocks.configure_mock( "docker_client", { "exec_create.return_value": {"Id": "container-foobar"}, "exec_start.return_value": "This is the return", "exec_inspect.return_value": {"ExitCode": 1}, }, ) build_env = DockerEnvironment(version=self.version, project=self.project, build={}) with build_env: build_env.run("echo test", cwd="/tmp") self.mocks.docker_client.exec_create.assert_called_with( container="version-foobar-of-pip-20", cmd="/bin/sh -c 'cd /tmp && echo\\ test'", stderr=True, stdout=True ) self.assertEqual(build_env.commands[0].exit_code, 1) self.assertEqual(build_env.commands[0].output, "This is the return") self.assertEqual(build_env.commands[0].error, None) self.assertTrue(build_env.failed) def test_command_execution_cleanup_exception(self): """Command execution through Docker, catch exception during cleanup""" response = Mock(status_code=500, reason="Because") self.mocks.configure_mock( "docker_client", { "exec_create.return_value": {"Id": "container-foobar"}, "exec_start.return_value": "This is the return", "exec_inspect.return_value": {"ExitCode": 0}, "kill.side_effect": DockerAPIError("Failure killing container", response, "Failure killing container"), }, ) build_env = DockerEnvironment(version=self.version, project=self.project, build={}) with build_env: build_env.run("echo", "test", cwd="/tmp") self.mocks.docker_client.kill.assert_called_with("version-foobar-of-pip-20") self.assertTrue(build_env.successful) def test_container_already_exists(self): """Docker container already exists""" self.mocks.configure_mock( "docker_client", { "inspect_container.return_value": {"State": {"Running": True}}, "exec_create.return_value": {"Id": "container-foobar"}, "exec_start.return_value": "This is the return", "exec_inspect.return_value": {"ExitCode": 0}, }, ) build_env = DockerEnvironment(version=self.version, project=self.project, build={}) def _inner(): with build_env: build_env.run("echo", "test", cwd="/tmp") self.assertRaises(BuildEnvironmentError, _inner) self.assertEqual(str(build_env.failure), "A build environment is currently running for this version") self.assertEqual(self.mocks.docker_client.exec_create.call_count, 0) self.assertTrue(build_env.failed) def test_container_timeout(self): """Docker container timeout and command failure""" response = Mock(status_code=404, reason="Container not found") self.mocks.configure_mock( "docker_client", { "inspect_container.side_effect": [ DockerAPIError("No container found", response, "No container found"), {"State": {"Running": False, "ExitCode": 42}}, ], "exec_create.return_value": {"Id": "container-foobar"}, "exec_start.return_value": "This is the return", "exec_inspect.return_value": {"ExitCode": 0}, }, ) build_env = DockerEnvironment(version=self.version, project=self.project, build={}) with build_env: build_env.run("echo", "test", cwd="/tmp") self.assertEqual(str(build_env.failure), "Build exited due to time out") self.assertEqual(self.mocks.docker_client.exec_create.call_count, 1) self.assertTrue(build_env.failed)
class TestLocalEnvironment(TestCase): """Test execution and exception handling in environment""" fixtures = ["test_data"] def setUp(self): self.project = Project.objects.get(slug="pip") self.version = Version(slug="foo", verbose_name="foobar") self.project.versions.add(self.version) self.mocks = EnvironmentMockGroup() self.mocks.start() def tearDown(self): self.mocks.stop() def test_normal_execution(self): """Normal build in passing state""" self.mocks.configure_mock("process", {"communicate.return_value": ("This is okay", "")}) type(self.mocks.process).returncode = PropertyMock(return_value=0) build_env = LocalEnvironment(version=self.version, project=self.project, build={}) with build_env: build_env.run("echo", "test") self.assertTrue(self.mocks.process.communicate.called) self.assertTrue(build_env.done) self.assertTrue(build_env.successful) self.assertEqual(len(build_env.commands), 1) self.assertEqual(build_env.commands[0].output, u"This is okay") def test_failing_execution(self): """Build in failing state""" self.mocks.configure_mock("process", {"communicate.return_value": ("This is not okay", "")}) type(self.mocks.process).returncode = PropertyMock(return_value=1) build_env = LocalEnvironment(version=self.version, project=self.project, build={}) with build_env: build_env.run("echo", "test") self.fail("This should be unreachable") self.assertTrue(self.mocks.process.communicate.called) self.assertTrue(build_env.done) self.assertTrue(build_env.failed) self.assertEqual(len(build_env.commands), 1) self.assertEqual(build_env.commands[0].output, u"This is not okay") def test_failing_execution_with_caught_exception(self): """Build in failing state with BuildEnvironmentError exception""" build_env = LocalEnvironment(version=self.version, project=self.project, build={}) with build_env: raise BuildEnvironmentError("Foobar") self.assertFalse(self.mocks.process.communicate.called) self.assertEqual(len(build_env.commands), 0) self.assertTrue(build_env.done) self.assertTrue(build_env.failed) def test_failing_execution_with_uncaught_exception(self): """Build in failing state with exception from code""" build_env = LocalEnvironment(version=self.version, project=self.project, build={}) def _inner(): with build_env: raise Exception() self.assertRaises(Exception, _inner) self.assertFalse(self.mocks.process.communicate.called) self.assertTrue(build_env.done) self.assertTrue(build_env.failed)
class TestLocalEnvironment(TestCase): """Test execution and exception handling in environment.""" fixtures = ['test_data'] def setUp(self): self.project = Project.objects.get(slug='pip') self.version = Version(slug='foo', verbose_name='foobar') self.project.versions.add(self.version, bulk=False) self.mocks = EnvironmentMockGroup() self.mocks.start() def tearDown(self): self.mocks.stop() def test_normal_execution(self): """Normal build in passing state.""" self.mocks.configure_mock('process', { 'communicate.return_value': (b'This is okay', '') }) type(self.mocks.process).returncode = PropertyMock(return_value=0) build_env = LocalEnvironment( version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}, ) with build_env: build_env.run('echo', 'test') self.assertTrue(self.mocks.process.communicate.called) self.assertTrue(build_env.done) self.assertTrue(build_env.successful) self.assertEqual(len(build_env.commands), 1) self.assertEqual(build_env.commands[0].output, u'This is okay') # api() is not called anymore, we use api_v2 instead self.assertFalse(self.mocks.api()(DUMMY_BUILD_ID).put.called) self.mocks.mocks['api_v2.build']().put.assert_called_with({ 'id': DUMMY_BUILD_ID, 'version': self.version.pk, 'success': True, 'project': self.project.pk, 'setup_error': u'', 'length': mock.ANY, 'error': '', 'setup': u'', 'output': u'', 'state': u'finished', 'builder': mock.ANY, 'exit_code': 0, }) def test_incremental_state_update_with_no_update(self): """Build updates to a non-finished state when update_on_success=True.""" build_envs = [ LocalEnvironment( version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}, ), LocalEnvironment( version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}, update_on_success=False, ), ] for build_env in build_envs: with build_env: build_env.update_build(BUILD_STATE_CLONING) self.mocks.mocks['api_v2.build']().put.assert_called_with({ 'id': DUMMY_BUILD_ID, 'version': self.version.pk, 'success': True, 'project': self.project.pk, 'setup_error': u'', 'length': mock.ANY, 'error': '', 'setup': u'', 'output': u'', 'state': BUILD_STATE_CLONING, 'builder': mock.ANY, 'exit_code': 0, }) def test_failing_execution(self): """Build in failing state.""" self.mocks.configure_mock('process', { 'communicate.return_value': (b'This is not okay', '') }) type(self.mocks.process).returncode = PropertyMock(return_value=1) build_env = LocalEnvironment( version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}, ) with build_env: build_env.run('echo', 'test') self.fail('This should be unreachable') self.assertTrue(self.mocks.process.communicate.called) self.assertTrue(build_env.done) self.assertTrue(build_env.failed) self.assertEqual(len(build_env.commands), 1) self.assertEqual(build_env.commands[0].output, u'This is not okay') # api() is not called anymore, we use api_v2 instead self.assertFalse(self.mocks.api()(DUMMY_BUILD_ID).put.called) self.mocks.mocks['api_v2.build']().put.assert_called_with({ 'id': DUMMY_BUILD_ID, 'version': self.version.pk, 'success': False, 'project': self.project.pk, 'setup_error': u'', 'length': mock.ANY, 'error': '', 'setup': u'', 'output': u'', 'state': u'finished', 'builder': mock.ANY, 'exit_code': 1, }) def test_failing_execution_with_caught_exception(self): """Build in failing state with BuildEnvironmentError exception.""" build_env = LocalEnvironment( version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}, ) with build_env: raise BuildEnvironmentError('Foobar') self.assertFalse(self.mocks.process.communicate.called) self.assertEqual(len(build_env.commands), 0) self.assertTrue(build_env.done) self.assertTrue(build_env.failed) # api() is not called anymore, we use api_v2 instead self.assertFalse(self.mocks.api()(DUMMY_BUILD_ID).put.called) self.mocks.mocks['api_v2.build']().put.assert_called_with({ 'id': DUMMY_BUILD_ID, 'version': self.version.pk, 'success': False, 'project': self.project.pk, 'setup_error': u'', 'length': mock.ANY, 'error': 'Foobar', 'setup': u'', 'output': u'', 'state': u'finished', 'builder': mock.ANY, 'exit_code': 1, }) def test_failing_execution_with_unexpected_exception(self): """Build in failing state with exception from code.""" build_env = LocalEnvironment( version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}, ) with build_env: raise ValueError('uncaught') self.assertFalse(self.mocks.process.communicate.called) self.assertTrue(build_env.done) self.assertTrue(build_env.failed) # api() is not called anymore, we use api_v2 instead self.assertFalse(self.mocks.api()(DUMMY_BUILD_ID).put.called) self.mocks.mocks['api_v2.build']().put.assert_called_with({ 'id': DUMMY_BUILD_ID, 'version': self.version.pk, 'success': False, 'project': self.project.pk, 'setup_error': u'', 'length': mock.ANY, 'error': ( 'There was a problem with Read the Docs while building your ' 'documentation. Please report this to us with your build id (123).' ), 'setup': u'', 'output': u'', 'state': u'finished', 'builder': mock.ANY, })
class TestDockerEnvironment(TestCase): """Test docker build environment.""" fixtures = ['test_data'] def setUp(self): self.project = Project.objects.get(slug='pip') self.version = Version(slug='foo', verbose_name='foobar') self.project.versions.add(self.version, bulk=False) self.mocks = EnvironmentMockGroup() self.mocks.start() def tearDown(self): self.mocks.stop() def test_container_id(self): """Test docker build command.""" docker = DockerEnvironment( version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}, ) self.assertEqual(docker.container_id, 'build-123-project-6-pip') def test_environment_successful_build(self): """A successful build exits cleanly and reports the build output.""" build_env = DockerEnvironment( version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}, ) with build_env: pass self.assertTrue(build_env.successful) # api() is not called anymore, we use api_v2 instead self.assertFalse(self.mocks.api()(DUMMY_BUILD_ID).put.called) self.mocks.mocks['api_v2.build']().put.assert_called_with({ 'id': DUMMY_BUILD_ID, 'version': self.version.pk, 'success': True, 'project': self.project.pk, 'setup_error': u'', 'length': 0, 'error': '', 'setup': u'', 'output': u'', 'state': u'finished', 'builder': mock.ANY, }) def test_environment_successful_build_without_update(self): """A successful build exits cleanly and doesn't update build.""" build_env = DockerEnvironment( version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}, update_on_success=False, ) with build_env: pass self.assertTrue(build_env.successful) # api() is not called anymore, we use api_v2 instead self.assertFalse(self.mocks.api()(DUMMY_BUILD_ID).put.called) self.assertFalse(self.mocks.mocks['api_v2.build']().put.called) def test_environment_failed_build_without_update_but_with_error(self): """A failed build exits cleanly and doesn't update build.""" build_env = DockerEnvironment( version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}, update_on_success=False, ) with build_env: raise BuildEnvironmentError('Test') self.assertFalse(build_env.successful) # api() is not called anymore, we use api_v2 instead self.assertFalse(self.mocks.api()(DUMMY_BUILD_ID).put.called) self.mocks.mocks['api_v2.build']().put.assert_called_with({ 'id': DUMMY_BUILD_ID, 'version': self.version.pk, 'success': False, 'project': self.project.pk, 'setup_error': u'', 'exit_code': 1, 'length': 0, 'error': 'Test', 'setup': u'', 'output': u'', 'state': u'finished', 'builder': mock.ANY, }) def test_connection_failure(self): """Connection failure on to docker socket should raise exception.""" self.mocks.configure_mock('docker', {'side_effect': DockerException}) build_env = DockerEnvironment( version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}, ) def _inner(): with build_env: self.fail('Should not hit this') self.assertRaises(BuildEnvironmentError, _inner) # api() is not called anymore, we use api_v2 instead self.assertFalse(self.mocks.api()(DUMMY_BUILD_ID).put.called) self.mocks.mocks['api_v2.build']().put.assert_called_with({ 'id': DUMMY_BUILD_ID, 'version': self.version.pk, 'success': False, 'project': self.project.pk, 'setup_error': u'', 'exit_code': 1, 'length': 0, 'error': ( "There was a problem with Read the Docs while building your " "documentation. Please report this to us with your build id " "(123)." ), 'setup': u'', 'output': u'', 'state': u'finished', 'builder': mock.ANY, }) def test_api_failure(self): """Failing API error response from docker should raise exception.""" response = Mock(status_code=500, reason='Because') self.mocks.configure_mock( 'docker_client', { 'create_container.side_effect': DockerAPIError( 'Failure creating container', response, 'Failure creating container') }) build_env = DockerEnvironment( version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}, ) def _inner(): with build_env: self.fail('Should not hit this') self.assertRaises(BuildEnvironmentError, _inner) # api() is not called anymore, we use api_v2 instead self.assertFalse(self.mocks.api()(DUMMY_BUILD_ID).put.called) self.mocks.mocks['api_v2.build']().put.assert_called_with({ 'id': DUMMY_BUILD_ID, 'version': self.version.pk, 'success': False, 'project': self.project.pk, 'setup_error': u'', 'exit_code': 1, 'length': mock.ANY, 'error': 'Build environment creation failed', 'setup': u'', 'output': u'', 'state': u'finished', 'builder': mock.ANY, }) def test_api_failure_on_docker_memory_limit(self): """Docker exec_create raised memory issue on `exec`""" response = Mock(status_code=500, reason='Internal Server Error') self.mocks.configure_mock( 'docker_client', { 'exec_create.side_effect': DockerAPIError( 'Failure creating container', response, 'Failure creating container'), }) build_env = DockerEnvironment( version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}, ) with build_env: build_env.run('echo test', cwd='/tmp') self.assertEqual(build_env.commands[0].exit_code, -1) self.assertEqual(build_env.commands[0].error, None) self.assertTrue(build_env.failed) # api() is not called anymore, we use api_v2 instead self.assertFalse(self.mocks.api()(DUMMY_BUILD_ID).put.called) self.mocks.mocks['api_v2.build']().put.assert_called_with({ 'id': DUMMY_BUILD_ID, 'version': self.version.pk, 'success': False, 'project': self.project.pk, 'setup_error': u'', 'exit_code': -1, 'length': mock.ANY, 'error': '', 'setup': u'', 'output': u'', 'state': u'finished', 'builder': mock.ANY, }) def test_api_failure_on_error_in_exit(self): response = Mock(status_code=500, reason='Internal Server Error') self.mocks.configure_mock('docker_client', { 'kill.side_effect': BuildEnvironmentError('Failed') }) build_env = DockerEnvironment( version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}, ) with build_env: pass # api() is not called anymore, we use api_v2 instead self.assertFalse(self.mocks.api()(DUMMY_BUILD_ID).put.called) self.mocks.mocks['api_v2.build']().put.assert_called_with({ 'id': DUMMY_BUILD_ID, 'version': self.version.pk, 'success': False, 'project': self.project.pk, 'setup_error': u'', 'exit_code': 1, 'length': 0, 'error': 'Failed', 'setup': u'', 'output': u'', 'state': u'finished', 'builder': mock.ANY, }) def test_api_failure_returns_previous_error_on_error_in_exit(self): """ Treat previously raised errors with more priority. Don't report a connection problem to Docker on cleanup if we have a more usable error to show the user. """ response = Mock(status_code=500, reason='Internal Server Error') self.mocks.configure_mock('docker_client', { 'kill.side_effect': BuildEnvironmentError('Outer failed') }) build_env = DockerEnvironment( version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}, ) with build_env: raise BuildEnvironmentError('Inner failed') # api() is not called anymore, we use api_v2 instead self.assertFalse(self.mocks.api()(DUMMY_BUILD_ID).put.called) self.mocks.mocks['api_v2.build']().put.assert_called_with({ 'id': DUMMY_BUILD_ID, 'version': self.version.pk, 'success': False, 'project': self.project.pk, 'setup_error': u'', 'exit_code': 1, 'length': 0, 'error': 'Inner failed', 'setup': u'', 'output': u'', 'state': u'finished', 'builder': mock.ANY, }) def test_command_execution(self): """Command execution through Docker.""" self.mocks.configure_mock( 'docker_client', { 'exec_create.return_value': {'Id': b'container-foobar'}, 'exec_start.return_value': b'This is the return', 'exec_inspect.return_value': {'ExitCode': 1}, }) build_env = DockerEnvironment( version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}, ) with build_env: build_env.run('echo test', cwd='/tmp') self.mocks.docker_client.exec_create.assert_called_with( container='build-123-project-6-pip', cmd="/bin/sh -c 'cd /tmp && echo\\ test'", stderr=True, stdout=True) self.assertEqual(build_env.commands[0].exit_code, 1) self.assertEqual(build_env.commands[0].output, u'This is the return') self.assertEqual(build_env.commands[0].error, None) self.assertTrue(build_env.failed) # api() is not called anymore, we use api_v2 instead self.assertFalse(self.mocks.api()(DUMMY_BUILD_ID).put.called) self.mocks.mocks['api_v2.build']().put.assert_called_with({ 'id': DUMMY_BUILD_ID, 'version': self.version.pk, 'success': False, 'project': self.project.pk, 'setup_error': u'', 'exit_code': 1, 'length': 0, 'error': '', 'setup': u'', 'output': u'', 'state': u'finished', 'builder': mock.ANY, }) def test_command_execution_cleanup_exception(self): """Command execution through Docker, catch exception during cleanup.""" response = Mock(status_code=500, reason='Because') self.mocks.configure_mock( 'docker_client', { 'exec_create.return_value': {'Id': b'container-foobar'}, 'exec_start.return_value': b'This is the return', 'exec_inspect.return_value': {'ExitCode': 0}, 'kill.side_effect': DockerAPIError( 'Failure killing container', response, 'Failure killing container', ) }) build_env = DockerEnvironment( version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}, ) with build_env: build_env.run('echo', 'test', cwd='/tmp') self.mocks.docker_client.kill.assert_called_with( 'build-123-project-6-pip') self.assertTrue(build_env.successful) # api() is not called anymore, we use api_v2 instead self.assertFalse(self.mocks.api()(DUMMY_BUILD_ID).put.called) self.mocks.mocks['api_v2.build']().put.assert_called_with({ 'id': DUMMY_BUILD_ID, 'version': self.version.pk, 'error': '', 'success': True, 'project': self.project.pk, 'setup_error': u'', 'exit_code': 0, 'length': 0, 'setup': u'', 'output': u'', 'state': u'finished', 'builder': mock.ANY, }) def test_container_already_exists(self): """Docker container already exists.""" self.mocks.configure_mock( 'docker_client', { 'inspect_container.return_value': {'State': {'Running': True}}, 'exec_create.return_value': {'Id': b'container-foobar'}, 'exec_start.return_value': b'This is the return', 'exec_inspect.return_value': {'ExitCode': 0}, }) build_env = DockerEnvironment( version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}, ) def _inner(): with build_env: build_env.run('echo', 'test', cwd='/tmp') self.assertRaises(BuildEnvironmentError, _inner) self.assertEqual( str(build_env.failure), 'A build environment is currently running for this version') self.assertEqual(self.mocks.docker_client.exec_create.call_count, 0) self.assertTrue(build_env.failed) # api() is not called anymore, we use api_v2 instead self.assertFalse(self.mocks.api()(DUMMY_BUILD_ID).put.called) self.mocks.mocks['api_v2.build']().put.assert_called_with({ 'id': DUMMY_BUILD_ID, 'version': self.version.pk, 'success': False, 'project': self.project.pk, 'setup_error': u'', 'exit_code': 1, 'length': 0, 'error': 'A build environment is currently running for this version', 'setup': u'', 'output': u'', 'state': u'finished', 'builder': mock.ANY, }) def test_container_timeout(self): """Docker container timeout and command failure.""" response = Mock(status_code=404, reason='Container not found') self.mocks.configure_mock( 'docker_client', { 'inspect_container.side_effect': [ DockerAPIError( 'No container found', response, 'No container found', ), {'State': {'Running': False, 'ExitCode': 42}}, ], 'exec_create.return_value': {'Id': b'container-foobar'}, 'exec_start.return_value': b'This is the return', 'exec_inspect.return_value': {'ExitCode': 0}, }) build_env = DockerEnvironment( version=self.version, project=self.project, build={'id': DUMMY_BUILD_ID}, ) with build_env: build_env.run('echo', 'test', cwd='/tmp') self.assertEqual(str(build_env.failure), 'Build exited due to time out') self.assertEqual(self.mocks.docker_client.exec_create.call_count, 1) self.assertTrue(build_env.failed) # api() is not called anymore, we use api_v2 instead self.assertFalse(self.mocks.api()(DUMMY_BUILD_ID).put.called) self.mocks.mocks['api_v2.build']().put.assert_called_with({ 'id': DUMMY_BUILD_ID, 'version': self.version.pk, 'success': False, 'project': self.project.pk, 'setup_error': u'', 'exit_code': 1, 'length': 0, 'error': 'Build exited due to time out', 'setup': u'', 'output': u'', 'state': u'finished', 'builder': mock.ANY, })