From 943c22be9eccbd24f235243d25b188e20b8d9c74 Mon Sep 17 00:00:00 2001 From: Jason Robbins Date: Sat, 23 Mar 2024 02:04:44 +0000 Subject: [PATCH] Added unit tests and logging --- api/feature_latency_api_test.py | 7 +- api/review_latency_api.py | 15 +++- api/review_latency_api_test.py | 126 ++++++++++++++++++++++++++++++++ 3 files changed, 141 insertions(+), 7 deletions(-) create mode 100644 api/review_latency_api_test.py diff --git a/api/feature_latency_api_test.py b/api/feature_latency_api_test.py index 30e75758..e76aad1f 100644 --- a/api/feature_latency_api_test.py +++ b/api/feature_latency_api_test.py @@ -19,6 +19,7 @@ from datetime import datetime import flask from unittest import mock import werkzeug.exceptions # Flask HTTP stuff. +from google.cloud import ndb # type: ignore from api import feature_latency_api from internals.core_enums import * @@ -45,11 +46,11 @@ class FeatureLatencyAPITest(testing_config.CustomTestCase): def setUp(self): self.handler = feature_latency_api.FeatureLatencyAPI() - self.request_path = '/api/v0/feature_latency' + self.request_path = '/api/v0/feature-latency' - self.fe_1, self.fe_1_id = make_feature( + self.fe_1a, self.fe_1a_id = make_feature( 'has no milestone', (2023, 2, 18), ENABLED_BY_DEFAULT, None) - self.fe_1, self.fe_1_id = make_feature( + self.fe_1b, self.fe_1b_id = make_feature( 'not a launch status', (2023, 2, 18), NO_ACTIVE_DEV, 119) self.fe_2, self.fe_2_id = make_feature( 'launched before start', (2022, 8, 19), ENABLED_BY_DEFAULT, 108) diff --git a/api/review_latency_api.py b/api/review_latency_api.py index 6c51d142..516ee479 100644 --- a/api/review_latency_api.py +++ b/api/review_latency_api.py @@ -45,9 +45,13 @@ class ReviewLatencyAPI(basehandlers.APIHandler): Returns: A list of data on all public origin trials. """ - gates = self.get_recently_reviewed_gates(DEFAULT_RECENT_DAYS) - logging.info('gates %r', gates) + today = kwargs.get('today') + gates = self.get_recently_reviewed_gates(DEFAULT_RECENT_DAYS, today=today) gates_by_fid = self.organize_gates_by_feature_id(gates) + for fid, feature_gates in gates_by_fid.items(): + logging.info('feautre %r:', fid) + for fg in feature_gates: + logging.info(' %r', fg) features = self.get_features_by_id(gates_by_fid.keys()) features = self.sort_features_by_request(features, gates_by_fid) latencies_by_fid = { @@ -57,9 +61,12 @@ class ReviewLatencyAPI(basehandlers.APIHandler): latencies_by_fid, features) return result - def get_recently_reviewed_gates(self, days) -> list[Gate]: + def get_recently_reviewed_gates( + self, days:int, today:datetime|None = None + ) -> list[Gate]: """Retrieve a list of Gates responded to in recent days.""" - start_date = datetime.today() - timedelta(days=90) + today = today or datetime.today() + start_date = today - timedelta(days=90) gates_in_range = Gate.query( Gate.requested_on >= start_date).fetch() feature_ids = {g.feature_id for g in gates_in_range} diff --git a/api/review_latency_api_test.py b/api/review_latency_api_test.py new file mode 100644 index 00000000..7811a4be --- /dev/null +++ b/api/review_latency_api_test.py @@ -0,0 +1,126 @@ +# Copyright 2024 Google LLC +# +# Licensed 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 +# +# https://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 testing_config # isort: split + +from datetime import datetime + +from unittest import mock +from google.cloud import ndb # type: ignore + +from api import review_latency_api +from internals.core_enums import * +from internals.core_models import FeatureEntry +from internals.review_models import Gate + + +def make_feature_and_gates(name): + fe = FeatureEntry(name=name, summary='sum', category=1) + fe.put() + fe_id = fe.key.integer_id() + g_1 = Gate(feature_id=fe_id, gate_type=1, stage_id=1, state=Gate.PREPARING) + g_1.put() + g_2 = Gate(feature_id=fe_id, gate_type=2, stage_id=1, state=Gate.PREPARING) + g_2.put() + g_3 = Gate(feature_id=fe_id, gate_type=3, stage_id=1, state=Gate.PREPARING) + g_3.put() + return fe, fe_id, g_1, g_2, g_3 + + +class ReviewLatencyAPITest(testing_config.CustomTestCase): + + def setUp(self): + self.handler = review_latency_api.ReviewLatencyAPI() + self.request_path = '/api/v0/review-latency' + + self.fe_1, self.fe_1_id, self.g_1_1, self.g_1_2, self.g_1_3 = ( + make_feature_and_gates('Feature one')) + self.fe_2, self.fe_2_id, self.g_2_1, self.g_2_2, self.g_2_3 = ( + make_feature_and_gates('Feature two')) + self.today = datetime(2024, 3, 22) + self.yesterday = datetime(2024, 3, 21) + self.last_week = datetime(2024, 3, 15) + + def tearDown(self): + kinds: list[ndb.Model] = [FeatureEntry, Gate] + for kind in kinds: + for entity in kind.query(): + entity.key.delete() + + def test_do_get__nothing_requested(self): + """When no reviews have been started, the result is empty.""" + actual = self.handler.do_get() + self.assertEqual([], actual) + + def test_do_get__normal(self): + """It produces a report of review latency.""" + self.g_1_1.requested_on = self.last_week + self.g_1_1.responded_on = self.today + self.g_1_1.put() + self.g_1_2.requested_on = self.last_week + self.g_1_2.put() + + actual = self.handler.do_get() + + expected = [ + { 'feature': {'name': 'Feature one', 'id': self.fe_1_id}, + 'gate_reviews': [ + {'gate_type': 1, + 'latency_days': 5}, + {'gate_type': 2, + 'latency_days': review_latency_api.PENDING_LATENCY}, + {'gate_type': 3, + 'latency_days': review_latency_api.NOT_STARTED_LATENCY}, + ], + }] + self.assertEqual(expected, actual) + + def test_do_get__sorting(self): + """Multiple results are sorted by earlest review request.""" + self.g_1_1.requested_on = self.yesterday + self.g_1_1.responded_on = self.today + self.g_1_1.put() + self.g_1_2.requested_on = self.today + self.g_1_2.responded_on = self.today + self.g_1_2.put() + + self.g_2_1.requested_on = self.last_week + self.g_2_1.responded_on = self.yesterday + self.g_2_1.put() + + actual = self.handler.do_get() + + expected = [ + { 'feature': {'name': 'Feature two', 'id': self.fe_2_id}, + 'gate_reviews': [ + {'gate_type': 1, + 'latency_days': 4}, + {'gate_type': 2, + 'latency_days': review_latency_api.NOT_STARTED_LATENCY}, + {'gate_type': 3, + 'latency_days': review_latency_api.NOT_STARTED_LATENCY}, + ], + }, + { 'feature': {'name': 'Feature one', 'id': self.fe_1_id}, + 'gate_reviews': [ + {'gate_type': 1, + 'latency_days': 1}, + {'gate_type': 2, + 'latency_days': 0}, + {'gate_type': 3, + 'latency_days': review_latency_api.NOT_STARTED_LATENCY}, + ], + }, + ] + self.assertEqual(expected, actual)