This is an automated email from the ASF dual-hosted git repository.
kamilbregula pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/master by this push:
new 3cc5756 Add unit tests for mlengine_operator_utils (#9702)
3cc5756 is described below
commit 3cc5756d04c7d3fd14b71e9ec6f5f906d5a24212
Author: chipmyersjr <[email protected]>
AuthorDate: Fri Jul 10 15:26:00 2020 -0700
Add unit tests for mlengine_operator_utils (#9702)
---
.../cloud/utils/test_mlengine_operator_utils.py | 277 +++++++++++++++++++++
tests/test_project_structure.py | 1 -
2 files changed, 277 insertions(+), 1 deletion(-)
diff --git a/tests/providers/google/cloud/utils/test_mlengine_operator_utils.py
b/tests/providers/google/cloud/utils/test_mlengine_operator_utils.py
new file mode 100644
index 0000000..326c14e
--- /dev/null
+++ b/tests/providers/google/cloud/utils/test_mlengine_operator_utils.py
@@ -0,0 +1,277 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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 base64
+import json
+import unittest
+from datetime import datetime
+
+import dill
+import mock
+
+from airflow.exceptions import AirflowException
+from airflow.models import DAG
+from airflow.operators.python import PythonOperator
+from airflow.providers.google.cloud.hooks.gcs import GCSHook
+from airflow.providers.google.cloud.operators.dataflow import
DataflowCreatePythonJobOperator
+from airflow.providers.google.cloud.utils.mlengine_operator_utils import
create_evaluate_ops
+
+TASK_PREFIX = "test-task-prefix"
+TASK_PREFIX_PREDICTION = TASK_PREFIX + "-prediction"
+TASK_PREFIX_SUMMARY = TASK_PREFIX + "-summary"
+TASK_PREFIX_VALIDATION = TASK_PREFIX + "-validation"
+DATA_FORMAT = "TEXT"
+INPUT_PATHS = ["gs://path/to/input/file.json",
+ "gs://path/to/input/file2.json",
+ "gs://path/to/input/file3.json"]
+PREDICTION_PATH = "gs://path/to/output/predictions.json"
+BATCH_PREDICTION_JOB_ID = "test-batch-prediction-job-id"
+PROJECT_ID = "test-project-id"
+REGION = "test-region"
+DATAFLOW_OPTIONS = {
+ "project": "my-gcp-project",
+ "zone": "us-central1-f",
+ "stagingLocation": "gs://bucket/tmp/dataflow/staging/"
+}
+MODEL_URI = "gs://path/to/model/model"
+MODEL_NAME = "test-model-name"
+VERSION_NAME = "test-version-name"
+DAG_DEFAULT_ARGS = {
+ "project_id": PROJECT_ID,
+ "region": REGION,
+ "model_name": MODEL_NAME,
+ "version_name": VERSION_NAME,
+ "dataflow_default_options": DATAFLOW_OPTIONS
+}
+TEST_DAG = DAG(dag_id="test-dag-id", start_date=datetime(2000, 1, 1),
default_args=DAG_DEFAULT_ARGS)
+
+
+def get_metric_fn_and_keys():
+ import math
+
+ def error_and_squared_error(inst):
+ label = float(inst['input_label'])
+ classes = float(inst['classes'])
+ err = abs(classes - label)
+ squared_err = math.pow(classes - label, 2)
+ return err, squared_err
+
+ return error_and_squared_error, ['err', 'mse']
+
+
+METRIC_FN, METRIC_KEYS = get_metric_fn_and_keys()
+METRIC_FN_ENCODED = base64.b64encode(dill.dumps(METRIC_FN,
recurse=True)).decode()
+METRIC_KEYS_EXPECTED = ','.join(METRIC_KEYS)
+
+
+def validate_err_and_count(summary):
+ if summary['err'] > 0.2:
+ raise ValueError('Too high err>0.2; summary=%s' % summary)
+ if summary['mse'] > 0.05:
+ raise ValueError('Too high mse>0.05; summary=%s' % summary)
+ if summary['count'] < 1000:
+ raise ValueError('Too few instances<1000; summary=%s' % summary)
+ return summary
+
+
+class TestMlengineOperatorUtils(unittest.TestCase):
+ @mock.patch.object(PythonOperator, "set_upstream")
+ @mock.patch.object(DataflowCreatePythonJobOperator, "set_upstream")
+ def test_create_evaluate_ops(self, mock_dataflow, mock_python):
+ result = create_evaluate_ops(task_prefix=TASK_PREFIX,
+ data_format=DATA_FORMAT,
+ input_paths=INPUT_PATHS,
+ prediction_path=PREDICTION_PATH,
+
metric_fn_and_keys=get_metric_fn_and_keys(),
+ validate_fn=validate_err_and_count,
+
batch_prediction_job_id=BATCH_PREDICTION_JOB_ID,
+ project_id=PROJECT_ID,
+ region=REGION,
+ dataflow_options=DATAFLOW_OPTIONS,
+ model_uri=MODEL_URI
+ )
+
+ evaluate_prediction, evaluate_summary, evaluate_validation = result
+
+ mock_dataflow.assert_called_once_with(evaluate_prediction)
+ mock_python.assert_called_once_with(evaluate_summary)
+
+ self.assertEqual(TASK_PREFIX_PREDICTION, evaluate_prediction.task_id)
+ self.assertEqual(PROJECT_ID, evaluate_prediction._project_id)
+ self.assertEqual(BATCH_PREDICTION_JOB_ID, evaluate_prediction._job_id)
+ self.assertEqual(REGION, evaluate_prediction._region)
+ self.assertEqual(DATA_FORMAT, evaluate_prediction._data_format)
+ self.assertEqual(INPUT_PATHS, evaluate_prediction._input_paths)
+ self.assertEqual(PREDICTION_PATH, evaluate_prediction._output_path)
+ self.assertEqual(MODEL_URI, evaluate_prediction._uri)
+
+ self.assertEqual(TASK_PREFIX_SUMMARY, evaluate_summary.task_id)
+ self.assertEqual(DATAFLOW_OPTIONS,
evaluate_summary.dataflow_default_options)
+ self.assertEqual(PREDICTION_PATH,
evaluate_summary.options["prediction_path"])
+ self.assertEqual(METRIC_FN_ENCODED,
evaluate_summary.options["metric_fn_encoded"])
+ self.assertEqual(METRIC_KEYS_EXPECTED,
evaluate_summary.options["metric_keys"])
+
+ self.assertEqual(TASK_PREFIX_VALIDATION, evaluate_validation.task_id)
+ self.assertEqual(PREDICTION_PATH,
evaluate_validation.templates_dict["prediction_path"])
+
+ @mock.patch.object(PythonOperator, "set_upstream")
+ @mock.patch.object(DataflowCreatePythonJobOperator, "set_upstream")
+ def test_create_evaluate_ops_model_and_version_name(self, mock_dataflow,
mock_python):
+ result = create_evaluate_ops(task_prefix=TASK_PREFIX,
+ data_format=DATA_FORMAT,
+ input_paths=INPUT_PATHS,
+ prediction_path=PREDICTION_PATH,
+
metric_fn_and_keys=get_metric_fn_and_keys(),
+ validate_fn=validate_err_and_count,
+
batch_prediction_job_id=BATCH_PREDICTION_JOB_ID,
+ project_id=PROJECT_ID,
+ region=REGION,
+ dataflow_options=DATAFLOW_OPTIONS,
+ model_name=MODEL_NAME,
+ version_name=VERSION_NAME
+ )
+
+ evaluate_prediction, evaluate_summary, evaluate_validation = result
+
+ mock_dataflow.assert_called_once_with(evaluate_prediction)
+ mock_python.assert_called_once_with(evaluate_summary)
+
+ self.assertEqual(TASK_PREFIX_PREDICTION, evaluate_prediction.task_id)
+ self.assertEqual(PROJECT_ID, evaluate_prediction._project_id)
+ self.assertEqual(BATCH_PREDICTION_JOB_ID, evaluate_prediction._job_id)
+ self.assertEqual(REGION, evaluate_prediction._region)
+ self.assertEqual(DATA_FORMAT, evaluate_prediction._data_format)
+ self.assertEqual(INPUT_PATHS, evaluate_prediction._input_paths)
+ self.assertEqual(PREDICTION_PATH, evaluate_prediction._output_path)
+ self.assertEqual(MODEL_NAME, evaluate_prediction._model_name)
+ self.assertEqual(VERSION_NAME, evaluate_prediction._version_name)
+
+ self.assertEqual(TASK_PREFIX_SUMMARY, evaluate_summary.task_id)
+ self.assertEqual(DATAFLOW_OPTIONS,
evaluate_summary.dataflow_default_options)
+ self.assertEqual(PREDICTION_PATH,
evaluate_summary.options["prediction_path"])
+ self.assertEqual(METRIC_FN_ENCODED,
evaluate_summary.options["metric_fn_encoded"])
+ self.assertEqual(METRIC_KEYS_EXPECTED,
evaluate_summary.options["metric_keys"])
+
+ self.assertEqual(TASK_PREFIX_VALIDATION, evaluate_validation.task_id)
+ self.assertEqual(PREDICTION_PATH,
evaluate_validation.templates_dict["prediction_path"])
+
+ @mock.patch.object(PythonOperator, "set_upstream")
+ @mock.patch.object(DataflowCreatePythonJobOperator, "set_upstream")
+ def test_create_evaluate_ops_dag(self, mock_dataflow, mock_python):
+ result = create_evaluate_ops(task_prefix=TASK_PREFIX,
+ data_format=DATA_FORMAT,
+ input_paths=INPUT_PATHS,
+ prediction_path=PREDICTION_PATH,
+
metric_fn_and_keys=get_metric_fn_and_keys(),
+ validate_fn=validate_err_and_count,
+
batch_prediction_job_id=BATCH_PREDICTION_JOB_ID,
+ dag=TEST_DAG
+ )
+
+ evaluate_prediction, evaluate_summary, evaluate_validation = result
+
+ mock_dataflow.assert_called_once_with(evaluate_prediction)
+ mock_python.assert_called_once_with(evaluate_summary)
+
+ self.assertEqual(TASK_PREFIX_PREDICTION, evaluate_prediction.task_id)
+ self.assertEqual(PROJECT_ID, evaluate_prediction._project_id)
+ self.assertEqual(BATCH_PREDICTION_JOB_ID, evaluate_prediction._job_id)
+ self.assertEqual(REGION, evaluate_prediction._region)
+ self.assertEqual(DATA_FORMAT, evaluate_prediction._data_format)
+ self.assertEqual(INPUT_PATHS, evaluate_prediction._input_paths)
+ self.assertEqual(PREDICTION_PATH, evaluate_prediction._output_path)
+ self.assertEqual(MODEL_NAME, evaluate_prediction._model_name)
+ self.assertEqual(VERSION_NAME, evaluate_prediction._version_name)
+
+ self.assertEqual(TASK_PREFIX_SUMMARY, evaluate_summary.task_id)
+ self.assertEqual(DATAFLOW_OPTIONS,
evaluate_summary.dataflow_default_options)
+ self.assertEqual(PREDICTION_PATH,
evaluate_summary.options["prediction_path"])
+ self.assertEqual(METRIC_FN_ENCODED,
evaluate_summary.options["metric_fn_encoded"])
+ self.assertEqual(METRIC_KEYS_EXPECTED,
evaluate_summary.options["metric_keys"])
+
+ self.assertEqual(TASK_PREFIX_VALIDATION, evaluate_validation.task_id)
+ self.assertEqual(PREDICTION_PATH,
evaluate_validation.templates_dict["prediction_path"])
+
+ @mock.patch.object(GCSHook, "download")
+ @mock.patch.object(PythonOperator, "set_upstream")
+ @mock.patch.object(DataflowCreatePythonJobOperator, "set_upstream")
+ def test_apply_validate_fn(self, mock_dataflow, mock_python,
mock_download):
+ result = create_evaluate_ops(task_prefix=TASK_PREFIX,
+ data_format=DATA_FORMAT,
+ input_paths=INPUT_PATHS,
+ prediction_path=PREDICTION_PATH,
+
metric_fn_and_keys=get_metric_fn_and_keys(),
+ validate_fn=validate_err_and_count,
+
batch_prediction_job_id=BATCH_PREDICTION_JOB_ID,
+ project_id=PROJECT_ID,
+ region=REGION,
+ dataflow_options=DATAFLOW_OPTIONS,
+ model_uri=MODEL_URI
+ )
+
+ _, _, evaluate_validation = result
+
+ mock_download.return_value = json.dumps({
+ "err": 0.3,
+ "mse": 0.04,
+ "count": 1100
+ })
+ templates_dict = {"prediction_path": PREDICTION_PATH}
+ with self.assertRaises(ValueError) as context:
+ evaluate_validation.python_callable(templates_dict=templates_dict)
+
+ self.assertEqual("Too high err>0.2; summary={'err': 0.3, 'mse': 0.04,
'count': 1100}",
+ str(context.exception))
+ mock_download.assert_called_once_with("path",
"to/output/predictions.json/prediction.summary.json")
+
+ invalid_prediction_paths = ["://path/to/output/predictions.json",
"gs://", ""]
+
+ for path in invalid_prediction_paths:
+ templates_dict = {"prediction_path": path}
+ with self.assertRaises(ValueError) as context:
+
evaluate_validation.python_callable(templates_dict=templates_dict)
+ self.assertEqual("Wrong format prediction_path:",
str(context.exception)[:29])
+
+ def test_invalid_task_prefix(self):
+ invalid_task_prefix_values = ["test-task-prefix&",
"~test-task-prefix", "test-task(-prefix"]
+
+ for invalid_task_prefix_value in invalid_task_prefix_values:
+ with self.assertRaises(AirflowException):
+ create_evaluate_ops(task_prefix=invalid_task_prefix_value,
+ data_format=DATA_FORMAT,
+ input_paths=INPUT_PATHS,
+ prediction_path=PREDICTION_PATH,
+
metric_fn_and_keys=get_metric_fn_and_keys(),
+ validate_fn=validate_err_and_count)
+
+ def test_non_callable_metric_fn(self):
+ with self.assertRaises(AirflowException):
+ create_evaluate_ops(task_prefix=TASK_PREFIX,
+ data_format=DATA_FORMAT,
+ input_paths=INPUT_PATHS,
+ prediction_path=PREDICTION_PATH,
+ metric_fn_and_keys=("error_and_squared_error",
['err', 'mse']),
+ validate_fn=validate_err_and_count)
+
+ def test_non_callable_validate_fn(self):
+ with self.assertRaises(AirflowException):
+ create_evaluate_ops(task_prefix=TASK_PREFIX,
+ data_format=DATA_FORMAT,
+ input_paths=INPUT_PATHS,
+ prediction_path=PREDICTION_PATH,
+ metric_fn_and_keys=get_metric_fn_and_keys(),
+ validate_fn="validate_err_and_count")
diff --git a/tests/test_project_structure.py b/tests/test_project_structure.py
index 0166868..bd5941e 100644
--- a/tests/test_project_structure.py
+++ b/tests/test_project_structure.py
@@ -36,7 +36,6 @@ MISSING_TEST_FILES = {
'tests/providers/google/cloud/transfers/test_sql_to_gcs.py',
'tests/providers/google/cloud/utils/test_field_sanitizer.py',
'tests/providers/google/cloud/utils/test_field_validator.py',
- 'tests/providers/google/cloud/utils/test_mlengine_operator_utils.py',
'tests/providers/google/cloud/utils/test_mlengine_prediction_summary.py',
'tests/providers/jenkins/hooks/test_jenkins.py',
'tests/providers/microsoft/azure/sensors/test_azure_cosmos.py',