Example #1
0
def test_generic_stage_input_validation(project_repo_location: Path):
    stage_name = 'stage_14_bad_executable_script'
    path_to_stage_dir = project_repo_location / stage_name
    path_to_config = path_to_stage_dir / STAGE_CONFIG_FILENAME
    config = BodyworkConfig(path_to_config)
    with raises(BodyworkStageConfigError, match='EXECUTABLE_SCRIPT'):
        Stage(stage_name, config, path_to_stage_dir)

    stage_name = 'stage_2_bad_config'
    path_to_stage_dir = project_repo_location / stage_name
    path_to_config = path_to_stage_dir / STAGE_CONFIG_FILENAME
    config = BodyworkConfig(path_to_config)
    with raises(FileExistsError, match='Cannot find'):
        Stage(stage_name, config, path_to_stage_dir)

    stage_name = 'stage_15_bad_requirements'
    path_to_stage_dir = project_repo_location / stage_name
    path_to_config = path_to_stage_dir / STAGE_CONFIG_FILENAME
    config = BodyworkConfig(path_to_config)
    with raises(FileExistsError, match='Cannot find'):
        Stage(stage_name, config, path_to_stage_dir)

    stage_name = 'stage_16_bad_memory_request'
    path_to_stage_dir = project_repo_location / stage_name
    path_to_config = path_to_stage_dir / STAGE_CONFIG_FILENAME
    config = BodyworkConfig(path_to_config)
    with raises(BodyworkStageConfigError, match='MEMORY_REQUEST_MB'):
        Stage(stage_name, config, path_to_stage_dir)

    stage_name = 'stage_17_bad_cpu_request'
    path_to_stage_dir = project_repo_location / stage_name
    path_to_config = path_to_stage_dir / STAGE_CONFIG_FILENAME
    config = BodyworkConfig(path_to_config)
    with raises(BodyworkStageConfigError, match='CPU_REQUEST'):
        Stage(stage_name, config, path_to_stage_dir)
Example #2
0
def test_get_workflow_stages_return_valid_stage_info(
    project_repo_location: Path, ):
    dag = [['stage_1_good'], ['stage_4_good', 'stage_5_good']]

    path_to_stage_1_dir = project_repo_location / 'stage_1_good'
    stage_1_info = BatchStage(
        'stage_1_good',
        BodyworkConfig(path_to_stage_1_dir / STAGE_CONFIG_FILENAME),
        path_to_stage_1_dir)

    path_to_stage_4_dir = project_repo_location / 'stage_4_good'
    stage_4_info = BatchStage(
        'stage_4_good',
        BodyworkConfig(path_to_stage_4_dir / STAGE_CONFIG_FILENAME),
        path_to_stage_4_dir)

    path_to_stage_5_dir = project_repo_location / 'stage_5_good'
    stage_5_info = ServiceStage(
        'stage_5_good',
        BodyworkConfig(path_to_stage_5_dir / STAGE_CONFIG_FILENAME),
        path_to_stage_5_dir)

    all_stage_info = _get_workflow_stages(dag, project_repo_location)
    assert len(all_stage_info) == 3
    assert all_stage_info['stage_1_good'] == stage_1_info
    assert all_stage_info['stage_4_good'] == stage_4_info
    assert all_stage_info['stage_5_good'] == stage_5_info
Example #3
0
def test_service_stage_input_validation(project_repo_location: Path):
    stage_name = 'stage_10_bad_service_data'
    path_to_stage_dir = project_repo_location / stage_name
    path_to_config = path_to_stage_dir / STAGE_CONFIG_FILENAME
    config = BodyworkConfig(path_to_config)
    with raises(BodyworkStageConfigError, match='REPLICAS'):
        ServiceStage(stage_name, config, path_to_stage_dir)

    stage_name = 'stage_11_bad_service_data'
    path_to_stage_dir = project_repo_location / stage_name
    path_to_config = path_to_stage_dir / STAGE_CONFIG_FILENAME
    config = BodyworkConfig(path_to_config)
    with raises(BodyworkStageConfigError, match='PORT'):
        ServiceStage(stage_name, config, path_to_stage_dir)

    stage_name = 'stage_12_bad_service_data'
    path_to_stage_dir = project_repo_location / stage_name
    path_to_config = path_to_stage_dir / STAGE_CONFIG_FILENAME
    config = BodyworkConfig(path_to_config)
    with raises(BodyworkStageConfigError, match='MAX_STARTUP_TIME_SECONDS'):
        ServiceStage(stage_name, config, path_to_stage_dir)

    stage_name = 'stage_13_bad_service_data'
    path_to_stage_dir = project_repo_location / stage_name
    path_to_config = path_to_stage_dir / STAGE_CONFIG_FILENAME
    config = BodyworkConfig(path_to_config)
    with raises(BodyworkStageConfigError, match='MAX_STARTUP_TIME_SECONDS'):
        ServiceStage(stage_name, config, path_to_stage_dir)
