def test_build_with_failure(storage): config = JobControlConfig.from_string(""" jobs: - id: foo function: jobcontrol.utils.testing:testing_job kwargs: retval: "Foo Retval" fail: True """) jc = JobControl(storage=storage, config=config) job = jc.get_job('foo') build = job.create_build() build.run() assert build['started'] assert build['finished'] assert not build['success'] assert not build['skipped'] assert job.has_builds() assert not job.has_successful_builds() assert not job.has_running_builds() assert list(job.iter_builds()) == [build] assert list(job.iter_builds(success=True)) == [] assert list(job.iter_builds(success=False)) == [build] assert isinstance(build['exception'], RuntimeError) assert isinstance(build['exception_tb'], TracebackInfo)
def test_build_with_skip(storage): config = JobControlConfig.from_string(""" jobs: - id: job-to-skip function: jobcontrol.utils.testing:testing_job kwargs: retval: "Foo Retval" skip: True """) jc = JobControl(storage=storage, config=config) job = jc.get_job('job-to-skip') build = job.create_build() build.run() assert build['started'] assert build['finished'] assert build['skipped'] assert job.has_builds() assert not job.has_successful_builds() # Skipped builds are ignored assert not job.has_running_builds() assert list(job.iter_builds()) == [build] assert list(job.iter_builds(skipped=False)) == [] assert build['exception'] is None assert build['exception_tb'] is None
def test_logging_with_context(storage): import logging from jobcontrol.core import JobExecutionContext, JobControl from jobcontrol.config import JobControlConfig logger = logging.getLogger('foo_logger') build_id = storage.create_build('foo') jc = JobControl(storage=storage, config=JobControlConfig()) jc._install_log_handler() logger.debug('This will be ignored') logger.info('This will be ignored') logger.error('This will be ignored') with JobExecutionContext(app=jc, job_id='foo', build_id=build_id): logger.debug('This is a log message [D]') logger.info('This is a log message [I]') logger.warning('This is a log message [W]') logger.error('This is a log message [E]') try: raise ValueError('foobar') except: logger.exception('Shit happens') logger.info('This will get ignored as well') # ------------------------------------------------------------ assert len(list(storage.iter_log_messages(build_id))) == 5
def test_build_deletion(storage): func = 'jobcontrol.utils.testing:testing_job' config = { 'jobs': [ {'id': 'job-1', 'function': func}, ] } jc = JobControl(storage=storage, config=config) job = jc.get_job('job-1') build_1 = job.create_build() build_1.run() assert len(list(job.iter_builds())) == 1 build_2 = job.create_build() build_2.run() assert len(list(job.iter_builds())) == 2 build_1.delete() assert len(list(job.iter_builds())) == 1
def test_build_configuration_pinning(storage): config = dedent("""\ jobs: - id: job-1 function: jobcontrol.utils.testing:testing_job kwargs: retval: "original-retval" """) config = JobControlConfig.from_string(config) jc = JobControl(storage=storage, config=config) # ------------------------------------------------------------ # Create a build with old configuration # ------------------------------------------------------------ job = jc.get_job('job-1') build = job.create_build() build.run() build.refresh() assert build['finished'] and build['success'] assert build['retval'] == 'original-retval' build = job.create_build() build_id = build.id # Then stop using this object # ------------------------------------------------------------ # Update the configuration # ------------------------------------------------------------ config = dedent("""\ jobs: - id: job-1 function: jobcontrol.utils.testing:testing_job kwargs: retval: "new-retval" """) config = JobControlConfig.from_string(config) jc = JobControl(storage=storage, config=config) # ------------------------------------------------------------ # Running that build will return the original return value build = jc.get_build(build_id) build.run() build.refresh() assert build['finished'] and build['success'] assert build['retval'] == 'original-retval' # ------------------------------------------------------------ # A freshly created build will return the new return value job = jc.get_job('job-1') build = job.create_build() build.run() build.refresh() assert build['finished'] and build['success'] assert build['retval'] == 'new-retval' build = job.create_build() build_id = build.id # Then stop using this object
def test_build_failure_nonserializable_exception(storage): """ It only gets worse when we cannot even serialize the exception.. But still, we can wrap it in a serialization error exception and be fine with it. Hopefully, we can keep the original traceback.. """ config = JobControlConfig.from_string(""" jobs: - id: job-nse function: jobcontrol.utils.testing:job_raising_nonserializable """) jc = JobControl(storage=storage, config=config) # Run build for RAISE nonserializable # It should just fail with an exception in the post-run serialization # todo: We might even check the traceback for that.. job = jc.get_job('job-nse') build = job.create_build() build.run() assert build['started'] assert build['finished'] assert not build['success'] # WARNING! How to tell whether this job failed due to # the raised exception being serialized properly, or due # to the exception serialization failed? assert not isinstance(build['exception'], NonSerializableException) assert isinstance(build['exception'], ExceptionPlaceholder)
def test_build_logging(storage): config = { 'jobs': [ {'id': 'job-with-logging', 'function': 'jobcontrol.utils.testing:job_with_logging'}, ] } jc = JobControl(storage=storage, config=config) job = jc.get_job('job-with-logging') build = job.create_build() build.run() build.refresh() assert build['finished'] and build['success'] log_messages = build.iter_log_messages() messages_from_job = [ msg for msg in log_messages if msg.name == 'jobcontrol.utils.testing.job_with_logging'] assert len(messages_from_job) == 6 assert messages_from_job[0].levelno == logging.DEBUG assert messages_from_job[0].message == 'This is a debug message' assert messages_from_job[0].args == () assert isinstance(messages_from_job[0].created, datetime) assert messages_from_job[0].filename == 'testing.py' assert messages_from_job[0].function == 'job_with_logging' assert messages_from_job[0].level_name == 'DEBUG' assert messages_from_job[0].level == logging.DEBUG assert isinstance(messages_from_job[0].lineno, int) assert messages_from_job[0].module == 'testing' assert messages_from_job[0].message == 'This is a debug message' assert messages_from_job[0].msg == 'This is a debug message' assert messages_from_job[0].name == 'jobcontrol.utils.testing.job_with_logging' # noqa assert isinstance(messages_from_job[0].pathname, basestring) assert messages_from_job[0].pathname.endswith('jobcontrol/utils/testing.py') # noqa assert messages_from_job[0].exception is None assert messages_from_job[0].exception_tb is None assert messages_from_job[1].levelno == logging.INFO assert messages_from_job[1].message == 'This is an info message' assert messages_from_job[2].levelno == logging.WARNING assert messages_from_job[2].message == 'This is a warning message' assert messages_from_job[3].levelno == logging.ERROR assert messages_from_job[3].message == 'This is an error message' assert messages_from_job[4].levelno == logging.CRITICAL assert messages_from_job[4].message == 'This is a critical message' assert messages_from_job[5].levelno == logging.ERROR assert messages_from_job[5].message == 'This is an exception message' assert isinstance(messages_from_job[5].exception, ValueError) assert isinstance(messages_from_job[5].exception_tb, TracebackInfo)
def test_build_deletion(storage): func = 'jobcontrol.utils.testing:testing_job' config = { 'jobs': [ { 'id': 'job-1', 'function': func }, ] } jc = JobControl(storage=storage, config=config) job = jc.get_job('job-1') build_1 = job.create_build() build_1.run() assert len(list(job.iter_builds())) == 1 build_2 = job.create_build() build_2.run() assert len(list(job.iter_builds())) == 2 build_1.delete() assert len(list(job.iter_builds())) == 1
def test_build_progress_reporting(storage): jc = JobControl(storage=storage, config=JobControlConfig()) jc = JobControl(storage=storage, config={ 'jobs': [ {'id': 'foo_job', 'function': 'jobcontrol.utils.testing:job_with_progress', 'kwargs': {'config': [ (None, 5), (('foo', 'spam'), 2), (('foo', 'eggs'), 4), (('bar', 'bacon', 'X'), 8), (('bar', 'bacon', 'Y'), 16)]}} ] }) build = jc.create_build(job_id='foo_job')
def test_simple_build_run(storage): config = JobControlConfig.from_string(""" jobs: - id: foo function: jobcontrol.utils.testing:testing_job kwargs: retval: "Foo Retval" """) jc = JobControl(storage=storage, config=config) job = jc.get_job('foo') assert job.has_builds() is False assert job.has_successful_builds() is False assert job.has_running_builds() is False assert job.is_outdated() is None assert job.can_be_built() is True # Create and run a build # ------------------------------------------------------------ build = job.create_build() assert job.has_builds() is False # "finished" builds only assert job.has_successful_builds() is False assert job.has_running_builds() is False assert job.is_outdated() is None assert job.can_be_built() is True assert list(job.iter_builds()) == [build] build.run() assert build['started'] is True assert build['finished'] is True assert build['success'] is True assert build['skipped'] is False assert build['retval'] == 'Foo Retval' assert job.has_builds() is True assert job.has_successful_builds() is True assert job.has_running_builds() is False assert job.is_outdated() is False assert job.can_be_built() is True assert list(job.iter_builds()) == [build]
def test_build_progress_reporting(storage): jc = JobControl(storage=storage, config=JobControlConfig()) jc = JobControl(storage=storage, config={ 'jobs': [{ 'id': 'foo_job', 'function': 'jobcontrol.utils.testing:job_with_progress', 'kwargs': { 'config': [(None, 5), (('foo', 'spam'), 2), (('foo', 'eggs'), 4), (('bar', 'bacon', 'X'), 8), (('bar', 'bacon', 'Y'), 16)] } }] }) build = jc.create_build(job_id='foo_job')
def test_build_failure_due_to_nonserializable_object(storage): config = JobControlConfig.from_string(""" jobs: - id: job-nso function: jobcontrol.utils.testing:job_returning_nonserializable """) jc = JobControl(storage=storage, config=config) job = jc.get_job('job-nso') build = job.create_build() build.run() assert build['started'] assert build['finished'] assert not build['success'] assert isinstance(build['exception'], SerializationError) assert ( # The original exception message is kept.. "TypeError('a class that defines __slots__ without defining " "__getstate__ cannot be pickled',)") in build['exception'].message
def cli_main_grp(config_file, outfmt): # todo: use pass_context for passing context instead of global objects? global jc, output_fmt output_fmt = outfmt if config_file is None: raise ValueError('Configuration file missing') jc = JobControl.from_config_file(config_file)
def test_simple_build_deletion(storage): config = JobControlConfig.from_string(""" jobs: - id: job-to-delete function: jobcontrol.utils.testing:testing_job """) jc = JobControl(storage=storage, config=config) job = jc.get_job('job-to-delete') build_1 = job.create_build() build_1.run() assert len(list(job.iter_builds())) == 1 build_2 = job.create_build() build_2.run() assert len(list(job.iter_builds())) == 2 build_1.delete() assert len(list(job.iter_builds())) == 1
def test_build_deletion_with_cleanup(storage): config = JobControlConfig.from_string(""" jobs: - id: job-to-delete function: jobcontrol.utils.testing:job_creating_temp_file cleanup_function: jobcontrol.utils.testing:cleanup_temp_file """) jc = JobControl(storage=storage, config=config) job = jc.get_job('job-to-delete') build = job.create_build() build.run() assert build['finished'] and build['success'] assert isinstance(build.retval, str) assert os.path.isfile(build.retval) assert len(list(job.iter_builds())) == 1 build.delete() assert len(list(job.iter_builds())) == 0 assert not os.path.isfile(build.retval)
def test_core_config_jobs(storage): config = JobControlConfig.from_string(""" jobs: - id: foo function: mymodule.foo dependencies: [] - id: bar function: mymodule.bar dependencies: ['foo'] - id: baz function: mymodule.baz dependencies: ['foo', 'bar'] """) jc = JobControl(storage=storage, config=config) job_foo = jc.get_job('foo') job_bar = jc.get_job('bar') job_baz = jc.get_job('baz') # Check jobs # ------------------------------------------------------------ assert isinstance(job_foo, JobInfo) assert job_foo.id == 'foo' assert job_foo.config['id'] == 'foo' assert job_foo.config['function'] == 'mymodule.foo' assert job_foo.config['args'] == () assert job_foo.config['kwargs'] == {} assert job_foo.config['dependencies'] == [] assert list(job_foo.get_deps()) == [] assert list(job_foo.get_revdeps()) == [job_bar, job_baz] assert job_foo.get_status() == 'not_built' assert list(job_foo.iter_builds()) == [] assert job_foo.get_latest_successful_build() is None assert job_foo.has_builds() is False assert job_foo.has_successful_builds() is False assert job_foo.has_running_builds() is False assert job_foo.is_outdated() is None # no builds.. assert job_foo.can_be_built() is True assert isinstance(job_bar, JobInfo) assert job_bar.id == 'bar' assert job_bar.config['id'] == 'bar' assert job_bar.config['function'] == 'mymodule.bar' assert job_bar.config['args'] == () assert job_bar.config['kwargs'] == {} assert job_bar.config['dependencies'] == ['foo'] assert list(job_bar.get_deps()) == [job_foo] assert list(job_bar.get_revdeps()) == [job_baz] assert job_bar.get_status() == 'not_built' assert list(job_bar.iter_builds()) == [] assert job_bar.get_latest_successful_build() is None assert job_bar.has_builds() is False assert job_bar.has_successful_builds() is False assert job_bar.has_running_builds() is False assert job_bar.is_outdated() is None # no builds.. assert job_bar.can_be_built() is False # "foo" has no builds assert isinstance(job_baz, JobInfo) assert job_baz.id == 'baz' assert job_baz.config['id'] == 'baz' assert job_baz.config['function'] == 'mymodule.baz' assert job_baz.config['args'] == () assert job_baz.config['kwargs'] == {} assert job_baz.config['dependencies'] == ['foo', 'bar'] assert list(job_baz.get_deps()) == [job_foo, job_bar] assert list(job_baz.get_revdeps()) == [] assert job_baz.get_status() == 'not_built' assert list(job_baz.iter_builds()) == [] assert job_baz.get_latest_successful_build() is None assert job_baz.has_builds() is False assert job_baz.has_successful_builds() is False assert job_baz.has_running_builds() is False assert job_baz.is_outdated() is None # no builds.. assert job_baz.can_be_built() is False # "foo" and "bar" have no builds # Exception on non-existing job with pytest.raises(NotFound): jc.get_job('does-not-exist') # Iterate jobs assert list(jc.iter_jobs()) == [job_foo, job_bar, job_baz]
def test_build_logging(storage): config = { 'jobs': [ { 'id': 'job-with-logging', 'function': 'jobcontrol.utils.testing:job_with_logging' }, ] } jc = JobControl(storage=storage, config=config) job = jc.get_job('job-with-logging') build = job.create_build() build.run() build.refresh() assert build['finished'] and build['success'] log_messages = build.iter_log_messages() messages_from_job = [ msg for msg in log_messages if msg.name == 'jobcontrol.utils.testing.job_with_logging' ] assert len(messages_from_job) == 6 assert messages_from_job[0].levelno == logging.DEBUG assert messages_from_job[0].message == 'This is a debug message' assert messages_from_job[0].args == () assert isinstance(messages_from_job[0].created, datetime) assert messages_from_job[0].filename == 'testing.py' assert messages_from_job[0].function == 'job_with_logging' assert messages_from_job[0].level_name == 'DEBUG' assert messages_from_job[0].level == logging.DEBUG assert isinstance(messages_from_job[0].lineno, int) assert messages_from_job[0].module == 'testing' assert messages_from_job[0].message == 'This is a debug message' assert messages_from_job[0].msg == 'This is a debug message' assert messages_from_job[ 0].name == 'jobcontrol.utils.testing.job_with_logging' # noqa assert isinstance(messages_from_job[0].pathname, basestring) assert messages_from_job[0].pathname.endswith( 'jobcontrol/utils/testing.py') # noqa assert messages_from_job[0].exception is None assert messages_from_job[0].exception_tb is None assert messages_from_job[1].levelno == logging.INFO assert messages_from_job[1].message == 'This is an info message' assert messages_from_job[2].levelno == logging.WARNING assert messages_from_job[2].message == 'This is a warning message' assert messages_from_job[3].levelno == logging.ERROR assert messages_from_job[3].message == 'This is an error message' assert messages_from_job[4].levelno == logging.CRITICAL assert messages_from_job[4].message == 'This is a critical message' assert messages_from_job[5].levelno == logging.ERROR assert messages_from_job[5].message == 'This is an exception message' assert isinstance(messages_from_job[5].exception, ValueError) assert isinstance(messages_from_job[5].exception_tb, TracebackInfo)
def test_dependency_pinning(storage): # Test for dependency pinning # --------------------------- # # We want to make sure that a build uses the latest build for # a dependency at the time it was created; so if we run a build # for job-1, then create a build for job-2, then run another build # for job-1, the return values used when running a build for job-2 # will be the one from the *first* build. # To ensure this, we are going to change the return value # in the configuration. config = dedent("""\ jobs: - id: job-1 function: jobcontrol.utils.testing:testing_job kwargs: retval: "original-retval" - id: job-2 function: jobcontrol.utils.testing:testing_job kwargs: retval: !retval 'job-1' dependencies: ['job-1'] """) config = JobControlConfig.from_string(config) jc = JobControl(storage=storage, config=config) build_1_1 = jc.create_build('job-1') build_1_1.run() build_1_1.refresh() assert build_1_1['finished'] and build_1_1['success'] assert build_1_1['retval'] == 'original-retval' # This should have pinned dependency on build_1_1 build_2_1 = jc.create_build('job-2') # Update configuration # -------------------- config = dedent("""\ jobs: - id: job-1 function: jobcontrol.utils.testing:testing_job kwargs: retval: "new-retval" - id: job-2 function: jobcontrol.utils.testing:testing_job kwargs: retval: !retval 'job-1' dependencies: ['job-1'] """) config = JobControlConfig.from_string(config) jc = JobControl(storage=storage, config=config) build_1_2 = jc.create_build('job-1') build_1_2.run() build_1_2.refresh() assert build_1_2['finished'] and build_1_2['success'] assert build_1_2['retval'] == 'new-retval' build_2_1 = jc.get_build(build_2_1.id) # Get from *new* JC build_2_1.run() build_2_1.refresh() assert build_2_1['finished'] and build_2_1['success'] assert build_2_1['retval'] == 'original-retval' build_2_2 = jc.create_build('job-2') build_2_2.run() build_2_2.refresh() assert build_2_2['finished'] and build_2_2['success'] assert build_2_2['retval'] == 'new-retval'
def test_job_status_reporting(storage): config = JobControlConfig.from_string(""" jobs: - id: job-1 function: jobcontrol.utils.testing:testing_job - id: job-2 function: jobcontrol.utils.testing:testing_job - id: job-3 function: jobcontrol.utils.testing:testing_job dependencies: ['job-1', 'job-2'] - id: job-4 function: jobcontrol.utils.testing:testing_job dependencies: ['job-3'] """) jc = JobControl(storage=storage, config=config) # Check status of unbuilt jobs job_1 = jc.get_job('job-1') job_2 = jc.get_job('job-2') job_3 = jc.get_job('job-3') job_4 = jc.get_job('job-4') assert job_1.get_status() == 'not_built' assert job_2.get_status() == 'not_built' assert job_3.get_status() == 'not_built' assert job_4.get_status() == 'not_built' assert list(job_1.iter_builds()) == [] assert job_1.get_latest_successful_build() is None assert job_1.has_builds() is False assert job_1.has_successful_builds() is False assert job_1.has_running_builds() is False assert job_1.is_outdated() is None # IDK assert job_1.can_be_built() is True assert job_2.can_be_built() is True assert job_3.can_be_built() is False # deps not met assert job_4.can_be_built() is False # deps not met # ------------------------------------------------------------ # Manually start a build for job 1, as we want to # check it is running, etc.. # ------------------------------------------------------------ build_1_1 = job_1.create_build() assert build_1_1['started'] is False assert build_1_1['finished'] is False assert job_1.has_builds() is False assert job_1.has_running_builds() is False assert job_1.get_status() == 'not_built' jc.storage.start_build(build_1_1.id) build_1_1.refresh() assert build_1_1['started'] is True assert build_1_1['finished'] is False assert job_1.has_builds() is False # **Completed** builds.. assert job_1.has_running_builds() is True # Note: "running" is not anymore reported as a state assert job_1.get_status() == 'not_built' jc.storage.finish_build(build_1_1.id, success=False) build_1_1.refresh() assert build_1_1['started'] is True assert build_1_1['finished'] is True assert build_1_1['success'] is False assert job_1.has_builds() is True assert job_1.has_successful_builds() is False assert job_1.has_running_builds() is False assert job_1.get_status() == 'failed' # ------------------------------------------------------------ # Do it again, with a new build, which should succeed this time # ------------------------------------------------------------ build_1_2 = job_1.create_build() build_1_2.run() build_1_2.refresh() assert len(list(job_1.iter_builds())) == 2 assert build_1_2['started'] is True assert build_1_2['finished'] is True assert build_1_2['success'] is True assert job_1.has_builds() is True assert job_1.has_successful_builds() is True assert job_1.has_running_builds() is False assert job_1.get_status() == 'success' # ------------------------------------------------------------ # Now build job 2 and make sure 3 becomes buildable # ------------------------------------------------------------ assert job_3.can_be_built() is False build_2_1 = job_2.create_build() build_2_1.run() assert job_3.can_be_built() is True # Job 4 is still missing a build from 3 assert job_4.can_be_built() is False job_3.create_build().run() assert job_4.can_be_built() is True assert job_2.get_status() == 'success' assert job_3.get_status() == 'success' assert job_4.get_status() == 'not_built' # ------------------------------------------------------------ # Rebuild #1 to get #3 to be "outdated" # ------------------------------------------------------------ assert job_3.is_outdated() is False job_1.create_build().run() assert job_3.is_outdated() is True assert job_3.get_status() == 'outdated'
def jc(storage): from jobcontrol.core import JobControl return JobControl(storage)