Remove old migration scripts (#2843)
* remove old schema migration scripts * delete test file
This commit is contained in:
Родитель
772746640c
Коммит
7405abb2e6
|
@ -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
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),
|
||||
]
|
||||
|
|
Загрузка…
Ссылка в новой задаче