Example #4
0
def test_that_config_file_with_missing_sections_raises_error(
    bodywork_config: BodyworkConfig, ):
    del bodywork_config._config["version"]
    del bodywork_config._config["project"]
    del bodywork_config._config["stages"]
    del bodywork_config._config["logging"]
    expected_exception_msg = "missing sections: version, project, stages, logging"
    with raises(BodyworkConfigMissingSectionError,
                match=expected_exception_msg):
        bodywork_config._validate_parsed_config()
Example #5
0
def test_that_config_file_with_mismatched_schema_version_raises_error(
    bodywork_config: BodyworkConfig, ):
    bodywork_config._config["version"] = "0.1"
    expected_exception_msg = (
        f"config file has schema version 0.1, when Bodywork "
        f"version {BODYWORK_VERSION} requires schema version "
        f"{BODYWORK_CONFIG_VERSION}")
    with raises(BodyworkConfigVersionMismatchError,
                match=expected_exception_msg):
        bodywork_config._validate_parsed_config()
Example #6
0
def test_that_config_file_with_non_list_stages_raises_error(
    bodywork_config: BodyworkConfig, ):
    bodywork_config._config["stages"] = "bad"
    expected_exception_msg = (
        "missing or invalid parameters: "
        "project.workflow -> cannot find valid stage @ stages.stage_1, "
        "project.workflow -> cannot find valid stage @ stages.stage_2, "
        "project.workflow -> cannot find valid stage @ stages.stage_3, "
        "project.workflow -> cannot find valid stage @ stages.stage_4, "
        "stages._ - no stage configs provided")
    with raises(BodyworkConfigValidationError, match=expected_exception_msg):
        bodywork_config._validate_parsed_config()
Example #7
0
def test_that_invalid_config_requests_raise_error(project_repo_location: Path):
    config_file_path = project_repo_location / PROJECT_CONFIG_FILENAME
    config = BodyworkConfig(config_file_path)
    with raises(KeyError, match='not_a_real_config_section'):
        config['not_a_real_config_section']
    with raises(KeyError, match='not_a_real_parameter'):
        config['default']['not_a_real_parameter']
Example #8
0
def test_that_subsection_validation_feeds_through_to_validation_report(
    bodywork_config: BodyworkConfig, ):
    del bodywork_config._config["project"]["docker_image"]
    del bodywork_config._config["logging"]["log_level"]
    del bodywork_config._config["stages"]["stage_1"]["batch"]
    bodywork_config._config["stages"]["stage_2"]["service"] = {"foo": "bar"}
    expected_exception_msg = (
        "missing or invalid parameters: "
        "logging.log_level -> required field, "
        "project.docker_image -> required field, "
        "project.workflow -> cannot find valid stage @ stages.stage_1, "
        "project.workflow -> cannot find valid stage @ stages.stage_2, "
        "stages.stage_1.batch/service, "
        "stages.stage_2.batch/service")
    with raises(BodyworkConfigValidationError, match=expected_exception_msg):
        bodywork_config._validate_parsed_config()
Example #9
0
def test_batch_stage_input_validation(project_repo_location: Path):
    stage_name = 'stage_7_bad_batch_data'
    path_to_stage_dir = project_repo_location / stage_name
    path_to_config = path_to_stage_dir / STAGE_CONFIG_FILENAME
    config = BodyworkConfig(path_to_config)
    with raises(BodyworkStageConfigError, match='RETRIES'):
        BatchStage(stage_name, config, path_to_stage_dir)

    stage_name = 'stage_8_bad_batch_data'
    path_to_stage_dir = project_repo_location / stage_name
    path_to_config = path_to_stage_dir / STAGE_CONFIG_FILENAME
    config = BodyworkConfig(path_to_config)
    with raises(BodyworkStageConfigError, match='MAX_COMPLETION_TIME_SECONDS'):
        BatchStage(stage_name, config, path_to_stage_dir)

    stage_name = 'stage_9_bad_batch_data'
    path_to_stage_dir = project_repo_location / stage_name
    path_to_config = path_to_stage_dir / STAGE_CONFIG_FILENAME
    config = BodyworkConfig(path_to_config)
    with raises(BodyworkStageConfigError, match='MAX_COMPLETION_TIME_SECONDS'):
        BatchStage(stage_name, config, path_to_stage_dir)
