From 891e84d7a3d0edc39344775c3b7d815fb5a21a67 Mon Sep 17 00:00:00 2001 From: Jason Robbins Date: Tue, 23 Mar 2021 13:09:00 -0700 Subject: [PATCH] Refactor internal stuff into new internals/ directory. (#1235) --- api/stars_api.py | 2 +- api/stars_api_test.py | 2 +- app.yaml | 8 +- {tests => internals}/__init__.py | 0 util.py => internals/fetchchannels.py | 12 +- admin.py => internals/fetchmetrics.py | 105 +++----------- internals/fetchmetrics_test.py | 128 ++++++++++++++++++ notifier.py => internals/notifier.py | 0 {tests => internals}/notifier_test.py | 12 +- processes.py => internals/processes.py | 0 {tests => internals}/processes_test.py | 2 +- models.py | 4 +- tests/models_test.py => models_test.py | 0 package.json | 4 +- pages/blink_handler.py | 1 - pages/featuredetail.py | 2 +- pages/guide.py | 2 +- pages/guideforms.py | 2 +- pages/intentpreview.py | 99 ++++++++++++++ .../intentpreview_test.py | 115 +--------------- pages/metrics.py | 7 +- pages/schedule.py | 4 +- pages/schedule_test.py | 2 +- 23 files changed, 284 insertions(+), 229 deletions(-) rename {tests => internals}/__init__.py (100%) rename util.py => internals/fetchchannels.py (91%) rename admin.py => internals/fetchmetrics.py (76%) mode change 100755 => 100644 create mode 100644 internals/fetchmetrics_test.py rename notifier.py => internals/notifier.py (100%) rename {tests => internals}/notifier_test.py (98%) rename processes.py => internals/processes.py (100%) rename {tests => internals}/processes_test.py (99%) rename tests/models_test.py => models_test.py (100%) create mode 100644 pages/intentpreview.py rename tests/admin_test.py => pages/intentpreview_test.py (60%) diff --git a/api/stars_api.py b/api/stars_api.py index 5137bafd..826131b2 100644 --- a/api/stars_api.py +++ b/api/stars_api.py @@ -20,7 +20,7 @@ import logging from framework import basehandlers import models -import notifier +from internals import notifier class StarsAPI(basehandlers.APIHandler): diff --git a/api/stars_api_test.py b/api/stars_api_test.py index 016fbb78..477c8677 100644 --- a/api/stars_api_test.py +++ b/api/stars_api_test.py @@ -25,7 +25,7 @@ import werkzeug.exceptions # Flask HTTP stuff. from api import register from api import stars_api import models -import notifier +from internals import notifier diff --git a/app.yaml b/app.yaml index 8c13f02d..b88b77b1 100644 --- a/app.yaml +++ b/app.yaml @@ -36,15 +36,15 @@ handlers: # Admin ------------------------------------------------------------------------ - url: /cron/.* - script: admin.app + script: internals.fetchmetrics.app # Any cron job must be harmless if it is called too often or with bad args - url: /tasks/.* - script: notifier.app + script: internals.notifier.app # Header checks prevent raw access to this handler. Tasks have headers. - url: /_ah/bounce - script: notifier.app + script: internals.notifier.app login: admin # Prevents raw access to this handler. - url: /admin/blink.* @@ -58,7 +58,7 @@ handlers: secure: always - url: /admin/features/.* - script: admin.app + script: pages.intentpreview.app login: required # non-admin secure: always diff --git a/tests/__init__.py b/internals/__init__.py similarity index 100% rename from tests/__init__.py rename to internals/__init__.py diff --git a/util.py b/internals/fetchchannels.py similarity index 91% rename from util.py rename to internals/fetchchannels.py index d71fdfbb..0ea72fb3 100644 --- a/util.py +++ b/internals/fetchchannels.py @@ -1,8 +1,5 @@ -from __future__ import division -from __future__ import print_function - # -*- coding: utf-8 -*- -# Copyright 2017 Google Inc. +# Copyright 2021 Google Inc. # # Licensed under the Apache License, Version 2.0 (the "License") # you may not use this file except in compliance with the License. @@ -16,12 +13,15 @@ from __future__ import print_function # See the License for the specific language governing permissions and # limitations under the License. -__author__ = 'ericbidelman@chromium.org (Eric Bidelman)' +from __future__ import division +from __future__ import print_function import json +import requests from framework import ramcache -import requests +# Note: this file cannot import models because it would be circular. + def get_omaha_data(): omaha_data = ramcache.get('omaha_data') diff --git a/admin.py b/internals/fetchmetrics.py old mode 100755 new mode 100644 similarity index 76% rename from admin.py rename to internals/fetchmetrics.py index a48d12c7..8dec1deb --- a/admin.py +++ b/internals/fetchmetrics.py @@ -1,8 +1,5 @@ -from __future__ import division -from __future__ import print_function - # -*- coding: utf-8 -*- -# Copyright 2013 Google Inc. +# Copyright 2021 Google Inc. # # Licensed under the Apache License, Version 2.0 (the "License") # you may not use this file except in compliance with the License. @@ -16,28 +13,35 @@ from __future__ import print_function # See the License for the specific language governing permissions and # limitations under the License. -__author__ = 'ericbidelman@chromium.org (Eric Bidelman)' +from __future__ import division +from __future__ import print_function import datetime import json import logging -import os -import re from xml.dom import minidom # Appengine imports. from framework import ramcache import requests -from google.appengine.api import users -from google.appengine.ext import db # File imports. from framework import basehandlers from framework import utils import models -import processes import settings + +def get_omaha_data(): + omaha_data = ramcache.get('omaha_data') + if omaha_data is None: + result = requests.get('https://omahaproxy.appspot.com/all.json') + if result.status_code == 200: + omaha_data = json.loads(result.content) + ramcache.set('omaha_data', omaha_data, time=86400) # cache for 24hrs. + return omaha_data + + UMA_QUERY_SERVER = 'https://uma-export.appspot.com/chromestatus/' HISTOGRAMS_URL = 'https://chromium.googlesource.com/chromium/src/+/master/' \ @@ -257,9 +261,12 @@ class HistogramsHandler(basehandlers.FlaskHandler): enum_tags = dom.getElementsByTagName('enum') - # Save bucket ids for each histogram type, FeatureObserver and MappedCSSProperties. + # Save bucket ids for each histogram type, FeatureObserver and + # MappedCSSProperties. for histogram_id in self.MODEL_CLASS.keys(): - enum = filter(lambda enum: enum.attributes['name'].value == histogram_id, enum_tags)[0] + enum = filter( + lambda enum: enum.attributes['name'].value == histogram_id, + enum_tags)[0] for child in enum.getElementsByTagName('int'): self._SaveData({ 'bucket_id': child.attributes['value'].value, @@ -269,77 +276,6 @@ class HistogramsHandler(basehandlers.FlaskHandler): return 'Success' - -INTENT_PARAM = 'intent' -LAUNCH_PARAM = 'launch' -VIEW_FEATURE_URL = '/feature' - - -class IntentEmailPreviewHandler(basehandlers.FlaskHandler): - """Show a preview of an intent email, as appropriate to the feature stage.""" - - TEMPLATE_PATH = 'admin/features/launch.html' - - def get_template_data(self, feature_id=None, stage_id=None): - user = users.get_current_user() - if user is None: - return self.redirect(users.create_login_url(self.request.path)) - - if not feature_id: - self.abort(404) - f = models.Feature.get_by_id(feature_id) - if f is None: - self.abort(404) - - intent_stage = stage_id if stage_id is not None else f.intent_stage - - if not self.user_can_edit(user): - self.abort(403) - - page_data = self.get_page_data(feature_id, f, intent_stage) - return page_data - - def get_page_data(self, feature_id, f, intent_stage): - """Return a dictionary of data used to render the page.""" - page_data = { - 'subject_prefix': self.compute_subject_prefix(f, intent_stage), - 'feature': f.format_for_template(), - 'sections_to_show': processes.INTENT_EMAIL_SECTIONS.get( - intent_stage, []), - 'intent_stage': intent_stage, - 'default_url': '%s://%s%s/%s' % ( - self.request.scheme, self.request.host, - VIEW_FEATURE_URL, feature_id), - } - - if LAUNCH_PARAM in self.request.args: - page_data[LAUNCH_PARAM] = True - if INTENT_PARAM in self.request.args: - page_data[INTENT_PARAM] = True - - return page_data - - def compute_subject_prefix(self, feature, intent_stage): - """Return part of the subject line for an intent email.""" - - if intent_stage == models.INTENT_INCUBATE: - if feature.feature_type == models.FEATURE_TYPE_DEPRECATION_ID: - return 'Intent to Deprecate and Remove' - elif intent_stage == models.INTENT_IMPLEMENT: - return 'Intent to Prototype' - elif intent_stage == models.INTENT_EXPERIMENT: - return 'Ready for Trial' - elif intent_stage == models.INTENT_EXTEND_TRIAL: - if feature.feature_type == models.FEATURE_TYPE_DEPRECATION_ID: - return 'Request for Deprecation Trial' - else: - return 'Intent to Experiment' - elif intent_stage == models.INTENT_SHIP: - return 'Intent to Ship' - - return 'Intent stage "%s"' % models.INTENT_STAGES[intent_stage] - - class BlinkComponentHandler(basehandlers.FlaskHandler): """Updates the list of Blink components in the db.""" def get_template_data(self): @@ -351,7 +287,4 @@ app = basehandlers.FlaskApplication([ ('/cron/metrics', YesterdayHandler), ('/cron/histograms', HistogramsHandler), ('/cron/update_blink_components', BlinkComponentHandler), - ('/admin/features/launch/', IntentEmailPreviewHandler), - ('/admin/features/launch//', - IntentEmailPreviewHandler), ], debug=settings.DEBUG) diff --git a/internals/fetchmetrics_test.py b/internals/fetchmetrics_test.py new file mode 100644 index 00000000..062a6730 --- /dev/null +++ b/internals/fetchmetrics_test.py @@ -0,0 +1,128 @@ +# Copyright 2020 Google Inc. +# +# 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 +# +# 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. + +from __future__ import division +from __future__ import print_function + +import datetime +import unittest +import testing_config # Must be imported before the module under test. +import urllib + +import mock +import flask +import werkzeug + +from internals import fetchmetrics +import models + + +class FetchMetricsTest(unittest.TestCase): + + @mock.patch('settings.PROD', True) + @mock.patch('requests.request') + def test__prod(self, mock_fetch): + """In prod, we actually request metrics from uma-export.""" + mock_fetch.return_value = 'mock response' + actual = fetchmetrics._FetchMetrics('a url') + + self.assertEqual('mock response', actual) + mock_fetch.assert_called_once_with('GET', + 'a url', timeout=120, allow_redirects=False) + + + @mock.patch('settings.STAGING', True) + @mock.patch('requests.request') + def test__staging(self, mock_fetch): + """In prod, we actually request metrics from uma-export.""" + mock_fetch.return_value = 'mock response' + actual = fetchmetrics._FetchMetrics('a url') + + self.assertEqual('mock response', actual) + mock_fetch.assert_called_once_with('GET', + 'a url', timeout=120, allow_redirects=False) + + @mock.patch('requests.request') + def test__dev(self, mock_fetch): + """In Dev, we cannot access uma-export.""" + actual = fetchmetrics._FetchMetrics('a url') + + self.assertIsNone(actual) + mock_fetch.assert_not_called() + + +class UmaQueryTest(unittest.TestCase): + + def setUp(self): + self.uma_query = fetchmetrics.UmaQuery( + query_name='usecounter.features', + model_class=models.FeatureObserver, + property_map_class=models.FeatureObserverHistogram) + + def testHasCapstone__not_found(self): + """If there is no capstone entry, we get False.""" + query_date = datetime.date(2021, 1, 20) + actual = self.uma_query._HasCapstone(query_date) + self.assertFalse(actual) + + def testHasCapstone__found(self): + """If we set a capstone entry, we can find it.""" + query_date = datetime.date(2021, 1, 20) + capstone = self.uma_query._SetCapstone(query_date) + + try: + actual = self.uma_query._HasCapstone(query_date) + finally: + capstone.delete() + + self.assertTrue(actual) + + +class YesterdayHandlerTest(unittest.TestCase): + + def setUp(self): + self.request_path = '/cron/metrics' + self.handler = fetchmetrics.YesterdayHandler() + + @mock.patch('internals.fetchmetrics.UmaQuery.FetchAndSaveData') + def test_get__normal(self, mock_FetchAndSaveData): + """When requested with no date, we check the previous 5 days.""" + mock_FetchAndSaveData.return_value = 200 + today = datetime.date(2021, 1, 20) + + with fetchmetrics.app.test_request_context(self.request_path): + actual_response = self.handler.get_template_data(today=today) + + self.assertEqual('Success', actual_response) + expected_calls = [ + mock.call(datetime.date(2021, 1, day)) + for day in [19, 18, 17, 16, 15] + for query in fetchmetrics.UMA_QUERIES] + mock_FetchAndSaveData.assert_has_calls(expected_calls) + + @mock.patch('internals.fetchmetrics.UmaQuery.FetchAndSaveData') + def test_get__debugging(self, mock_FetchAndSaveData): + """We can request that the app get metrics for one specific day.""" + mock_FetchAndSaveData.return_value = 200 + today = datetime.date(2021, 1, 20) + + with fetchmetrics.app.test_request_context( + self.request_path, query_string={'date': '20210120'}): + actual_response = self.handler.get_template_data(today=today) + + self.assertEqual('Success', actual_response) + expected_calls = [ + mock.call(datetime.date(2021, 1, 20)) + for query in fetchmetrics.UMA_QUERIES] + mock_FetchAndSaveData.assert_has_calls(expected_calls) diff --git a/notifier.py b/internals/notifier.py similarity index 100% rename from notifier.py rename to internals/notifier.py diff --git a/tests/notifier_test.py b/internals/notifier_test.py similarity index 98% rename from tests/notifier_test.py rename to internals/notifier_test.py index 13791072..97c42b9a 100644 --- a/tests/notifier_test.py +++ b/internals/notifier_test.py @@ -29,7 +29,7 @@ from google.appengine.api import mail from google.appengine.api import users import models -import notifier +from internals import notifier import settings @@ -177,7 +177,7 @@ class EmailFormattingTest(unittest.TestCase): actual = notifier.apply_subscription_rules(self.feature_1, changes) self.assertEqual({}, actual) - @mock.patch('notifier.format_email_body') + @mock.patch('internals.notifier.format_email_body') def test_make_email_tasks__new(self, mock_f_e_b): """We send email to component owners and subscribers for new features.""" mock_f_e_b.return_value = 'mock body html' @@ -194,7 +194,7 @@ class EmailFormattingTest(unittest.TestCase): mock_f_e_b.assert_called_once_with( False, self.feature_1, []) - @mock.patch('notifier.format_email_body') + @mock.patch('internals.notifier.format_email_body') def test_make_email_tasks__update(self, mock_f_e_b): """We send email to component owners and subscribers for edits.""" mock_f_e_b.return_value = 'mock body html' @@ -211,7 +211,7 @@ class EmailFormattingTest(unittest.TestCase): mock_f_e_b.assert_called_once_with( True, self.feature_1, self.changes) - @mock.patch('notifier.format_email_body') + @mock.patch('internals.notifier.format_email_body') def test_make_email_tasks__starrer(self, mock_f_e_b): """We send email to users who starred the feature.""" mock_f_e_b.return_value = 'mock body html' @@ -232,7 +232,7 @@ class EmailFormattingTest(unittest.TestCase): True, self.feature_1, self.changes) - @mock.patch('notifier.format_email_body') + @mock.patch('internals.notifier.format_email_body') def test_make_email_tasks__starrer_unsubscribed(self, mock_f_e_b): """We don't email users who starred the feature but opted out.""" mock_f_e_b.return_value = 'mock body html' @@ -421,7 +421,7 @@ class BouncedEmailHandlerTest(unittest.TestCase): settings.APP_ID) self.expected_to = settings.BOUNCE_ESCALATION_ADDR - @mock.patch('notifier.BouncedEmailHandler.receive') + @mock.patch('internals.notifier.BouncedEmailHandler.receive') def test_process_post_data(self, mock_receive): with notifier.app.test_request_context('/_ah/bounce'): actual_json = self.handler.process_post_data() diff --git a/processes.py b/internals/processes.py similarity index 100% rename from processes.py rename to internals/processes.py diff --git a/tests/processes_test.py b/internals/processes_test.py similarity index 99% rename from tests/processes_test.py rename to internals/processes_test.py index 6246a14c..123f2def 100644 --- a/tests/processes_test.py +++ b/internals/processes_test.py @@ -23,7 +23,7 @@ import mock from google.appengine.ext import db import models -import processes +from internals import processes class HelperFunctionsTest(unittest.TestCase): diff --git a/models.py b/models.py index a7880f18..734c130e 100644 --- a/models.py +++ b/models.py @@ -32,7 +32,7 @@ from google.appengine.api import users from framework import cloud_tasks_helpers import settings -import util +from internals import fetchchannels import hack_components import hack_wf_components @@ -503,7 +503,7 @@ class Feature(DictModel): @classmethod def _annotate_first_of_milestones(self, feature_list, version=None): try: - omaha_data = util.get_omaha_data() + omaha_data = fetchchannels.get_omaha_data() win_versions = omaha_data[0]['versions'] diff --git a/tests/models_test.py b/models_test.py similarity index 100% rename from tests/models_test.py rename to models_test.py diff --git a/package.json b/package.json index 261dc255..c4de0964 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,8 @@ "deps": "pip install -t lib -r requirements.txt --upgrade", "travis-deps": "pip install -t lib -r requirements.travis.txt --upgrade", "dev-deps": "pip install -r requirements.dev.txt --upgrade", - "test": "python -m unittest discover -p *_test.py -b", - "coverage": "python -m coverage erase && python -m coverage run -m unittest discover -p *_test.py -s tests -b && python -m coverage html", + "test": "python -m unittest discover -p '*_test.py' -b", + "coverage": "python -m coverage erase && python -m coverage run -m unittest discover -p '*_test.py' -b && python -m coverage html", "lint": "gulp lint-fix && lit-analyzer \"static/elements/chromedash-!(featurelist)*.js\"", "build": "gulp", "watch": "gulp watch", diff --git a/pages/blink_handler.py b/pages/blink_handler.py index eb52e786..445b2b0d 100644 --- a/pages/blink_handler.py +++ b/pages/blink_handler.py @@ -28,7 +28,6 @@ from framework import basehandlers from framework import permissions import models import settings -import util from schedule import construct_chrome_channels_details diff --git a/pages/featuredetail.py b/pages/featuredetail.py index 794a4248..e6979b09 100644 --- a/pages/featuredetail.py +++ b/pages/featuredetail.py @@ -23,7 +23,7 @@ import settings from framework import basehandlers from pages import guideforms import models -import processes +from internals import processes class FeatureDetailHandler(basehandlers.FlaskHandler): diff --git a/pages/guide.py b/pages/guide.py index cd8dd233..e25eb3fb 100644 --- a/pages/guide.py +++ b/pages/guide.py @@ -34,7 +34,7 @@ from framework import basehandlers from framework import utils from pages import guideforms import models -import processes +from internals import processes import settings diff --git a/pages/guideforms.py b/pages/guideforms.py index f0268dec..e58b4fe4 100644 --- a/pages/guideforms.py +++ b/pages/guideforms.py @@ -22,7 +22,7 @@ from django import forms from google.appengine.api import users import models -import processes +from internals import processes SHIPPED_HELP_TXT = ( 'First milestone to ship with this status. Applies to: Enabled by ' diff --git a/pages/intentpreview.py b/pages/intentpreview.py new file mode 100644 index 00000000..cbea47cd --- /dev/null +++ b/pages/intentpreview.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Google Inc. +# +# 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 +# +# 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. + +from __future__ import division +from __future__ import print_function + +from google.appengine.api import users + +import models +import settings +from framework import basehandlers +from internals import processes + +INTENT_PARAM = 'intent' +LAUNCH_PARAM = 'launch' +VIEW_FEATURE_URL = '/feature' + + +class IntentEmailPreviewHandler(basehandlers.FlaskHandler): + """Show a preview of an intent email, as appropriate to the feature stage.""" + + TEMPLATE_PATH = 'admin/features/launch.html' + + def get_template_data(self, feature_id=None, stage_id=None): + user = users.get_current_user() + if user is None: + return self.redirect(users.create_login_url(self.request.path)) + + if not feature_id: + self.abort(404) + f = models.Feature.get_by_id(feature_id) + if f is None: + self.abort(404) + + intent_stage = stage_id if stage_id is not None else f.intent_stage + + if not self.user_can_edit(user): + self.abort(403) + + page_data = self.get_page_data(feature_id, f, intent_stage) + return page_data + + def get_page_data(self, feature_id, f, intent_stage): + """Return a dictionary of data used to render the page.""" + page_data = { + 'subject_prefix': self.compute_subject_prefix(f, intent_stage), + 'feature': f.format_for_template(), + 'sections_to_show': processes.INTENT_EMAIL_SECTIONS.get( + intent_stage, []), + 'intent_stage': intent_stage, + 'default_url': '%s://%s%s/%s' % ( + self.request.scheme, self.request.host, + VIEW_FEATURE_URL, feature_id), + } + + if LAUNCH_PARAM in self.request.args: + page_data[LAUNCH_PARAM] = True + if INTENT_PARAM in self.request.args: + page_data[INTENT_PARAM] = True + + return page_data + + def compute_subject_prefix(self, feature, intent_stage): + """Return part of the subject line for an intent email.""" + + if intent_stage == models.INTENT_INCUBATE: + if feature.feature_type == models.FEATURE_TYPE_DEPRECATION_ID: + return 'Intent to Deprecate and Remove' + elif intent_stage == models.INTENT_IMPLEMENT: + return 'Intent to Prototype' + elif intent_stage == models.INTENT_EXPERIMENT: + return 'Ready for Trial' + elif intent_stage == models.INTENT_EXTEND_TRIAL: + if feature.feature_type == models.FEATURE_TYPE_DEPRECATION_ID: + return 'Request for Deprecation Trial' + else: + return 'Intent to Experiment' + elif intent_stage == models.INTENT_SHIP: + return 'Intent to Ship' + + return 'Intent stage "%s"' % models.INTENT_STAGES[intent_stage] + +app = basehandlers.FlaskApplication([ + ('/admin/features/launch/', IntentEmailPreviewHandler), + ('/admin/features/launch//', + IntentEmailPreviewHandler), +], debug=settings.DEBUG) diff --git a/tests/admin_test.py b/pages/intentpreview_test.py similarity index 60% rename from tests/admin_test.py rename to pages/intentpreview_test.py index 0d14fdd3..3b1b1325 100644 --- a/tests/admin_test.py +++ b/pages/intentpreview_test.py @@ -15,117 +15,16 @@ from __future__ import division from __future__ import print_function -import datetime import unittest import testing_config # Must be imported before the module under test. -import urllib import mock import flask import werkzeug -import admin +from pages import intentpreview import models -import processes -class FetchMetricsTest(unittest.TestCase): - - @mock.patch('settings.PROD', True) - @mock.patch('requests.request') - def test__prod(self, mock_fetch): - """In prod, we actually request metrics from uma-export.""" - mock_fetch.return_value = 'mock response' - actual = admin._FetchMetrics('a url') - - self.assertEqual('mock response', actual) - mock_fetch.assert_called_once_with('GET', - 'a url', timeout=120, allow_redirects=False) - - - @mock.patch('settings.STAGING', True) - @mock.patch('requests.request') - def test__staging(self, mock_fetch): - """In prod, we actually request metrics from uma-export.""" - mock_fetch.return_value = 'mock response' - actual = admin._FetchMetrics('a url') - - self.assertEqual('mock response', actual) - mock_fetch.assert_called_once_with('GET', - 'a url', timeout=120, allow_redirects=False) - - @mock.patch('requests.request') - def test__dev(self, mock_fetch): - """In Dev, we cannot access uma-export.""" - actual = admin._FetchMetrics('a url') - - self.assertIsNone(actual) - mock_fetch.assert_not_called() - - -class UmaQueryTest(unittest.TestCase): - - def setUp(self): - self.uma_query = admin.UmaQuery( - query_name='usecounter.features', - model_class=models.FeatureObserver, - property_map_class=models.FeatureObserverHistogram) - - def testHasCapstone__not_found(self): - """If there is no capstone entry, we get False.""" - query_date = datetime.date(2021, 1, 20) - actual = self.uma_query._HasCapstone(query_date) - self.assertFalse(actual) - - def testHasCapstone__found(self): - """If we set a capstone entry, we can find it.""" - query_date = datetime.date(2021, 1, 20) - capstone = self.uma_query._SetCapstone(query_date) - - try: - actual = self.uma_query._HasCapstone(query_date) - finally: - capstone.delete() - - self.assertTrue(actual) - - -class YesterdayHandlerTest(unittest.TestCase): - - def setUp(self): - self.request_path = '/cron/metrics' - self.handler = admin.YesterdayHandler() - - @mock.patch('admin.UmaQuery.FetchAndSaveData') - def test_get__normal(self, mock_FetchAndSaveData): - """When requested with no date, we check the previous 5 days.""" - mock_FetchAndSaveData.return_value = 200 - today = datetime.date(2021, 1, 20) - - with admin.app.test_request_context(self.request_path): - actual_response = self.handler.get_template_data(today=today) - - self.assertEqual('Success', actual_response) - expected_calls = [ - mock.call(datetime.date(2021, 1, day)) - for day in [19, 18, 17, 16, 15] - for query in admin.UMA_QUERIES] - mock_FetchAndSaveData.assert_has_calls(expected_calls) - - @mock.patch('admin.UmaQuery.FetchAndSaveData') - def test_get__debugging(self, mock_FetchAndSaveData): - """We can request that the app get metrics for one specific day.""" - mock_FetchAndSaveData.return_value = 200 - today = datetime.date(2021, 1, 20) - - with admin.app.test_request_context( - self.request_path, query_string={'date': '20210120'}): - actual_response = self.handler.get_template_data(today=today) - - self.assertEqual('Success', actual_response) - expected_calls = [ - mock.call(datetime.date(2021, 1, 20)) - for query in admin.UMA_QUERIES] - mock_FetchAndSaveData.assert_has_calls(expected_calls) class IntentEmailPreviewHandlerTest(unittest.TestCase): @@ -139,7 +38,7 @@ class IntentEmailPreviewHandlerTest(unittest.TestCase): self.request_path = '/admin/features/launch/%d/%d?intent' % ( models.INTENT_SHIP, self.feature_1.key().id()) - self.handler = admin.IntentEmailPreviewHandler() + self.handler = intentpreview.IntentEmailPreviewHandler() def tearDown(self): self.feature_1.delete() @@ -148,7 +47,7 @@ class IntentEmailPreviewHandlerTest(unittest.TestCase): """Anon cannot view this preview features, gets redirected to login.""" testing_config.sign_out() feature_id = self.feature_1.key().id() - with admin.app.test_request_context(self.request_path): + with intentpreview.app.test_request_context(self.request_path): actual_response = self.handler.get_template_data(feature_id=feature_id) self.assertEqual('302 FOUND', actual_response.status) @@ -156,7 +55,7 @@ class IntentEmailPreviewHandlerTest(unittest.TestCase): """Trying to view a feature that does not exist gives a 404.""" testing_config.sign_in('user1@google.com', 123567890) bad_feature_id = self.feature_1.key().id() + 1 - with admin.app.test_request_context(self.request_path): + with intentpreview.app.test_request_context(self.request_path): with self.assertRaises(werkzeug.exceptions.NotFound): self.handler.get_template_data(feature_id=bad_feature_id) @@ -166,7 +65,7 @@ class IntentEmailPreviewHandlerTest(unittest.TestCase): '/admin/features/launch/%d?intent' % self.feature_1.key().id()) testing_config.sign_in('user1@google.com', 123567890) feature_id = self.feature_1.key().id() - with admin.app.test_request_context(self.request_path): + with intentpreview.app.test_request_context(self.request_path): actual_data = self.handler.get_template_data(feature_id=feature_id) self.assertIn('feature', actual_data) self.assertEqual('feature one', actual_data['feature']['name']) @@ -175,7 +74,7 @@ class IntentEmailPreviewHandlerTest(unittest.TestCase): """Allowed user can preview intent email for a feature.""" testing_config.sign_in('user1@google.com', 123567890) feature_id = self.feature_1.key().id() - with admin.app.test_request_context(self.request_path): + with intentpreview.app.test_request_context(self.request_path): actual_data = self.handler.get_template_data(feature_id=feature_id) self.assertIn('feature', actual_data) self.assertEqual('feature one', actual_data['feature']['name']) @@ -183,7 +82,7 @@ class IntentEmailPreviewHandlerTest(unittest.TestCase): def test_get_page_data(self): """page_data has correct values.""" feature_id = self.feature_1.key().id() - with admin.app.test_request_context(self.request_path): + with intentpreview.app.test_request_context(self.request_path): page_data = self.handler.get_page_data( feature_id, self.feature_1, models.INTENT_IMPLEMENT) self.assertEqual( diff --git a/pages/metrics.py b/pages/metrics.py index f9ea6563..a828f45a 100644 --- a/pages/metrics.py +++ b/pages/metrics.py @@ -24,10 +24,9 @@ from framework import basehandlers from framework import utils from pages import guideforms import models -import processes from framework import ramcache from framework import utils -import util +from internals import fetchchannels from google.appengine.api import users @@ -74,7 +73,7 @@ class OmahaDataHandler(basehandlers.FlaskHandler): JSONIFY = True def get_template_data(self): - omaha_data = util.get_omaha_data() + omaha_data = fetchchannels.get_omaha_data() return omaha_data @@ -99,8 +98,6 @@ routes = [ ('/metrics/feature/timeline/popularity', FeaturePopularityHandler), ('/metrics/feature/timeline/popularity/', FeaturePopularityHandler), - # TODO(jrobbins): util.py has only one thing in it, so maybe move - # it and this handler to a new omaha.py file. ('/omaha_data', OmahaDataHandler), ] diff --git a/pages/schedule.py b/pages/schedule.py index 1b28ffa6..404cc307 100644 --- a/pages/schedule.py +++ b/pages/schedule.py @@ -29,7 +29,7 @@ from google.appengine.api import users from framework import basehandlers import models import settings -import util +from internals import fetchchannels SCHEDULE_CACHE_TIME = 60 * 60 # 1 hour @@ -67,7 +67,7 @@ def fetch_chrome_release_info(version): return data def construct_chrome_channels_details(): - omaha_data = util.get_omaha_data() + omaha_data = fetchchannels.get_omaha_data() channels = {} win_versions = omaha_data[0]['versions'] diff --git a/pages/schedule_test.py b/pages/schedule_test.py index 2510435a..9870589d 100644 --- a/pages/schedule_test.py +++ b/pages/schedule_test.py @@ -25,7 +25,7 @@ from pages import schedule class ScheduleFunctionsTest(unittest.TestCase): @mock.patch('pages.schedule.fetch_chrome_release_info') - @mock.patch('util.get_omaha_data') + @mock.patch('internals.fetchchannels.get_omaha_data') def test_construct_chrome_channels_details( self, mock_get_omaha_data, mock_fetch_chrome_release_info): win_data = {