* remove old schema migration scripts

* delete test file
This commit is contained in:
Daniel Smith 2023-03-20 02:26:06 -07:00 коммит произвёл GitHub
Родитель 772746640c
Коммит 7405abb2e6
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
3 изменённых файлов: 4 добавлений и 1190 удалений

Просмотреть файл

@ -16,386 +16,12 @@ import logging
from google.cloud import ndb # type: ignore
from framework.basehandlers import FlaskHandler
from internals import approval_defs, stage_helpers
from internals.core_models import FeatureEntry, MilestoneSet, Stage
from internals.legacy_models import Feature, Approval, Comment
from internals.review_models import Activity, Gate, Vote
from internals import approval_defs
from internals.core_models import FeatureEntry, Stage
from internals.legacy_models import Feature
from internals.review_models import Gate, Vote
from internals.core_enums import *
# Gate type enums (declared in approval_defs.py)
PROTOTYPE_ENUM = 1
OT_ENUM = 2
EXTEND_ENUM = 3
SHIP_ENUM = 4
def handle_migration(original_cls, new_cls, kwarg_mapping,
special_handler=None):
originals = original_cls.query().fetch()
new_keys = new_cls.query().fetch(keys_only=True)
new_ids = set(key.integer_id() for key in new_keys)
migration_count = 0
for original in originals:
# Check if a new entity with the same ID has already been created.
# If so, do not create the entity again.
if original.key.integer_id() in new_ids:
continue
kwargs = {new_field : getattr(original, old_field)
for (new_field, old_field) in kwarg_mapping}
kwargs['id'] = original.key.integer_id()
# If any fields need special mapping, handle them in the given method.
if callable(special_handler):
special_handler(original, kwargs)
new_entity = new_cls(**kwargs)
new_entity.put()
migration_count += 1
message = (f'{migration_count} {original_cls.__name__} entities migrated '
f'to {new_cls.__name__} entities.')
logging.info(message)
return message
class MigrateCommentsToActivities(FlaskHandler):
def get_template_data(self, **kwargs):
"""Writes an Activity entity for each unmigrated Comment entity."""
self.require_cron_header()
logging.info(self._remove_bad_id_activities())
kwarg_mapping = [
('feature_id', 'feature_id'),
('gate_id', 'field_id'),
('author', 'author'),
('content', 'content'),
('deleted_by', 'deleted_by'),
('created', 'created')]
return handle_migration(Comment, Activity, kwarg_mapping)
def _remove_bad_id_activities(self):
"""Deletes old Activity entities that do not have a matching comment ID."""
q = Activity.query()
activities = q.fetch()
old_migrations_deleted = 0
for activity in activities:
# Non-empty content field means this is an Activity entity
# that represents a comment.
if activity.content:
# Check if there is a Comment entity with a matching ID.
q = Comment.query().filter(
Comment.key == ndb.Key(Comment, activity.key.integer_id()))
comments_with_same_id = q.fetch()
if len(comments_with_same_id) != 1:
# If not, it is from the old migration and it can be deleted.
activity.key.delete()
old_migrations_deleted += 1
return (f'{old_migrations_deleted} Activities deleted '
'from previous migration.')
class MigrateEntities(FlaskHandler):
def get_template_data(self, **kwargs):
"""Write FeatureEntry, Stage, Gate, and Vote entities"""
self.require_cron_header()
# Feature -> FeatureEntry mapping.
kwarg_mapping = [
('created', 'created'),
('updated', 'updated'),
('accurate_as_of', 'accurate_as_of'),
('creator_email', 'creator'), # Renamed
('owner_emails', 'owner'), # Renamed
('editor_emails', 'editors'), # Renamed
('unlisted', 'unlisted'),
('cc_emails', 'cc_recipients'), # Renamed
('feature_notes', 'comments'), # Renamed
('deleted', 'deleted'),
('name', 'name'),
('summary', 'summary'),
('category', 'category'),
('blink_components', 'blink_components'),
('star_count', 'star_count'),
('search_tags', 'search_tags'),
('feature_type', 'feature_type'),
('intent_stage', 'intent_stage'),
('bug_url', 'bug_url'),
('launch_bug_url', 'launch_bug_url'),
('impl_status_chrome', 'impl_status_chrome'),
('flag_name', 'flag_name'),
('ongoing_constraints', 'ongoing_constraints'),
('motivation', 'motivation'),
('devtrial_instructions', 'devtrial_instructions'),
('activation_risks', 'activation_risks'),
('measurement', 'measurement'),
('initial_public_proposal_url', 'initial_public_proposal_url'),
('explainer_links', 'explainer_links'),
('requires_embedder_support', 'requires_embedder_support'),
('standard_maturity', 'standard_maturity'),
('spec_link', 'spec_link'),
('api_spec', 'api_spec'),
('spec_mentor_emails', 'spec_mentors'), # Renamed
('interop_compat_risks', 'interop_compat_risks'),
('prefixed', 'prefixed'),
('all_platforms', 'all_platforms'),
('all_platforms_descr', 'all_platforms_descr'),
('tag_review', 'tag_review'),
('tag_review_status', 'tag_review_status'),
('non_oss_deps', 'non_oss_deps'),
('anticipated_spec_changes', 'anticipated_spec_changes'),
('ff_views', 'ff_views'),
('safari_views', 'safari_views'),
('web_dev_views', 'web_dev_views'),
('ff_views_link', 'ff_views_link'),
('safari_views_link', 'safari_views_link'),
('web_dev_views_link', 'web_dev_views_link'),
('ff_views_notes', 'ff_views_notes'),
('safari_views_notes', 'safari_views_notes'),
('web_dev_views_notes', 'web_dev_views_notes'),
('other_views_notes', 'other_views_notes'),
('security_risks', 'security_risks'),
('security_review_status', 'security_review_status'),
('privacy_review_status', 'privacy_review_status'),
('ergonomics_risks', 'ergonomics_risks'),
('wpt', 'wpt'),
('wpt_descr', 'wpt_descr'),
('webview_risks', 'webview_risks'),
('devrel_emails', 'devrel'), # Renamed
('debuggability', 'debuggability'),
('doc_links', 'doc_links'),
('sample_links', 'sample_links'),
('experiment_timeline', 'experiment_timeline')]
return handle_migration(Feature, FeatureEntry, kwarg_mapping,
self.special_handler)
@classmethod
def special_handler(cls, original_entity, kwargs):
# updater_email will use the email from the updated_by field
kwargs['updater_email'] = (original_entity.updated_by.email()
if original_entity.updated_by else None)
# If Feature is being migrated, then Stages, Gates, and Votes will need to
# also be migrated.
cls.write_stages_for_feature(original_entity)
@classmethod
def write_stages_for_feature(cls, feature):
"""Creates new Stage entities MilestoneSet entities based on Feature data"""
# Create MilestoneSets for each major paradigm.
devtrial_mstones = MilestoneSet(
desktop_first=feature.dt_milestone_desktop_start,
android_first=feature.dt_milestone_android_start,
ios_first=feature.dt_milestone_ios_start,
webview_first=feature.dt_milestone_webview_start)
ot_mstones = MilestoneSet(
desktop_first=feature.ot_milestone_desktop_start,
desktop_last=feature.ot_milestone_desktop_end,
android_first=feature.ot_milestone_android_start,
android_last=feature.ot_milestone_android_end,
webview_first=feature.ot_milestone_webview_start,
webview_last=feature.ot_milestone_webview_end)
extension_mstones = MilestoneSet(
desktop_last=feature.ot_milestone_desktop_end,
android_last=feature.ot_milestone_android_end,
webview_last=feature.ot_milestone_webview_end)
ship_mstones = MilestoneSet(
desktop_first=feature.shipped_milestone,
android_first=feature.shipped_android_milestone,
ios_first=feature.shipped_ios_milestone,
webview_first=feature.shipped_webview_milestone)
# Depending on the feature type,
# create a different group of Stage entities.
f_type = feature.feature_type
if f_type == FEATURE_TYPE_INCUBATE_ID:
cls.write_incubate_stages(
feature, devtrial_mstones, ot_mstones, extension_mstones, ship_mstones)
elif f_type == FEATURE_TYPE_EXISTING_ID:
cls.write_existing_stages(
feature, devtrial_mstones, ot_mstones, extension_mstones, ship_mstones)
elif f_type == FEATURE_TYPE_CODE_CHANGE_ID:
cls.write_code_change_stages(
feature, devtrial_mstones, ship_mstones)
elif f_type == FEATURE_TYPE_DEPRECATION_ID:
cls.write_deprecation_stages(
feature, devtrial_mstones, ot_mstones, extension_mstones, ship_mstones)
else:
logging.error(f'Invalid feature type {f_type} for {feature.name}')
@classmethod
def write_gate(cls, feature_id, stage_id, gate_type):
"""Writes a Gate entity to match the stage created."""
gate = Gate(feature_id=feature_id, stage_id=stage_id,
gate_type=gate_type, state=Vote.NA)
gate.put()
@classmethod
def write_incubate_stages(cls, feature, devtrial_mstones, ot_mstones,
extension_mstones, ship_mstones):
feature_id = feature.key.integer_id()
kwargs = {'feature_id': feature_id, 'browser': 'Chrome'}
stage = Stage(stage_type=STAGE_BLINK_INCUBATE, **kwargs)
stage.put()
stage = Stage(stage_type=STAGE_BLINK_PROTOTYPE,
intent_thread_url=feature.intent_to_implement_url, **kwargs)
stage.put()
cls.write_gate(feature_id, stage.key.integer_id(), PROTOTYPE_ENUM)
stage = Stage(stage_type=STAGE_BLINK_DEV_TRIAL, milestones=devtrial_mstones,
announcement_url=feature.ready_for_trial_url, **kwargs)
stage.put()
stage = Stage(stage_type=STAGE_BLINK_EVAL_READINESS, **kwargs)
stage.put()
stage = Stage(stage_type=STAGE_BLINK_ORIGIN_TRIAL, milestones=ot_mstones,
intent_thread_url=feature.intent_to_experiment_url,
experiment_goals=feature.experiment_goals,
origin_trial_feedback_url=feature.origin_trial_feedback_url, **kwargs)
stage.put()
cls.write_gate(feature_id, stage.key.integer_id(), OT_ENUM)
stage = Stage(stage_type=STAGE_BLINK_EXTEND_ORIGIN_TRIAL,
milestones=extension_mstones,
intent_thread_url=feature.intent_to_extend_experiment_url,
experiment_extension_reason=feature.experiment_extension_reason,
ot_stage_id=stage.key.integer_id(), **kwargs)
stage.put()
cls.write_gate(feature_id, stage.key.integer_id(), EXTEND_ENUM)
stage = Stage(stage_type=STAGE_BLINK_SHIPPING, milestones=ship_mstones,
intent_thread_url=feature.intent_to_ship_url,
finch_url=feature.finch_url, **kwargs)
stage.put()
cls.write_gate(feature_id, stage.key.integer_id(), SHIP_ENUM)
@classmethod
def write_existing_stages(cls, feature, devtrial_mstones, ot_mstones,
extension_mstones, ship_mstones):
feature_id = feature.key.integer_id()
kwargs = {'feature_id': feature_id, 'browser': 'Chrome'}
stage = Stage(stage_type=STAGE_FAST_PROTOTYPE,
intent_thread_url=feature.intent_to_implement_url, **kwargs)
stage.put()
cls.write_gate(feature_id, stage.key.integer_id(), PROTOTYPE_ENUM)
stage = Stage(stage_type=STAGE_FAST_DEV_TRIAL, milestones=devtrial_mstones,
announcement_url=feature.ready_for_trial_url, **kwargs)
stage.put()
stage = Stage(stage_type=STAGE_FAST_ORIGIN_TRIAL, milestones=ot_mstones,
intent_thread_url=feature.intent_to_experiment_url,
experiment_goals=feature.experiment_goals,
origin_trial_feedback_url=feature.origin_trial_feedback_url, **kwargs)
stage.put()
cls.write_gate(feature_id, stage.key.integer_id(), OT_ENUM)
stage = Stage(stage_type=STAGE_FAST_EXTEND_ORIGIN_TRIAL,
milestones=extension_mstones,
intent_thread_url=feature.intent_to_extend_experiment_url,
experiment_extension_reason=feature.experiment_extension_reason,
ot_stage_id=stage.key.integer_id(), **kwargs)
stage.put()
cls.write_gate(feature_id, stage.key.integer_id(), EXTEND_ENUM)
stage = Stage(stage_type=STAGE_FAST_SHIPPING, milestones=ship_mstones,
intent_thread_url=feature.intent_to_ship_url,
finch_url=feature.finch_url, **kwargs)
stage.put()
cls.write_gate(feature_id, stage.key.integer_id(), SHIP_ENUM)
@classmethod
def write_code_change_stages(cls, feature, devtrial_mstones, ship_mstones):
feature_id = feature.key.integer_id()
kwargs = {'feature_id': feature_id, 'browser': 'Chrome'}
stage = Stage(stage_type=STAGE_PSA_IMPLEMENT, **kwargs)
stage.put()
stage = Stage(stage_type=STAGE_PSA_DEV_TRIAL, milestones=devtrial_mstones,
announcement_url=feature.ready_for_trial_url, **kwargs)
stage.put()
stage = Stage(stage_type=STAGE_PSA_SHIPPING, milestones=ship_mstones,
intent_thread_url=feature.intent_to_ship_url,
finch_url=feature.finch_url, **kwargs)
stage.put()
cls.write_gate(feature_id, stage.key.integer_id(), SHIP_ENUM)
@classmethod
def write_deprecation_stages(cls, feature, devtrial_mstones, ot_mstones,
extension_mstones, ship_mstones):
feature_id = feature.key.integer_id()
kwargs = {'feature_id': feature_id, 'browser': 'Chrome'}
stage = Stage(stage_type=STAGE_DEP_PLAN, **kwargs)
stage.put()
stage = Stage(stage_type=STAGE_DEP_DEV_TRIAL, milestones=devtrial_mstones,
announcement_url=feature.ready_for_trial_url, **kwargs)
stage.put()
stage = Stage(stage_type=STAGE_DEP_DEPRECATION_TRIAL, milestones=ot_mstones,
intent_thread_url=feature.intent_to_experiment_url,
experiment_goals=feature.experiment_goals,
origin_trial_feedback_url=feature.origin_trial_feedback_url, **kwargs)
stage.put()
cls.write_gate(feature_id, stage.key.integer_id(), OT_ENUM)
stage = Stage(stage_type=STAGE_DEP_EXTEND_DEPRECATION_TRIAL,
milestones=extension_mstones,
intent_thread_url=feature.intent_to_extend_experiment_url,
experiment_extension_reason=feature.experiment_extension_reason,
ot_stage_id=stage.key.integer_id(), **kwargs)
stage.put()
cls.write_gate(feature_id, stage.key.integer_id(), EXTEND_ENUM)
stage = Stage(stage_type=STAGE_DEP_SHIPPING, milestones=ship_mstones,
intent_thread_url=feature.intent_to_ship_url,
finch_url=feature.finch_url, **kwargs)
stage.put()
cls.write_gate(feature_id, stage.key.integer_id(), SHIP_ENUM)
stage = Stage(stage_type=STAGE_DEP_REMOVE_CODE, **kwargs)
stage.put()
class MigrateApprovalsToVotes(FlaskHandler):
def get_template_data(self, **kwargs):
"""Migrate all Approval entities to Vote entities."""
self.require_cron_header()
approvals: ndb.Query = Approval.query()
count = 0
for approval in approvals:
vote = Vote.get_by_id(approval.key.integer_id())
if vote:
continue
gates = Gate.query(
Gate.feature_id == approval.feature_id,
Gate.gate_type == approval.field_id).fetch()
# Skip if no gate is found for the given approval.
if len(gates) == 0:
continue
gate_id = gates[0].key.integer_id()
gate_type = gates[0].gate_type
vote = Vote(id=approval.key.integer_id(), feature_id=approval.feature_id,
gate_id=gate_id, gate_type=gate_type, state=approval.state,
set_on=approval.set_on, set_by=approval.set_by)
vote.put()
count += 1
return f'{count} Approval entities migrated to Vote entities.'
class EvaluateGateStatus(FlaskHandler):
@ -412,91 +38,6 @@ class EvaluateGateStatus(FlaskHandler):
return f'{count} Gate entities reevaluated.'
class DeleteNewEntities(FlaskHandler):
def get_template_data(self, **kwargs) -> str:
"""Deletes every entity for each kind in the new schema."""
self.require_cron_header()
kinds: list[ndb.Model] = [FeatureEntry, Gate, Stage, Vote]
count = 0
for kind in kinds:
for entity in kind.query():
entity.key.delete()
count += 1
return f'{count} entities deleted.'
class WriteUpdatedField(FlaskHandler):
def get_template_data(self, **kwargs) -> str:
"""Sets the FeatureEntry updated field if it is not initialized."""
self.require_cron_header()
count = 0
for fe in FeatureEntry.query():
if fe.updated is None:
fe.updated = fe.created
fe.put()
count += 1
return f'{count} FeatureEntry entities given updated field values.'
class UpdateDeprecatedViews(FlaskHandler):
def get_template_data(self, **kwargs):
"""Migrates deprecated feature views fields to their new values"""
self.require_cron_header()
for fe in FeatureEntry.query():
fe_changed = False
if fe.ff_views == MIXED_SIGNALS:
fe_changed = True
fe.ff_views = NO_PUBLIC_SIGNALS
elif fe.ff_views == PUBLIC_SKEPTICISM:
fe_changed = True
fe.ff_views = OPPOSED
if fe.safari_views == MIXED_SIGNALS:
fe_changed = True
fe.safari_views = NO_PUBLIC_SIGNALS
elif fe.safari_views == PUBLIC_SKEPTICISM:
fe_changed = True
fe.safari_views = OPPOSED
if fe_changed:
fe.put()
for f in Feature.query():
f_changed = False
if f.ff_views == MIXED_SIGNALS:
f_changed = True
f.ff_views = NO_PUBLIC_SIGNALS
if f.ff_views == PUBLIC_SKEPTICISM:
f_changed = True
f.ff_views = OPPOSED
if f.ie_views == MIXED_SIGNALS:
f_changed = True
f.ie_views = NO_PUBLIC_SIGNALS
elif f.ie_views == PUBLIC_SKEPTICISM:
f_changed = True
f.ie_views = OPPOSED
if f.safari_views == MIXED_SIGNALS:
f_changed = True
f.safari_views = NO_PUBLIC_SIGNALS
elif f.safari_views == PUBLIC_SKEPTICISM:
f_changed = True
f.safari_views = OPPOSED
if f_changed:
f.put()
return 'Feature and FeatureEntry view fields updated.'
class WriteMissingGates(FlaskHandler):
GATES_TO_CREATE_PER_RUN = 5000
@ -557,157 +98,6 @@ class WriteMissingGates(FlaskHandler):
return f'{len(gates_to_write)} missing gates created for stages.'
class CalcActiveStages(FlaskHandler):
def get_template_data(self, **kwargs):
"""Calculates the active stage of features based on the intent stage."""
self.require_cron_header()
active_stages_set = 0
stages_created = 0
for fe in FeatureEntry.query():
# Don't try to detect active stage if it's already set.
if fe.active_stage_id is not None:
continue
feature_id = fe.key.integer_id()
# Check which stage type is associated with the active intent stage.
active_stage_type = (
STAGE_TYPES_BY_INTENT_STAGE[fe.feature_type].get(fe.intent_stage))
# If a matching stage type isn't found, don't set it.
if active_stage_type is None:
continue
else:
active_stage = Stage.query(
Stage.stage_type == active_stage_type,
Stage.feature_id == feature_id).get()
# Find the stage ID and set active stage field to this ID.
if active_stage:
fe.active_stage_id = active_stage.key.integer_id()
else:
# If the stage doesn't exist, create it.
# This probably shouldn't need to happen if everything is
# migrated correctly.
stage = Stage(feature_id=feature_id, stage_type=active_stage_type)
stage.put()
stages_created += 1
fe.active_stage_id = stage.key.integer_id()
active_stages_set += 1
fe.put()
return (f'{active_stages_set} active stages set for features and '
f'{stages_created} stages created for features.')
class CreateTrialExtensionStages(FlaskHandler):
def get_template_data(self, **kwargs):
"""Associate trial extension stages with the correct origin trial stages."""
self.require_cron_header()
stages_created = 0
# Query for all origin trial stages.
ot_queries = [
Stage.query(Stage.stage_type == STAGE_BLINK_ORIGIN_TRIAL),
Stage.query(Stage.stage_type == STAGE_FAST_ORIGIN_TRIAL),
Stage.query(Stage.stage_type == STAGE_DEP_DEPRECATION_TRIAL)]
for q in ot_queries:
for s in q:
stage_id = s.key.integer_id()
# Query for an extension stage associated with this trial stage.
# There should typically be one for now until trial extension
# functionality is available to all users.
extension_stage = (
Stage.query(Stage.ot_stage_id == stage_id).get())
# If there isn't an extension stage, create one and associate it with
# this trial stage.
if extension_stage is None:
extension_stage = Stage(feature_id=s.feature_id,
stage_type=OT_EXTENSION_STAGE_TYPES_MAPPING[s.stage_type],
ot_stage_id=stage_id)
extension_stage.put()
stages_created += 1
return (f'{stages_created} extension stages created for '
'existing trial stages.')
class MigrateSubjectLineField(FlaskHandler):
def get_template_data(self, **kwargs) -> str:
"""Migrates old Feature subject line fields to their respective stages."""
self.require_cron_header()
count = 0
for f in Feature.query():
f_id = f.key.integer_id()
f_type = f.feature_type
# If there are no subject line fields present, no need to migrate.
if (not f.intent_to_implement_subject_line and
not f.intent_to_ship_subject_line and
not f.intent_to_experiment_subject_line and
not f.intent_to_extend_experiment_subject_line):
continue
stages_to_update = []
stages = stage_helpers.get_feature_stages(f_id)
# Check each corresponding FeatureEntry stage to migrate the
# intent subject line if needed.
proto_stage_type = STAGE_TYPES_PROTOTYPE[f_type] or -1
prototype_stages = stages.get(proto_stage_type, [])
if (f.intent_to_implement_subject_line and
# If there are more than 1 stage for a specific stage type,
# we can't be sure which is the correct intent, so don't migrate.
# (this should be very rare).
len(prototype_stages) == 1 and
not prototype_stages[0].intent_subject_line):
prototype_stages[0].intent_subject_line = (
f.intent_to_implement_subject_line)
stages_to_update.append(prototype_stages[0])
ship_stage_type = STAGE_TYPES_SHIPPING[f_type] or -1
ship_stages = stages.get(ship_stage_type, [])
if (f.intent_to_ship_subject_line and
len(ship_stages) == 1 and
not ship_stages[0].intent_subject_line):
ship_stages[0].intent_subject_line = (
f.intent_to_ship_subject_line)
stages_to_update.append(ship_stages[0])
ot_stage_type = STAGE_TYPES_ORIGIN_TRIAL[f_type] or -1
ot_stages = stages.get(ot_stage_type, [])
if (f.intent_to_experiment_subject_line and
len(ot_stages) == 1 and
not ot_stages[0].intent_subject_line):
ot_stages[0].intent_subject_line = (
f.intent_to_experiment_subject_line)
stages_to_update.append(ot_stages[0])
extension_stage_type = STAGE_TYPES_EXTEND_ORIGIN_TRIAL[f_type] or -1
extension_stages = stages.get(extension_stage_type, [])
if (f.intent_to_extend_experiment_subject_line and
len(extension_stages) == 1 and
not extension_stages[0].intent_subject_line):
extension_stages[0].intent_subject_line = (
f.intent_to_extend_experiment_subject_line)
stages_to_update.append(extension_stages[0])
# Save changes to all updated Stage entities.
if stages_to_update:
ndb.put_multi(stages_to_update)
count += len(stages_to_update)
return f'{count} subject line fields migrated.'
class MigrateLGTMFields(FlaskHandler):
def get_template_data(self, **kwargs) -> str:

