def test_update(tmp_work_dir): job = Job(id="foo123", action="foo") insert(job) job.action = "bar" update(job, update_fields=["action"]) jobs = find_where(Job, id="foo123") assert jobs[0].action == "bar"
def test_select_values(tmp_work_dir): insert(Job(id="foo123", state=State.PENDING)) insert(Job(id="foo124", state=State.RUNNING)) insert(Job(id="foo125", state=State.FAILED)) values = select_values(Job, "id", state__in=[State.PENDING, State.FAILED]) assert sorted(values) == ["foo123", "foo125"] values = select_values(Job, "state", id="foo124") assert values == [State.RUNNING]
def test_update_excluding_a_field(tmp_work_dir): job = Job(id="foo123", action="foo", commit="commit-of-glory") insert(job) job.action = "bar" job.commit = "commit-of-doom" update(job, exclude_fields=["commit"]) j = find_one(Job, id="foo123") assert j.action == "bar" assert j.commit == "commit-of-glory"
def test_model_overrides(complete_continuous): # case #1: assure global overrides applied data = deepcopy(complete_continuous) data["bmr"] = {"type": "Std. Dev.", "value": 0.123} session = Job.build_session(data, data["datasets"][0]) for model in session.models: assert model.overrides["bmr"] == 0.123 # case #2: assure model overrides are applied after global overrides data = deepcopy(complete_continuous) data["bmr"] = {"type": "Std. Dev.", "value": 0.123} data["models"] = [{"name": "Linear", "settings": {"bmr": 0.456}}, {"name": "Linear"}] session = Job.build_session(data, data["datasets"][0]) assert session.models[0].overrides["bmr"] == 0.456 assert session.models[1].overrides["bmr"] == 0.123
def create_failed_job(job_request, exception): """ Sometimes we want to say to the job-server (and the user): your JobRequest was broken so we weren't able to create any jobs for it. But the only way for the job-runner to communicate back to the job-server is by creating a job. So this function creates a single job with the special action name "__error__", which starts in the FAILED state and whose status_message contains the error we wish to communicate. This is a bit of a hack, but it keeps the sync protocol simple. """ # Special case for the NothingToDoError which we treat as a success if isinstance(exception, NothingToDoError): state = State.SUCCEEDED status_message = "All actions have already run" action = job_request.requested_actions[0] else: state = State.FAILED status_message = f"{type(exception).__name__}: {exception}" action = "__error__" now = int(time.time()) job = Job( job_request_id=job_request.id, state=state, repo_url=job_request.repo_url, commit=job_request.commit, workspace=job_request.workspace, action=action, status_message=status_message, created_at=now, started_at=now, updated_at=now, completed_at=now, ) insert_into_database(job_request, [job])
def test_formatting_filter(): record = logging.makeLogRecord({}) assert log_utils.formatting_filter(record) assert record.action == "" record = logging.makeLogRecord({"job": test_job}) assert log_utils.formatting_filter(record) assert record.action == "action: " assert record.tags == "project=project action=action id=id" record = logging.makeLogRecord({"job": test_job, "status_code": "code"}) assert log_utils.formatting_filter(record) assert record.tags == "status=code project=project action=action id=id" test_job2 = Job(id="id", action="action", repo_url=repo_url, status_code="code") record = logging.makeLogRecord({"job": test_job2}) assert log_utils.formatting_filter(record) assert record.tags == "status=code project=project action=action id=id" record = logging.makeLogRecord({"job": test_job, "job_request": test_request}) assert log_utils.formatting_filter(record) assert record.tags == "project=project action=action id=id req=request" record = logging.makeLogRecord({"status_code": ""}) assert log_utils.formatting_filter(record) assert record.tags == ""
def test_job_resource_weights(tmp_path): config = textwrap.dedent(""" [my-workspace] some_action = 2.5 pattern[\d]+ = 4 """) config_file = tmp_path / "config.ini" config_file.write_text(config) weights = parse_job_resource_weights(config_file) job = Job(workspace="foo", action="bar") assert get_job_resource_weight(job, weights=weights) == 1 job = Job(workspace="my-workspace", action="some_action") assert get_job_resource_weight(job, weights=weights) == 2.5 job = Job(workspace="my-workspace", action="pattern315") assert get_job_resource_weight(job, weights=weights) == 4 job = Job(workspace="my-workspace", action="pattern000no_match") assert get_job_resource_weight(job, weights=weights) == 1
def job_factory(job_request=None, **kwargs): if job_request is None: job_request = job_request_factory() values = deepcopy(JOB_DEFAULTS) values.update(kwargs) values["job_request_id"] = job_request.id job = Job(**values) insert(job) return job
def test_basic_roundtrip(tmp_work_dir): job = Job( id="foo123", job_request_id="bar123", state=State.RUNNING, output_spec={"hello": [1, 2, 3]}, ) insert(job) j = find_one(Job, job_request_id__in=["bar123", "baz123"]) assert job.id == j.id assert job.output_spec == j.output_spec
def test_model_overrides(complete_continuous): # case #1: assure global overrides applied data = deepcopy(complete_continuous) data['bmr'] = {'type': 'Std. Dev.', 'value': 0.123} session = Job.build_session(data, data['datasets'][0]) for model in session.models: assert model.overrides['bmr'] == 0.123 # case #2: assure model overrides are applied after global overrides data = deepcopy(complete_continuous) data['bmr'] = {'type': 'Std. Dev.', 'value': 0.123} data['models'] = [{ 'name': 'Linear', 'settings': { 'bmr': 0.456 } }, { 'name': 'Linear' }] session = Job.build_session(data, data['datasets'][0]) assert session.models[0].overrides['bmr'] == 0.456 assert session.models[1].overrides['bmr'] == 0.123
def recursively_build_jobs(jobs_by_action, job_request, pipeline_config, action): """ Recursively populate the `jobs_by_action` dict with jobs Args: jobs_by_action: A dict mapping action ID strings to Job instances job_request: An instance of JobRequest representing the job request. pipeline_config: A Pipeline instance representing the pipeline configuration. action: The string ID of the action to be added as a job. """ existing_job = jobs_by_action.get(action) if existing_job and not job_should_be_rerun(job_request, existing_job): return action_spec = get_action_specification( pipeline_config, action, using_dummy_data_backend=config.USING_DUMMY_DATA_BACKEND, ) # Walk over the dependencies of this action, creating any necessary jobs, # and ensure that this job waits for its dependencies to finish before it # starts wait_for_job_ids = [] for required_action in action_spec.needs: recursively_build_jobs( jobs_by_action, job_request, pipeline_config, required_action ) required_job = jobs_by_action[required_action] if required_job.state in [State.PENDING, State.RUNNING]: wait_for_job_ids.append(required_job.id) job = Job( job_request_id=job_request.id, state=State.PENDING, repo_url=job_request.repo_url, commit=job_request.commit, workspace=job_request.workspace, database_name=job_request.database_name, action=action, wait_for_job_ids=wait_for_job_ids, requires_outputs_from=action_spec.needs, run_command=action_spec.run, output_spec=action_spec.outputs, created_at=int(time.time()), updated_at=int(time.time()), ) # Add it to the dictionary of scheduled jobs jobs_by_action[action] = job
def job(job_id, action, state): spec = get_action_specification( project, action, using_dummy_data_backend=config.USING_DUMMY_DATA_BACKEND, ) return Job( id=job_id, job_request_id="previous-request", state=state, status_message="", repo_url=str(project_dir), workspace=project_dir.name, database_name="a-database", action=action, wait_for_job_ids=[], requires_outputs_from=spec.needs, run_command=spec.run, output_spec=spec.outputs, created_at=int(time.time()), updated_at=int(time.time()), outputs={}, )
from datetime import datetime import logging import time from jobrunner.models import Job, JobRequest from jobrunner import log_utils, local_run FROZEN_TIMESTAMP = 1608568119.1467905 FROZEN_TIMESTRING = datetime.utcfromtimestamp(FROZEN_TIMESTAMP).isoformat() repo_url = "https://github.com/opensafely/project" test_job = Job(id="id", action="action", repo_url=repo_url) test_request = JobRequest( id="request", repo_url=repo_url, workspace="workspace", commit="commit", requested_actions=["action"], cancelled_actions=[], database_name="dummy", ) def test_formatting_filter(): record = logging.makeLogRecord({}) assert log_utils.formatting_filter(record) assert record.action == "" record = logging.makeLogRecord({"job": test_job}) assert log_utils.formatting_filter(record)
def test_find_one_fails_if_there_is_more_than_one_result(tmp_work_dir): insert(Job(id="foo123", workspace="the-workspace")) insert(Job(id="foo456", workspace="the-workspace")) with pytest.raises(ValueError): find_one(Job, workspace="the-workspace")
def test_find_one_returns_a_single_value(tmp_work_dir): insert(Job(id="foo123", workspace="the-workspace")) job = find_one(Job, id="foo123") assert job.workspace == "the-workspace"
def test_update(tmp_work_dir): job = Job(id="foo123", action="foo") insert(job) job.action = "bar" update(job) assert find_one(Job, id="foo123").action == "bar"