This is an automated email from the ASF dual-hosted git repository. ephraimanierobi pushed a commit to branch v2-3-test in repository https://gitbox.apache.org/repos/asf/airflow.git
commit d54af711c42b62d23b84ce7377da64e90bbb7c4e Author: Ash Berlin-Taylor <a...@apache.org> AuthorDate: Fri Jun 10 15:35:38 2022 +0100 Add tests for the grid_data endpoint (#24375) The one fix/change here was to include the JSON content response here so that `resp.json` works in the test. (cherry picked from commit 2b2d97068fa45881672dab6f2134becae246f3f3) --- airflow/www/views.py | 5 +- tests/www/views/test_views_grid.py | 238 +++++++++++++++++++++++++++++++++++++ 2 files changed, 242 insertions(+), 1 deletion(-) diff --git a/airflow/www/views.py b/airflow/www/views.py index fe4217c452..7e1a80da1b 100644 --- a/airflow/www/views.py +++ b/airflow/www/views.py @@ -3544,7 +3544,10 @@ class Airflow(AirflowBaseView): 'dag_runs': encoded_runs, } # avoid spaces to reduce payload size - return htmlsafe_json_dumps(data, separators=(',', ':')) + return ( + htmlsafe_json_dumps(data, separators=(',', ':')), + {'Content-Type': 'application/json; charset=utf-8'}, + ) @expose('/robots.txt') @action_logging diff --git a/tests/www/views/test_views_grid.py b/tests/www/views/test_views_grid.py new file mode 100644 index 0000000000..e5d29be8a2 --- /dev/null +++ b/tests/www/views/test_views_grid.py @@ -0,0 +1,238 @@ +# +# 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 freezegun +import pendulum +import pytest + +from airflow.models import DagBag +from airflow.operators.empty import EmptyOperator +from airflow.utils.state import DagRunState, TaskInstanceState +from airflow.utils.task_group import TaskGroup +from airflow.utils.types import DagRunType +from tests.test_utils.mock_operators import MockOperator + +DAG_ID = 'test' +CURRENT_TIME = pendulum.DateTime(2021, 9, 7) + + +@pytest.fixture(autouse=True, scope="module") +def examples_dag_bag(): + # Speed up: We don't want example dags for this module + return DagBag(include_examples=False, read_dags_from_db=True) + + +@pytest.fixture +def dag_without_runs(dag_maker, session, app, monkeypatch): + with monkeypatch.context() as m: + # Remove global operator links for this test + m.setattr('airflow.plugins_manager.global_operator_extra_links', []) + m.setattr('airflow.plugins_manager.operator_extra_links', []) + m.setattr('airflow.plugins_manager.registered_operator_link_classes', {}) + + with dag_maker(dag_id=DAG_ID, serialized=True, session=session): + EmptyOperator(task_id="task1") + with TaskGroup(group_id='group'): + MockOperator.partial(task_id='mapped').expand(arg1=['a', 'b', 'c']) + + m.setattr(app, 'dag_bag', dag_maker.dagbag) + yield dag_maker + + +@pytest.fixture +def dag_with_runs(dag_without_runs): + with freezegun.freeze_time(CURRENT_TIME): + date = dag_without_runs.dag.start_date + run_1 = dag_without_runs.create_dagrun( + run_id='run_1', state=DagRunState.SUCCESS, run_type=DagRunType.SCHEDULED, execution_date=date + ) + run_2 = dag_without_runs.create_dagrun( + run_id='run_2', + run_type=DagRunType.SCHEDULED, + execution_date=dag_without_runs.dag.next_dagrun_info(date).logical_date, + ) + + yield run_1, run_2 + + +def test_no_runs(admin_client, dag_without_runs): + resp = admin_client.get(f'/object/grid_data?dag_id={DAG_ID}', follow_redirects=True) + assert resp.status_code == 200, resp.json + assert resp.json == { + 'dag_runs': [], + 'groups': { + 'children': [ + { + 'extra_links': [], + 'id': 'task1', + 'instances': [], + 'is_mapped': False, + 'label': 'task1', + }, + { + 'children': [ + { + 'extra_links': [], + 'id': 'group.mapped', + 'instances': [], + 'is_mapped': True, + 'label': 'mapped', + } + ], + 'id': 'group', + 'instances': [], + 'label': 'group', + 'tooltip': '', + }, + ], + 'id': None, + 'instances': [], + 'label': None, + 'tooltip': '', + }, + } + + +def test_one_run(admin_client, dag_with_runs, session): + run1, run2 = dag_with_runs + + for ti in run1.task_instances: + ti.state = TaskInstanceState.SUCCESS + + session.flush() + + resp = admin_client.get(f'/object/grid_data?dag_id={DAG_ID}', follow_redirects=True) + assert resp.status_code == 200, resp.json + assert resp.json == { + 'dag_runs': [ + { + 'data_interval_end': '2016-01-02T00:00:00+00:00', + 'data_interval_start': '2016-01-01T00:00:00+00:00', + 'end_date': '2021-09-07T00:00:00+00:00', + 'execution_date': '2016-01-01T00:00:00+00:00', + 'last_scheduling_decision': None, + 'run_id': 'run_1', + 'run_type': 'scheduled', + 'start_date': '2016-01-01T00:00:00+00:00', + 'state': 'success', + }, + { + 'data_interval_end': '2016-01-03T00:00:00+00:00', + 'data_interval_start': '2016-01-02T00:00:00+00:00', + 'end_date': None, + 'execution_date': '2016-01-02T00:00:00+00:00', + 'last_scheduling_decision': None, + 'run_id': 'run_2', + 'run_type': 'scheduled', + 'start_date': '2016-01-01T00:00:00+00:00', + 'state': 'running', + }, + ], + 'groups': { + 'children': [ + { + 'extra_links': [], + 'id': 'task1', + 'instances': [ + { + 'end_date': None, + 'map_index': -1, + 'run_id': 'run_1', + 'start_date': None, + 'state': 'success', + 'task_id': 'task1', + 'try_number': 1, + }, + { + 'end_date': None, + 'map_index': -1, + 'run_id': 'run_2', + 'start_date': None, + 'state': None, + 'task_id': 'task1', + 'try_number': 1, + }, + ], + 'is_mapped': False, + 'label': 'task1', + }, + { + 'children': [ + { + 'extra_links': [], + 'id': 'group.mapped', + 'instances': [ + { + 'end_date': None, + 'mapped_states': ['success', 'success', 'success'], + 'run_id': 'run_1', + 'start_date': None, + 'state': 'success', + 'task_id': 'group.mapped', + 'try_number': 1, + }, + { + 'end_date': None, + 'mapped_states': [None, None, None], + 'run_id': 'run_2', + 'start_date': None, + 'state': None, + 'task_id': 'group.mapped', + 'try_number': 1, + }, + ], + 'is_mapped': True, + 'label': 'mapped', + }, + ], + 'id': 'group', + 'instances': [ + { + 'end_date': None, + 'run_id': 'run_1', + 'start_date': None, + 'state': 'success', + 'task_id': 'group', + }, + { + 'end_date': None, + 'run_id': 'run_2', + 'start_date': None, + 'state': None, + 'task_id': 'group', + }, + ], + 'label': 'group', + 'tooltip': '', + }, + ], + 'id': None, + 'instances': [ + { + 'end_date': None, + 'run_id': 'run_1', + 'start_date': None, + 'state': 'success', + 'task_id': None, + }, + {'end_date': None, 'run_id': 'run_2', 'start_date': None, 'state': None, 'task_id': None}, + ], + 'label': None, + 'tooltip': '', + }, + }