blob: 73e91cff96a85eb0b92f6ffbb3b905299146d094 [file] [log] [blame]
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import contextlib
import pytest
from mock import Mock, call, create_autospec, patch
from twitter.common.contextutil import temporary_file
from apache.aurora.client.cli import (
EXIT_COMMAND_FAILURE,
EXIT_INTERRUPTED,
EXIT_INVALID_CONFIGURATION,
EXIT_OK,
Context
)
from apache.aurora.client.cli.client import AuroraCommandLine
from apache.aurora.client.cli.jobs import CreateJobCommand
from apache.aurora.common.aurora_job_key import AuroraJobKey
from apache.aurora.config import AuroraConfig
from .util import AuroraClientCommandTest, FakeAuroraCommandContext, mock_verb_options
from gen.apache.aurora.api.ttypes import (
JobKey,
ResponseCode,
Result,
ScheduleStatus,
ScheduleStatusResult,
TaskQuery
)
class UnknownException(Exception):
pass
class TestCreateJobCommand(AuroraClientCommandTest):
def test_create_with_lock(self):
command = CreateJobCommand()
jobkey = AuroraJobKey("cluster", "role", "env", "job")
mock_options = mock_verb_options(command)
mock_options.jobspec = jobkey
mock_options.config_file = "/tmp/whatever"
fake_context = FakeAuroraCommandContext()
fake_context.set_options(mock_options)
mock_config = create_autospec(spec=AuroraConfig, spec_set=True, instance=True)
mock_job_config = Mock()
mock_job_config.has_cron_schedule.return_value = False
mock_config.raw.return_value = mock_job_config
fake_context.get_job_config = Mock(return_value=mock_config)
mock_api = fake_context.get_api("test")
mock_api.create_job.return_value = AuroraClientCommandTest.create_blank_response(
ResponseCode.JOB_UPDATING_ERROR, "Error.")
with pytest.raises(Context.CommandError):
command.execute(fake_context)
mock_api.create_job.assert_called_once_with(mock_config)
self.assert_lock_message(fake_context)
class TestClientCreateCommand(AuroraClientCommandTest):
@classmethod
def create_mock_status_query_result(cls, scheduleStatus):
query_result = cls.create_simple_success_response()
query_result.result = Result(scheduleStatusResult=ScheduleStatusResult(tasks=[
cls.create_scheduled_task(0, initial_time=1000, status=scheduleStatus),
cls.create_scheduled_task(1, initial_time=1004, status=scheduleStatus)
]))
return query_result
@classmethod
def create_query(cls):
return TaskQuery(
jobKeys=[JobKey(role=cls.TEST_ROLE, environment=cls.TEST_ENV, name=cls.TEST_JOB)])
@classmethod
def get_createjob_response(cls):
# Then, we call api.create_job(config)
return cls.create_simple_success_response()
@classmethod
def get_failed_createjob_response(cls):
return cls.create_error_response()
@classmethod
def assert_create_job_called(cls, mock_api):
# Check that create_job was called exactly once, with an AuroraConfig parameter.
assert mock_api.create_job.call_count == 1
assert isinstance(mock_api.create_job.call_args_list[0][0][0], AuroraConfig)
@classmethod
def assert_scheduler_called(cls, mock_api, mock_query, num_queries):
assert mock_api.scheduler_proxy.getTasksWithoutConfigs.call_count == num_queries
mock_api.scheduler_proxy.getTasksWithoutConfigs.assert_called_with(mock_query, retry=False)
def test_simple_successful_create_job(self):
"""Run a test of the "create" command against a mocked-out API:
Verifies that the creation command sends the right API RPCs, and performs the correct
tests on the result."""
# We'll patch out create_context, which will give us a fake context
# object, and everything can be stubbed through that.
mock_context = FakeAuroraCommandContext()
with contextlib.nested(
# TODO(maxim): Patching threading.Event with all possible namespace/patch/mock
# combinations did not produce the desired effect. Investigate why (AURORA-510)
patch('threading._Event.wait'),
patch('apache.aurora.client.cli.jobs.Job.create_context', return_value=mock_context)):
# After making the client, create sets up a job monitor.
# The monitor uses TaskQuery to get the tasks. It's called at least twice:once before
# the job is created, and once after. So we need to set up mocks for the query results.
mock_query = self.create_query()
mock_context.add_expected_status_query_result(
self.create_mock_status_query_result(ScheduleStatus.PENDING))
mock_context.add_expected_status_query_result(
self.create_mock_status_query_result(ScheduleStatus.RUNNING))
api = mock_context.get_api('west')
api.create_job.return_value = self.get_createjob_response()
api.get_tier_configs.return_value = self.get_mock_tier_configurations()
# This is the real test: invoke create as if it had been called by the command line.
with temporary_file() as fp:
fp.write(self.get_valid_config())
fp.flush()
cmd = AuroraCommandLine()
cmd.execute(['job', 'create', '--wait-until=RUNNING', 'west/bozo/test/hello',
fp.name])
# Now check that the right API calls got made.
# Check that create_job was called exactly once, with an AuroraConfig parameter.
self.assert_create_job_called(api)
self.assert_scheduler_called(api, mock_query, 2)
def test_simple_successful_create_job_open_page(self):
mock_context = FakeAuroraCommandContext()
with contextlib.nested(
# TODO(maxim): Patching threading.Event with all possible namespace/patch/mock
# combinations did not produce the desired effect. Investigate why (AURORA-510)
patch('threading._Event.wait'),
patch('apache.aurora.client.cli.jobs.Job.create_context', return_value=mock_context)):
mock_query = self.create_query()
mock_context.add_expected_status_query_result(
self.create_mock_status_query_result(ScheduleStatus.PENDING))
mock_context.add_expected_status_query_result(
self.create_mock_status_query_result(ScheduleStatus.RUNNING))
api = mock_context.get_api('west')
api.create_job.return_value = self.get_createjob_response()
api.get_tier_configs.return_value = self.get_mock_tier_configurations()
with temporary_file() as fp:
fp.write(self.get_valid_config())
fp.flush()
cmd = AuroraCommandLine()
result = cmd.execute(['job', 'create', '--wait-until=RUNNING', '--open-browser',
'west/bozo/test/hello',
fp.name])
assert result == EXIT_OK
self.assert_create_job_called(api)
self.assert_scheduler_called(api, mock_query, 2)
assert self.mock_webbrowser.mock_calls == [
call("http://something_or_other/scheduler/bozo/test/hello")
]
def test_create_job_delayed(self):
"""Run a test of the "create" command against a mocked-out API:
this time, make the monitor check status several times before successful completion.
"""
mock_context = FakeAuroraCommandContext()
with contextlib.nested(
patch('threading._Event.wait'),
patch('apache.aurora.client.cli.jobs.Job.create_context', return_value=mock_context)):
mock_query = self.create_query()
for result in [ScheduleStatus.PENDING, ScheduleStatus.PENDING, ScheduleStatus.RUNNING]:
mock_context.add_expected_status_query_result(self.create_mock_status_query_result(result))
api = mock_context.get_api('west')
api.create_job.return_value = self.get_createjob_response()
api.get_tier_configs.return_value = self.get_mock_tier_configurations()
with temporary_file() as fp:
fp.write(self.get_valid_config())
fp.flush()
cmd = AuroraCommandLine()
cmd.execute(['job', 'create', '--wait-until=RUNNING', 'west/bozo/test/hello',
fp.name])
self.assert_create_job_called(api)
self.assert_scheduler_called(api, mock_query, 3)
def test_create_job_failed(self):
"""Run a test of the "create" command against a mocked-out API:
this time, make the monitor check status several times before successful completion.
"""
mock_context = FakeAuroraCommandContext()
with patch('apache.aurora.client.cli.jobs.Job.create_context', return_value=mock_context):
mock_context.add_expected_status_query_result(
self.create_mock_status_query_result(ScheduleStatus.INIT))
api = mock_context.get_api('west')
api.create_job.return_value = self.get_failed_createjob_response()
api.get_tier_configs.return_value = self.get_mock_tier_configurations()
with temporary_file() as fp:
fp.write(self.get_valid_config())
fp.flush()
cmd = AuroraCommandLine()
result = cmd.execute(['job', 'create', '--wait-until=RUNNING',
'west/bozo/test/hello', fp.name])
assert result == EXIT_COMMAND_FAILURE
# Check that create_job was called exactly once, with an AuroraConfig parameter.
self.assert_create_job_called(api)
def test_create_job_failed_invalid_config(self):
"""Run a test of the "create" command against a mocked-out API, with a configuration
containing a syntax error"""
mock_context = FakeAuroraCommandContext()
with patch('apache.aurora.client.cli.jobs.Job.create_context', return_value=mock_context):
with temporary_file() as fp:
fp.write(self.get_invalid_config('invalid_clause=oops'))
fp.flush()
cmd = AuroraCommandLine()
result = cmd.execute(['job', 'create', '--wait-until=RUNNING',
'west/bozo/test/hello', fp.name])
assert result == EXIT_INVALID_CONFIGURATION
# Now check that the right API calls got made.
# Check that create_job was not called.
api = mock_context.get_api('west')
assert api.create_job.call_count == 0
def test_interrupt(self):
mock_context = FakeAuroraCommandContext()
with contextlib.nested(
patch('time.sleep'),
patch('apache.aurora.client.cli.jobs.Job.create_context',
side_effect=KeyboardInterrupt())):
api = mock_context.get_api('west')
api.create_job.return_value = self.get_createjob_response()
with temporary_file() as fp:
fp.write(self.get_valid_config())
fp.flush()
cmd = AuroraCommandLine()
result = cmd.execute(['job', 'create', '--wait-until=RUNNING', 'west/bozo/test/hello',
fp.name])
assert result == EXIT_INTERRUPTED
assert api.create_job.call_count == 0
def test_interrupt_error(self):
mock_context = FakeAuroraCommandContext()
with contextlib.nested(
patch('time.sleep'),
patch('time.time', return_value=23),
patch('apache.aurora.client.cli.jobs.Job.create_context', side_effect=UnknownException())):
api = mock_context.get_api('west')
api.create_job.return_value = self.get_createjob_response()
with temporary_file() as fp:
fp.write(self.get_valid_config())
fp.flush()
cmd = AuroraCommandLine()
with pytest.raises(UnknownException):
cmd.execute(['job', 'create', '--wait-until=RUNNING', 'west/bozo/test/hello', fp.name])
def test_simple_successful_create_job_output(self):
"""Run a test of the "create" command against a mocked-out API:
Verifies that the creation command generates the correct output.
"""
mock_context = FakeAuroraCommandContext()
with contextlib.nested(
patch('threading._Event.wait'),
patch('apache.aurora.client.cli.jobs.Job.create_context', return_value=mock_context)):
mock_context.add_expected_status_query_result(
self.create_mock_status_query_result(ScheduleStatus.PENDING))
mock_context.add_expected_status_query_result(
self.create_mock_status_query_result(ScheduleStatus.RUNNING))
mock_context.add_expected_status_query_result(
self.create_mock_status_query_result(ScheduleStatus.RUNNING))
api = mock_context.get_api('west')
api.create_job.return_value = self.get_createjob_response()
api.get_tier_configs.return_value = self.get_mock_tier_configurations()
with temporary_file() as fp:
fp.write(self.get_valid_config())
fp.flush()
cmd = AuroraCommandLine()
result = cmd.execute(['job', 'create', '--wait-until=RUNNING', 'west/bozo/test/hello',
fp.name])
assert result == EXIT_OK
assert mock_context.get_out() == [
"Job create succeeded: job url=http://something_or_other/scheduler/bozo/test/hello"]
assert mock_context.get_err() == []
def test_create_job_startup_fails(self):
mock_context = FakeAuroraCommandContext()
with contextlib.nested(
patch('threading._Event.wait'),
patch('apache.aurora.client.cli.jobs.Job.create_context', return_value=mock_context)):
mock_context.add_expected_status_query_result(
self.create_mock_status_query_result(ScheduleStatus.PENDING))
mock_context.add_expected_status_query_result(
self.create_mock_status_query_result(ScheduleStatus.RUNNING))
# We need to override the side_effect behavior of check_status in the context.
def check_status_side_effect(*args):
return self.create_error_response()
mock_context.get_api("west").check_status.side_effect = check_status_side_effect
api = mock_context.get_api('west')
api.create_job.return_value = self.get_createjob_response()
api.get_tier_configs.return_value = self.get_mock_tier_configurations()
with temporary_file() as fp:
fp.write(self.get_valid_config())
fp.flush()
cmd = AuroraCommandLine()
result = cmd.execute(['job', 'create', '--wait-until=RUNNING', 'west/bozo/test/hello',
fp.name])
assert result == EXIT_COMMAND_FAILURE
assert mock_context.get_out() == []
assert mock_context.get_err() == ["Error occurred while creating job west/bozo/test/hello"]
def test_create_job_failed_output(self):
"""Test that a failed create generates the correct error messages"""
mock_context = FakeAuroraCommandContext()
with patch('apache.aurora.client.cli.jobs.Job.create_context', return_value=mock_context):
mock_context.add_expected_status_query_result(
self.create_mock_status_query_result(ScheduleStatus.INIT))
api = mock_context.get_api('west')
api.create_job.return_value = self.get_failed_createjob_response()
api.get_tier_configs.return_value = self.get_mock_tier_configurations()
with temporary_file() as fp:
fp.write(self.get_valid_config())
fp.flush()
cmd = AuroraCommandLine()
result = cmd.execute(['job', 'create', '--wait-until=RUNNING',
'west/bozo/test/hello', fp.name])
assert result == EXIT_COMMAND_FAILURE
# Check that create_job was called exactly once, with an AuroraConfig parameter.
assert mock_context.get_out() == []
assert mock_context.get_err() == [
'Job creation failed due to error:', '\tWhoops']
def test_simple_successful_create_job_with_bindings(self):
"""Run a test of the "create" command against a mocked-out API:
Verifies that the creation command sends the right API RPCs, and performs the correct
tests on the result."""
mock_context = FakeAuroraCommandContext()
with contextlib.nested(
patch('threading._Event.wait'),
patch('apache.aurora.client.cli.jobs.Job.create_context', return_value=mock_context)):
mock_query = self.create_query()
mock_context.add_expected_status_query_result(
self.create_mock_status_query_result(ScheduleStatus.PENDING))
mock_context.add_expected_status_query_result(
self.create_mock_status_query_result(ScheduleStatus.RUNNING))
api = mock_context.get_api('west')
api.create_job.return_value = self.get_createjob_response()
api.get_tier_configs.return_value = self.get_mock_tier_configurations()
# This is the real test: invoke create as if it had been called by the command line.
with temporary_file() as fp:
fp.write(self.get_unbound_test_config())
fp.flush()
cmd = AuroraCommandLine()
cmd.execute(['job', 'create', '--wait-until=RUNNING', '--bind', 'cluster_binding=west',
'--bind', 'instances_binding=20', '--bind', 'TEST_BATCH=1',
'--bind', 'flags_binding=-some_flag=value',
'west/bozo/test/hello',
fp.name])
# Now check that the right API calls got made.
# Check that create_job was called exactly once, with an AuroraConfig parameter.
self.assert_create_job_called(api)
self.assert_scheduler_called(api, mock_query, 2)
def test_failed_create_job_with_incomplete_bindings(self):
"""Run a test of the "create" command against a mocked-out API:
Verifies that the creation command sends the right API RPCs, and performs the correct
tests on the result."""
mock_context = FakeAuroraCommandContext()
with contextlib.nested(
patch('threading._Event.wait'),
patch('apache.aurora.client.cli.jobs.Job.create_context', return_value=mock_context)):
# This is the real test: invoke create as if it had been called by the command line.
with temporary_file() as fp:
fp.write(self.get_unbound_test_config())
fp.flush()
cmd = AuroraCommandLine()
result = cmd.execute(['job', 'create', '--wait-until=RUNNING',
'--bind', 'cluster_binding=west',
'west/bozo/test/hello',
fp.name])
assert result == EXIT_INVALID_CONFIGURATION
assert mock_context.get_out() == []
assert "Error loading configuration: " in mock_context.get_err()[0]
def test_create_cron_job_fails(self):
"""Test a cron job is not accepted."""
mock_context = FakeAuroraCommandContext()
api = mock_context.get_api('west')
api.get_tier_configs.return_value = self.get_mock_tier_configurations()
with patch('apache.aurora.client.cli.jobs.Job.create_context', return_value=mock_context):
with temporary_file() as fp:
fp.write(self.get_valid_cron_config())
fp.flush()
cmd = AuroraCommandLine()
result = cmd.execute(['job', 'create', '--wait-until=RUNNING',
'west/bozo/test/hello', fp.name])
assert result == EXIT_COMMAND_FAILURE