Example #10
0
def test_failure_stage_does_not_run_for_namespace_exception(
    mock_k8s: MagicMock, project_repo_location: Path
):
    config_path = Path(f"{project_repo_location}/bodywork.yaml")
    config = BodyworkConfig(config_path)
    mock_k8s.namespace_exists.return_value = False
    try:
        run_workflow(config, "foo_bar_foo_993", project_repo_location)
    except BodyworkWorkflowExecutionError:
        pass

    mock_k8s.configure_batch_stage_job.assert_not_called()
Example #11
0
def test_failure_stage_does_not_run_for_docker_image_exception(
    mock_k8s: MagicMock, mock_session: MagicMock, project_repo_location: Path
):
    config_path = Path(f"{project_repo_location}/bodywork.yaml")
    config = BodyworkConfig(config_path)
    mock_session().get.return_value = requests.Response().status_code = 401

    try:
        run_workflow(config, "foo_bar_foo_993", project_repo_location)
    except BodyworkWorkflowExecutionError:
        pass

    mock_k8s.configure_batch_stage_job.assert_not_called()
Example #12
0
def test_usage_stats_opt_out_does_not_ping_usage_stats_server(
    mock_k8s: MagicMock,
    mock_git_hash: MagicMock,
    mock_git_download: MagicMock,
    mock_session: MagicMock,
    mock_rmtree: MagicMock,
    project_repo_location: Path,
):
    config_path = Path(f"{project_repo_location}/bodywork.yaml")
    config = BodyworkConfig(config_path)

    run_workflow(config, "foo_bar_foo_993", project_repo_location)

    mock_session().get.assert_called_once()
Example #13
0
def test_run_workflow_pings_usage_stats_server(
    mock_k8s: MagicMock,
    mock_git_hash: MagicMock,
    mock_git_download: MagicMock,
    mock_session: MagicMock,
    mock_rmtree: MagicMock,
    project_repo_location: Path,
):
    config_path = Path(f"{project_repo_location}/bodywork.yaml")
    config = BodyworkConfig(config_path)
    config.project.usage_stats = True

    run_workflow(config, "foo_bar_foo_993", project_repo_location)

    mock_session().get.assert_called_with(USAGE_STATS_SERVER_URL, params={"type": "workflow"})
Example #14
0
def test_that_config_file_with_invalid_schema_version_raises_error(
    bodywork_config: BodyworkConfig, ):
    bodywork_config._config["version"] = "not the version"
    expected_exception_msg = "missing or invalid parameters: version"
    with raises(BodyworkConfigValidationError, match=expected_exception_msg):
        bodywork_config._validate_parsed_config()

    bodywork_config._config["version"] = "1.0.0"
    with raises(BodyworkConfigValidationError, match=expected_exception_msg):
        bodywork_config._validate_parsed_config()
Example #15
0
def test_run_workflow_adds_git_commit_to_batch_and_service_env_vars(
    mock_k8s: MagicMock,
    mock_git_hash: MagicMock,
    mock_git_download: MagicMock,
    mock_requests: MagicMock,
    mock_rmtree: MagicMock,
    project_repo_location: Path,
):
    commit_hash = "MY GIT COMMIT HASH"
    mock_git_hash.return_value = commit_hash
    expected_result = [
        k8sclient.V1EnvVar(name=GIT_COMMIT_HASH_K8S_ENV_VAR, value=commit_hash)
    ]
    mock_k8s.create_k8s_environment_variables.return_value = expected_result
    mock_k8s.configure_env_vars_from_secrets.return_value = []
    config_path = Path(f"{project_repo_location}/bodywork.yaml")
    config = BodyworkConfig(config_path)

    run_workflow(config, "foo_bar_foo_993", project_repo_location)

    mock_k8s.configure_service_stage_deployment.assert_called_once_with(
        ANY,
        ANY,
        ANY,
        ANY,
        ANY,
        replicas=ANY,
        port=ANY,
        container_env_vars=expected_result,
        image=ANY,
        cpu_request=ANY,
        memory_request=ANY,
        seconds_to_be_ready_before_completing=ANY,
    )
    mock_k8s.configure_batch_stage_job.assert_called_with(
        ANY,
        ANY,
        ANY,
        ANY,
        ANY,
        retries=ANY,
        container_env_vars=expected_result,
        image=ANY,
        cpu_request=ANY,
        memory_request=ANY,
    )