Просмотреть файл

@ -1,558 +0,0 @@
# Copyright 2022 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 google.cloud import ndb # type: ignore
import testing_config # Must be imported before the module under test.
from datetime import datetime
from internals.core_enums import *
from internals.core_models import FeatureEntry, Stage
from internals.legacy_models import Feature, Approval, Comment
from internals.review_models import Activity, Gate, Vote
from internals import schema_migration
class MigrateCommentsToActivitiesTest(testing_config.CustomTestCase):
def setUp(self):
comment_1 = Comment(id=1, feature_id=1, field_id=1,
author='user@example.com', content='some text',
created=datetime(2020, 1, 1))
comment_1.put()
comment_2 = Comment(id=2, feature_id=1, field_id=2,
author='other_user@example.com', content='some other text',
created=datetime(2020, 1, 1))
comment_2.put()
# Comment 3 is already migrated.
comment_3 = Comment(id=3, feature_id=2, field_id=1,
author='user@example.com', content='migrated text')
comment_3.put()
activity_3 = Activity(id=3, feature_id=2, gate_id=1,
author='user@example.com', content='migrated text')
activity_3.put()
def tearDown(self):
for comm in Comment.query().fetch():
comm.key.delete()
for activity in Activity.query().fetch():
activity.key.delete()
def test_migration__remove_bad_activities(self):
migration_handler = schema_migration.MigrateCommentsToActivities()
bad_activity = Activity(id=9, feature_id=1, gate_id=1,
author='user@example.com', content='some text')
bad_activity.put()
result = migration_handler._remove_bad_id_activities()
# One Activity is from an older version of the migration. Should be removed.
expected = '1 Activities deleted from previous migration.'
self.assertEqual(result, expected)
# One other Activity should still exist.
activities = Activity.query().fetch()
self.assertTrue(len(activities) == 1)
def test_migration(self):
migration_handler = schema_migration.MigrateCommentsToActivities()
result = migration_handler.get_template_data()
# One comment is already migrated, so only 2 need migration.
expected = '2 Comment entities migrated to Activity entities.'
self.assertEqual(result, expected)
activities = Activity.query().fetch()
self.assertEqual(len(activities), 3)
self.assertEqual(2020, activities[0].created.year)
# The migration should be idempotent, so nothing should be migrated twice.
result_2 = migration_handler.get_template_data()
expected = '0 Comment entities migrated to Activity entities.'
self.assertEqual(result_2, expected)
class MigrateEntitiesTest(testing_config.CustomTestCase):
# Fields that do not require name change or revisions during migration.
FEATURE_FIELDS = ['created', 'updated', 'accurate_as_of',
'unlisted', 'deleted', 'name', 'summary', 'category',
'blink_components', 'star_count', 'search_tags',
'feature_type', 'intent_stage', 'bug_url', 'launch_bug_url',
'impl_status_chrome', 'flag_name', 'ongoing_constraints', 'motivation',
'devtrial_instructions', 'activation_risks', 'measurement',
'initial_public_proposal_url', 'explainer_links',
'requires_embedder_support', 'standard_maturity', 'spec_link',
'api_spec', 'interop_compat_risks',
'prefixed', 'all_platforms', 'all_platforms_descr', 'tag_review',
'tag_review_status', 'non_oss_deps', 'anticipated_spec_changes',
'ff_views', 'safari_views', 'web_dev_views', 'ff_views_link',
'safari_views_link', 'web_dev_views_link', 'ff_views_notes',
'safari_views_notes', 'web_dev_views_notes', 'other_views_notes',
'security_risks', 'security_review_status', 'privacy_review_status',
'ergonomics_risks', 'wpt', 'wpt_descr', 'webview_risks',
'debuggability', 'doc_links', 'sample_links', 'experiment_timeline']
# (Feature field, FeatureEntry field)
RENAMED_FIELDS = [('creator', 'creator_email'),
('owner', 'owner_emails'),
('editors', 'editor_emails'),
('cc_recipients', 'cc_emails'),
('spec_mentors', 'spec_mentor_emails'),
('devrel', 'devrel_emails'),
('comments', 'feature_notes')]
def setUp(self):
self.feature_1 = Feature(
id=1,
created=datetime(2020, 1, 1),
updated=datetime(2020, 7, 1),
accurate_as_of=datetime(2020, 3, 1),
created_by=ndb.User(
_auth_domain='example.com', email='user@example.com'),
updated_by=ndb.User(
_auth_domain='example.com', email='editor@example.com'),
owner=['owner@example.com'],
creator='creator@example.com',
editors=['editor@example.com'],
cc_recipients=['cc_user@example.com'],
unlisted=False,
deleted=False,
name='feature_one',
summary='newly migrated summary',
comments='Some comments.',
category=1,
blink_components=['Blink'],
star_count=3,
search_tags=['tag1', 'tag2'],
feature_type=3,
intent_stage=1,
bug_url='https://bug.example.com',
launch_bug_url='https://bug.example.com',
impl_status_chrome=1,
flag_name='flagname',
ongoing_constraints='constraints',
motivation='motivation',
devtrial_instructions='instructions',
activation_risks='risks',
measurement=None,
shipped_milestone=105,
intent_to_ship_url='https://example.com/intentship',
ot_milestone_desktop_start=101,
ot_milestone_desktop_end=104,
ot_milestone_android_start = 102,
ot_milestone_android_end=104,
intent_to_experiment_url='https://example.com/intentexperiment',
finch_url='https://example.com/finch',
initial_public_proposal_url='proposal.example.com',
explainer_links=['explainer.example.com'],
requires_embedder_support=False,
standard_maturity=1,
spec_link='spec.example.com',
api_spec=False,
spec_mentors=['mentor1', 'mentor2'],
interop_compat_risks='risks',
prefixed=True,
all_platforms=True,
all_platforms_descr='All platforms',
tag_review='tag_review',
tag_review_status=1,
non_oss_deps='oss_deps',
anticipated_spec_changes='spec_changes',
ff_views=1,
safari_views=1,
web_dev_views=1,
ff_views_link='view.example.com',
safari_views_link='view.example.com',
web_dev_views_link='view.example.com',
ff_views_notes='notes',
safari_views_notes='notes',
web_dev_views_notes='notes',
other_views_notes='notes',
security_risks='risks',
security_review_status=1,
privacy_review_status=1,
ergonomics_risks='risks',
wpt=True,
wpt_descr='description',
webview_risks='risks',
devrel=['devrel'],
debuggability='debuggability',
doc_links=['link1.example.com', 'link2.example.com'],
sample_links=[])
self.feature_1.put()
self.feature_2 = Feature(
id=2,
created=datetime(2020, 4, 1),
updated=datetime(2020, 7, 1),
accurate_as_of=datetime(2020, 6, 1),
created_by=ndb.User(
_auth_domain='example.com', email='user@example.com'),
updated_by=ndb.User(
_auth_domain='example.com', email='editor@example.com'),
feature_type=0,
owner=['owner@example.com'],
editors=['editor@example.com'],
unlisted=False,
deleted=False,
name='feature_two',
summary='summary',
category=1,)
self.feature_2.put()
# Feature 3 stages are already migrated.
self.feature_3 = Feature(
id=3,
created=datetime(2020, 4, 1),
updated=datetime(2020, 7, 1),
accurate_as_of=datetime(2020, 6, 1),
created_by=ndb.User(
_auth_domain='example.com', email='user@example.com'),
updated_by=ndb.User(
_auth_domain='example.com', email='editor@example.com'),
owner=['owner@example.com'],
editors=['editor@example.com'],
unlisted=False,
deleted=False,
name='feature_three',
summary='summary',
category=1)
self.feature_3.put()
self.feature_4 = Feature(
id=4,
created=datetime(2020, 4, 1),
updated=datetime(2020, 7, 1),
accurate_as_of=datetime(2020, 6, 1),
created_by=ndb.User(
_auth_domain='example.com', email='user@example.com'),
updated_by=ndb.User(
_auth_domain='example.com', email='editor@example.com'),
owner=['owner@example.com'],
editors=['editor@example.com'],
feature_type=1,
unlisted=False,
deleted=False,
name='feature_four',
summary='migrated summary',
category=1)
self.feature_4.put()
self.feature_5 = Feature(
id=5,
created=datetime(2020, 4, 1),
updated=datetime(2020, 7, 1),
accurate_as_of=datetime(2020, 6, 1),
created_by=ndb.User(
_auth_domain='example.com', email='user@example.com'),
updated_by=ndb.User(
_auth_domain='example.com', email='editor@example.com'),
owner=['owner@example.com'],
editors=['editor@example.com'],
feature_type=2,
unlisted=False,
deleted=False,
name='feature_five',
summary='summary',
category=1)
self.feature_5.put()
# Create a "random" number of Approval entities.
appr_states = [Approval.NA, Approval.DENIED, Approval.APPROVED]
for i in range(20):
id = i % 5 + 1
gate_type = i % 4 + 1
state = appr_states[i % 3]
set_on = datetime(2020, 1, 1)
set_by = f'voter{i}@example.com'
appr = Approval(feature_id=id, field_id=gate_type, state=state,
set_on=set_on, set_by=set_by)
appr.put()
def tearDown(self):
kinds = [Feature, FeatureEntry, Stage, Gate, Approval, Vote]
for kind in kinds:
for entity in kind.query().fetch():
entity.key.delete()
def run_handler(self, handler):
return handler.get_template_data()
def test_migrations(self):
## Test FeatureEntry migration. ##
handler_message = self.run_handler(
schema_migration.MigrateEntities())
expected = '5 Feature entities migrated to FeatureEntry entities.'
self.assertEqual(handler_message, expected)
feature_entries = FeatureEntry.query().fetch()
self.assertEqual(len(feature_entries), 5)
# Check if all fields have been copied over.
feature_entry_1 = FeatureEntry.get_by_id(
self.feature_1.key.integer_id())
self.assertIsNotNone(feature_entry_1)
# Check that all fields are copied over as expected.
for field in self.FEATURE_FIELDS:
self.assertEqual(
getattr(feature_entry_1, field), getattr(self.feature_1, field))
for old_field, new_field in self.RENAMED_FIELDS:
self.assertEqual(
getattr(feature_entry_1, new_field), getattr(self.feature_1, old_field))
self.assertEqual(feature_entry_1.updater_email, self.feature_1.updated_by.email())
# The migration should be idempotent, so nothing should be migrated twice.
handler_message = self.run_handler(
schema_migration.MigrateEntities())
expected = '0 Feature entities migrated to FeatureEntry entities.'
self.assertEqual(handler_message, expected)
self.assertEqual(handler_message, expected)
stages = Stage.query().fetch()
gates = Gate.query().fetch()
self.assertEqual(len(stages), 28)
self.assertEqual(len(gates), 16)
# Check if certain fields were copied over correctly.
stages = Stage.query(
Stage.feature_id == self.feature_1.key.integer_id()).fetch()
self.assertEqual(len(stages), 6)
# Filter for STAGE_DEP_SHIPPING enum.
shipping_stage_list = [
stage for stage in stages if stage.stage_type == 460]
ot_stage_list = [
stage for stage in stages if stage.stage_type == 450]
# Check that 1 Stage exists and the milestone value is correct.
self.assertEqual(len(shipping_stage_list), 1)
self.assertEqual(shipping_stage_list[0].milestones.desktop_first, 105)
self.assertEqual(shipping_stage_list[0].intent_thread_url,
'https://example.com/intentship')
self.assertEqual(shipping_stage_list[0].finch_url,
'https://example.com/finch')
self.assertEqual(len(ot_stage_list), 1)
self.assertEqual(ot_stage_list[0].milestones.desktop_first, 101)
self.assertEqual(ot_stage_list[0].milestones.desktop_last, 104)
self.assertEqual(ot_stage_list[0].milestones.android_first, 102)
self.assertEqual(ot_stage_list[0].milestones.android_last, 104)
self.assertEqual(ot_stage_list[0].intent_thread_url,
'https://example.com/intentexperiment')
## Test Vote migration. ##
handler_message = self.run_handler(
schema_migration.MigrateApprovalsToVotes())
expected = "16 Approval entities migrated to Vote entities."
self.assertEqual(handler_message, expected)
votes = Vote.query().fetch()
self.assertEqual(len(votes), 16)
# The migration should be idempotent, so nothing should be migrated twice.
handler_message = self.run_handler(
schema_migration.MigrateApprovalsToVotes())
expected = "0 Approval entities migrated to Vote entities."
self.assertEqual(handler_message, expected)
class WriteMissingGatesTest(testing_config.CustomTestCase):
def setUp(self):
self.handler = schema_migration.WriteMissingGates()
self.fe_1 = FeatureEntry(
id=1, name='feature a', summary='sum', impl_status_chrome=3,
owner_emails=['feature_owner@example.com'], category=1,
updated=datetime(2020, 3, 1), feature_type=FEATURE_TYPE_INCUBATE_ID)
self.fe_1.put()
self.stage_1 = Stage(id=1, feature_id=1, stage_type=STAGE_BLINK_INCUBATE)
self.stage_2 = Stage(id=2, feature_id=1, stage_type=STAGE_BLINK_PROTOTYPE)
self.gate_2_api = Gate(id=3, feature_id=1, stage_id=2, state=Gate.PREPARING,
gate_type=GATE_API_PROTOTYPE)
self.stage_3 = Stage(id=3, feature_id=1, stage_type=STAGE_BLINK_ORIGIN_TRIAL)
def tearDown(self):
kinds = [FeatureEntry, Stage, Gate]
for kind in kinds:
for entity in kind.query().fetch():
entity.key.delete()
def test_get_template_data__no_stages(self):
"""This won't happen, but good to know that it does not crash."""
result = self.handler.get_template_data()
expected = '0 missing gates created for stages.'
self.assertEqual(result, expected)
# No gates were created as part of set-up or method call.
gates = Gate.query().fetch()
self.assertTrue(len(gates) == 0)
def test_get_template_data__none_needed(self):
"""Stages have all needed gates already, we create zero gates."""
self.stage_1.put()
self.stage_2.put()
self.gate_2_api.put()
result = self.handler.get_template_data()
expected = '0 missing gates created for stages.'
self.assertEqual(result, expected)
# Existing API gate should still exist.
gates = Gate.query().fetch()
self.assertTrue(len(gates) == 1)
def test_get_template_data__all_needed(self):
"""Stages lack all needed gates, so we create them."""
self.stage_3.put()
result = self.handler.get_template_data()
expected = '2 missing gates created for stages.'
self.assertEqual(result, expected)
gates = Gate.query().fetch()
self.assertTrue(len(gates) == 2)
self.assertTrue(all(g.feature_id == 1 for g in gates))
self.assertTrue(any(
g.gate_type == GATE_DEBUGGABILITY_ORIGIN_TRIAL for g in gates))
self.assertTrue(any(
g.gate_type == GATE_API_ORIGIN_TRIAL for g in gates))
class MigrateLGTMFieldsTest(testing_config.CustomTestCase):
def setUp(self):
self.feature_1 = Feature(
id=1,
created=datetime(2020, 1, 1),
updated=datetime(2020, 7, 1),
accurate_as_of=datetime(2020, 3, 1),
created_by=ndb.User(
_auth_domain='example.com', email='user@example.com'),
updated_by=ndb.User(
_auth_domain='example.com', email='editor@example.com'),
owner=['owner@example.com'],
creator='creator@example.com',
editors=['editor@example.com'],
cc_recipients=['cc_user@example.com'],
unlisted=False,
deleted=False,
name='feature_one',
summary='newly migrated summary',
comments='Some comments.',
category=1,
i2e_lgtms=['lgtm@gmail.com'])
self.feature_1.put()
self.feature_2 = Feature(
id=2,
created=datetime(2020, 4, 1),
updated=datetime(2020, 7, 1),
accurate_as_of=datetime(2020, 6, 1),
created_by=ndb.User(
_auth_domain='example.com', email='user@example.com'),
updated_by=ndb.User(
_auth_domain='example.com', email='editor@example.com'),
feature_type=0,
owner=['owner@example.com'],
editors=['editor@example.com'],
i2s_lgtms=['lgtm1@gmail.com', 'lgtm2@gmail.com'],
unlisted=False,
deleted=False,
name='feature_two',
summary='summary',
category=1,)
self.feature_2.put()
# Feature 3 stages are already migrated.
self.feature_3 = Feature(
id=3,
created=datetime(2020, 4, 1),
updated=datetime(2020, 7, 1),
accurate_as_of=datetime(2020, 6, 1),
created_by=ndb.User(
_auth_domain='example.com', email='user@example.com'),
updated_by=ndb.User(
_auth_domain='example.com', email='editor@example.com'),
owner=['owner@example.com'],
editors=['editor@example.com'],
unlisted=False,
deleted=False,
name='feature_three',
summary='summary',
category=1)
self.feature_3.put()
self.feature_4 = Feature(
id=4,
created=datetime(2020, 4, 1),
updated=datetime(2020, 7, 1),
accurate_as_of=datetime(2020, 6, 1),
created_by=ndb.User(
_auth_domain='example.com', email='user@example.com'),
updated_by=ndb.User(
_auth_domain='example.com', email='editor@example.com'),
owner=['owner@example.com'],
editors=['editor@example.com'],
feature_type=1,
unlisted=False,
deleted=False,
name='feature_four',
summary='migrated summary',
category=1)
self.feature_4.put()
self.gate_1 = Gate(feature_id=1, stage_id=2, gate_type=GATE_API_ORIGIN_TRIAL, state=0)
self.gate_1.put()
self.gate_2 = Gate(feature_id=2, stage_id=2, gate_type=GATE_API_SHIP, state=3)
self.gate_2.put()
def tearDown(self):
kinds = [Feature, FeatureEntry, Stage, Gate, Approval, Vote]
for kind in kinds:
for entity in kind.query().fetch():
entity.key.delete()
def run_handler(self, handler):
return handler.get_template_data()
def test_migrations_two_votes(self):
handler_message = self.run_handler(
schema_migration.MigrateLGTMFields())
expected = '3 of 3 lgtms fields migrated.'
self.assertEqual(handler_message, expected)
votes = Vote.query().fetch()
self.assertEqual(len(votes), 3)
vote_1 = votes[0]
self.assertEqual(vote_1.feature_id, 1)
self.assertEqual(vote_1.gate_type, GATE_API_ORIGIN_TRIAL)
self.assertEqual(vote_1.state, Vote.APPROVED)
self.assertEqual(vote_1.set_by, 'lgtm@gmail.com')
self.assertEqual(self.gate_1.state, Vote.APPROVED)
vote_2 = votes[1]
self.assertEqual(vote_2.feature_id, 2)
self.assertEqual(vote_2.gate_type, GATE_API_SHIP)
self.assertEqual(vote_2.state, Vote.APPROVED)
self.assertEqual(vote_2.set_by, 'lgtm1@gmail.com')
# THREE_LGTM required for GATE_API_SHIP.
self.assertEqual(self.gate_2.state, Gate.PREPARING)
vote_2 = votes[2]
self.assertEqual(vote_2.feature_id, 2)
self.assertEqual(vote_2.gate_type, GATE_API_SHIP)
self.assertEqual(vote_2.state, Vote.APPROVED)
self.assertEqual(vote_2.set_by, 'lgtm2@gmail.com')
# THREE_LGTM required for GATE_API_SHIP.
self.assertEqual(self.gate_2.state, Gate.PREPARING)
# Nothing should be migrated twice.
handler_message = self.run_handler(
schema_migration.MigrateLGTMFields())
expected = '0 of 0 lgtms fields migrated.'
self.assertEqual(handler_message, expected)