Example #16
0
def test_run_workflow_runs_failure_stage_on_failure(
    mock_k8s: MagicMock,
    mock_git_hash: MagicMock,
    mock_git_download: MagicMock,
    mock_requests: MagicMock,
    mock_rmtree: MagicMock,
    project_repo_location: Path,
):
    config_path = Path(f"{project_repo_location}/bodywork.yaml")
    config = BodyworkConfig(config_path)
    config.project.run_on_failure = "on_fail_stage"

    error_message = "Test Error"
    mock_job = MagicMock(k8sclient.V1Job)
    mock_k8s.configure_batch_stage_job.side_effect = [
        k8sclient.ApiException(error_message),
        mock_job,
    ]
    expected_result = [
        k8sclient.V1EnvVar(name=FAILURE_EXCEPTION_K8S_ENV_VAR, value=error_message)
    ]
    mock_k8s.create_k8s_environment_variables.return_value = expected_result
    mock_k8s.configure_env_vars_from_secrets.return_value = []

    try:
        run_workflow(config, "foo_bar_foo_993", project_repo_location)
    except BodyworkWorkflowExecutionError:
        pass

    mock_k8s.configure_batch_stage_job.assert_called_with(
        ANY,
        "on_fail_stage",
        ANY,
        ANY,
        ANY,
        retries=ANY,
        container_env_vars=expected_result,
        image=ANY,
        cpu_request=ANY,
        memory_request=ANY,
    )
Example #17
0
def test_failure_of_failure_stage_is_recorded_in_exception(
    mock_k8s: MagicMock,
    mock_git_hash: MagicMock,
    mock_git_download: MagicMock,
    mock_requests: MagicMock,
    mock_rmtree: MagicMock,
    project_repo_location: Path,
):
    config_path = Path(f"{project_repo_location}/bodywork.yaml")
    config = BodyworkConfig(config_path)
    config.project.run_on_failure = "on_fail_stage"

    error_message = "The run-on-failure stage experienced an error"
    mock_k8s.configure_batch_stage_job.side_effect = [
        k8sclient.ApiException("Original Error"),
        k8sclient.ApiException(reason=error_message),
    ]
    mock_k8s.configure_env_vars_from_secrets.return_value = []

    with raises(BodyworkWorkflowExecutionError, match=f"{error_message}"):
        run_workflow(config, "foo_bar_foo_993", project_repo_location)
Example #18
0
def test_py_modules_that_cannot_be_located_raise_error(
        bodywork_config: BodyworkConfig):
    bodywork_config.check_py_modules_exist = True
    try:
        bodywork_config._validate_parsed_config()
        assert True
    except Exception:
        assert False

    stage_1 = bodywork_config._config["stages"]["stage_1"]
    bodywork_config._config["stages"]["stage_one"] = stage_1
    del bodywork_config._config["stages"]["stage_1"]
    stage_2 = bodywork_config._config["stages"]["stage_2"]
    bodywork_config._config["stages"]["stage_two"] = stage_2
    del bodywork_config._config["stages"]["stage_2"]
    bodywork_config._config["stages"]["stage_3"][
        "executable_module_path"] = "not_dir/main.py"  # noqa
    expected_exception_msg = (
        "missing or invalid parameters: "
        "project.workflow -> cannot find valid stage @ stages.stage_1, "
        "project.workflow -> cannot find valid stage @ stages.stage_2, "
        "stages.stage_3.executable_module_path -> does not exist")
    with raises(BodyworkConfigValidationError, match=expected_exception_msg):
        bodywork_config._validate_parsed_config()
Example #19
0
def bodywork_config(project_repo_location: Path) -> BodyworkConfig:
    config_file = project_repo_location / "bodywork.yaml"
    return BodyworkConfig(config_file)
Example #20
0
def test_that_config_values_can_be_retreived(project_repo_location: Path):
    config_file_path = project_repo_location / PROJECT_CONFIG_FILENAME
    config = BodyworkConfig(config_file_path)
    assert config['default']['PROJECT_NAME'] == 'bodywork-test-project'
    assert config['logging']['LOG_LEVEL'] == 'INFO'
Example #21
0
def test_that_invalid_config_file_path_raises_error(
        project_repo_location: Path):
    bad_config_file = project_repo_location / "bodywerk.yaml"
    with raises(FileExistsError, match="no config file found"):
        BodyworkConfig(bad_config_file)
Example #22
0
def test_that_empty_config_file_raises_error(project_repo_location: Path):
    config_file = project_repo_location / "bodywork_empty.yaml"
    expected_exception_msg = f"cannot parse YAML from {config_file}"
    with raises(BodyworkConfigParsingError, match=expected_exception_msg):
        BodyworkConfig(config_file)
Example #23
0
def test_check_failure_stage_is_configured(bodywork_config: BodyworkConfig, ):
    bodywork_config._config["project"]["run_on_failure"] = "x"
    expected_exception_msg = f"project.run_on_failure -> cannot find valid stage: x to run on workflow failure."

    with raises(BodyworkConfigValidationError, match=expected_exception_msg):
        bodywork_config._validate_parsed_config()
Example #24
0
def test_that_invalid_config_file_path_raises_error():
    bad_config_file_path = Path('./tests/not_a_real_directory/bodywerk.ini')
    with raises(FileExistsError, match='no config file found'):
        BodyworkConfig(bad_config_file_path)