18
main.py
Просмотреть файл

@ -241,28 +241,10 @@ internals_routes: list[Route] = [
Route('/tasks/detect-intent', detect_intent.IntentEmailHandler),
Route('/tasks/email-reviewers', notifier.FeatureReviewHandler),
Route('/admin/schema_migration_delete_entities',
schema_migration.DeleteNewEntities),
Route('/admin/schema_migration_comment_activity',
schema_migration.MigrateCommentsToActivities),
Route('/admin/schema_migration_write_entities',
schema_migration.MigrateEntities),
Route('/admin/schema_migration_approval_vote',
schema_migration.MigrateApprovalsToVotes),
Route('/admin/schema_migration_gate_status',
schema_migration.EvaluateGateStatus),
Route('/admin/schema_migration_updated_field',
schema_migration.WriteUpdatedField),
Route('/admin/schema_migration_update_views',
schema_migration.UpdateDeprecatedViews),
Route('/admin/schema_migration_missing_gates',
schema_migration.WriteMissingGates),
Route('/admin/schema_migration_active_stage',
schema_migration.CalcActiveStages),
Route('/admin/schema_migration_extension_stages',
schema_migration.CreateTrialExtensionStages),
Route('/admin/schema_migration_subject_line',
schema_migration.MigrateSubjectLineField),
Route('/admin/schema_migration_lgtm_fields',
schema_migration.MigrateLGTMFields),
]