Commands to alter Android compatibilty to prepare for general availability (#21190)

* Commands to alter Android compatibilty to prepare for general availability
This commit is contained in:
Mathieu Pillard 2023-09-15 12:39:27 +02:00 коммит произвёл GitHub
Родитель eb127a420f
Коммит edb43b3743
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
4 изменённых файлов: 548 добавлений и 0 удалений

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

@ -359,6 +359,9 @@ DEFAULT_WEBEXT_MIN_VERSION_GECKO_ANDROID = '113.0'
# First version we consider as "Fenix".
MIN_VERSION_FENIX = '79.0a1'
# Last version we consider as "Fennec"
MAX_VERSION_FENNEC = '68.*'
# The minimum version of Fenix where extensions are all available. Expect this
# to be bumped to 120.0 later.
MIN_VERSION_FENIX_GENERAL_AVAILABILITY = '119.0a1'

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

@ -0,0 +1,88 @@
from django.core.management.base import BaseCommand
from django.db.models import Q
from django.db.transaction import atomic
import olympia.core.logger
from olympia import amo
from olympia.applications.models import AppVersion
from olympia.constants.promoted import PROMOTED_GROUPS
from olympia.files.models import File
from olympia.versions.models import ApplicationsVersions
log = olympia.core.logger.getLogger('z.versions.force_max_android_compatibility')
class Command(BaseCommand):
"""
Force *all* versions of add-ons compatible with Firefox for Android with a
minimum version lower than <MIN_VERSION_FENIX_GENERAL_AVAILABILITY> and not
recommended or line for Android to have a max version of 68.*, or to have
their compatibility with Firefox for Android dropped it their min was
higher than 68.* already.
"""
help = (
'Force add-ons not already compatible with '
f'{amo.MIN_VERSION_FENIX_GENERAL_AVAILABILITY} and higher to be '
'compatible with Firefox for Android 68.* or lower'
)
def handle(self, *args, **kwargs):
min_version_fenix = AppVersion.objects.get(
application=amo.ANDROID.id,
version=amo.MIN_VERSION_FENIX_GENERAL_AVAILABILITY,
)
max_version_fennec = AppVersion.objects.get(
application=amo.ANDROID.id, version=amo.MAX_VERSION_FENNEC
)
promoted_groups_ids = [
p.id for p in PROMOTED_GROUPS if p.can_be_compatible_with_fenix
]
qs = (
# We only care about listed extensions already marked as compatible
# for Android.
ApplicationsVersions.objects.filter(application=amo.ANDROID.id)
.filter(version__addon__type=amo.ADDON_EXTENSION)
.filter(version__channel=amo.CHANNEL_LISTED)
.filter(
# They need to be either:
Q(version__addon__promotedaddon__isnull=True) # Not promoted at all
| ~Q(
version__addon__promotedaddon__group_id__in=promoted_groups_ids
) # Promoted, but not for line / recommended
| Q(
version__addon__promotedaddon__application_id=amo.FIREFOX.id
) # Promoted, but for Firefox only (not Android / not both)
)
# If they are already marked as compatible with 119.0a1, we don't
# care.
.filter(min__version_int__lt=min_version_fenix.version_int)
)
# If the min is also over 68.* then it means the developer marked it
# as compatible with Fenix only but that's unlikely to be correct, so
# we drop that compatibility information completely.
qs_to_drop = qs.filter(min__version_int__gt=max_version_fennec.version_int)
# Otherwise we'll update it, setting the max to 68.*.
qs_to_update = qs.exclude(min__version_int__gte=max_version_fennec.version_int)
with atomic():
count_versions_compat_updated = qs_to_update.update(
max=max_version_fennec,
originated_from=amo.APPVERSIONS_ORIGINATED_FROM_MIGRATION,
)
with atomic():
count_versions_compat_dropped, _ = qs_to_drop.delete()
with atomic():
count_files = File.objects.filter(
version__apps__application=amo.ANDROID.id,
version__apps__max__version=max_version_fennec,
).update(strict_compatibility=True)
log.info(
'Done forcing max Android compatibility: '
'Dropped compat on %d versions, '
'Updated compat for %d versions, '
'Set strict compatibility on %d files',
count_versions_compat_dropped,
count_versions_compat_updated,
count_files,
)

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

@ -0,0 +1,90 @@
import csv
from django.core.management.base import BaseCommand
from django.db.transaction import atomic
import olympia.core.logger
from olympia import amo
from olympia.addons.models import Addon
from olympia.applications.models import AppVersion
from olympia.versions.models import ApplicationsVersions
log = olympia.core.logger.getLogger('z.versions.force_min_android_compatibility')
class Command(BaseCommand):
"""
Force current version of add-ons in the specified csv to be compatible with
Firefox for Android <MIN_VERSION_FENIX_GENERAL_AVAILABILITY> and higher.
Should not affect compatibility of add-ons recommended/line for Android.
"""
help = (
'Force add-ons to be compatible with Firefox for Android '
f'{amo.MIN_VERSION_FENIX_GENERAL_AVAILABILITY} and higher'
)
def add_arguments(self, parser):
parser.add_argument(
'CSVFILE',
help='Path to CSV file containing add-on ids.',
)
def read_csv(self, path):
with open(path) as file_:
csv_reader = csv.reader(file_)
# Format should be a single column with the add-on id.
# Ignore non-decimal to avoid the column header.
return [
int(row[0])
for row in csv_reader
if row[0] and row[0].strip().isdecimal()
]
def handle(self, *args, **kwargs):
addon_ids = self.read_csv(kwargs['CSVFILE'])
min_version_fenix = AppVersion.objects.get(
application=amo.ANDROID.id,
version=amo.MIN_VERSION_FENIX_GENERAL_AVAILABILITY,
)
max_version_fenix = AppVersion.objects.get(
application=amo.ANDROID.id, version=amo.DEFAULT_WEBEXT_MAX_VERSION
)
addons = (
Addon.objects.filter(pk__in=addon_ids)
.no_transforms()
.select_related(
'promotedaddon', '_current_version', '_current_version__file'
)
)
count = 0
skipped = 0
for addon in addons:
if (
addon.promoted
and addon.promoted.group.can_be_compatible_with_fenix
and amo.ANDROID in addon.promoted.approved_applications
):
log.info(
'Skipping add-on id %d because of its promoted group.', addon.pk
)
skipped += 1
continue
with atomic():
ApplicationsVersions.objects.update_or_create(
version=addon.current_version,
application=amo.ANDROID.id,
defaults={
'min': min_version_fenix,
'max': max_version_fenix,
'originated_from': amo.APPVERSIONS_ORIGINATED_FROM_MIGRATION,
},
)
count += 1
log.info(
'Done forcing Android compatibility on %d add-ons (%d skipped)',
count,
skipped,
)

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

@ -0,0 +1,367 @@
import csv
import os
import tempfile
from django.core.management import call_command
from django.core.management.base import CommandError
from olympia import amo
from olympia.amo.tests import TestCase, addon_factory
from olympia.applications.models import AppVersion
from olympia.constants.promoted import LINE, RECOMMENDED, SPOTLIGHT
from olympia.promoted.models import PromotedAddon
from olympia.versions.management.commands.force_min_android_compatibility import (
Command as ForceMinAndroidCompatibility,
)
from olympia.versions.models import ApplicationsVersions
class TestForceMinAndroidCompatibility(TestCase):
def setUp(self):
self.min_version_fenix = AppVersion.objects.get_or_create(
application=amo.ANDROID.id,
version=amo.MIN_VERSION_FENIX_GENERAL_AVAILABILITY,
)[0]
self.max_version_fenix = AppVersion.objects.get_or_create(
application=amo.ANDROID.id, version=amo.DEFAULT_WEBEXT_MAX_VERSION
)[0]
def _create_csv(self, contents):
with tempfile.NamedTemporaryFile(
mode='w', suffix='.csv', delete=False
) as csv_file:
self.addCleanup(os.remove, csv_file.name)
writer = csv.writer(csv_file)
writer.writerows(contents)
return csv_file.name
def test_missing_csv_path(self):
with self.assertRaises(CommandError):
call_command('force_min_android_compatibility')
def test_init_csv_parsing(self):
file_working_name = self._create_csv(
[['addon_id'], ['123456789'], ['4815162342'], ['007'], ['42'], [' 57 ']]
)
command = ForceMinAndroidCompatibility()
assert command.read_csv(file_working_name) == [123456789, 4815162342, 7, 42, 57]
def test_full(self):
addons_to_modify = [
addon_factory(name='Not yet compatible with Android'),
addon_factory(
name='Already compatible with Android with strict_compatibility',
version_kw={
'application': amo.ANDROID.id,
'min_app_version': '48.0',
'max_app_version': '68.*',
},
file_kw={'strict_compatibility': True},
),
addon_factory(
name='Already compatible with Android',
version_kw={
'application': amo.ANDROID.id,
'min_app_version': '120.0',
'max_app_version': '*',
},
),
]
addons_to_ignore_promoted = [
addon_factory(
name='Recommended for Android',
version_kw={
'application': amo.ANDROID.id,
'min_app_version': '48.0',
'max_app_version': '*',
},
promoted=RECOMMENDED,
),
addon_factory(
name='Line for all',
version_kw={
'application': amo.ANDROID.id,
'min_app_version': '48.0',
'max_app_version': '*',
},
promoted=LINE,
),
]
addons_to_ignore_not_in_csv = [addon_factory(name='Not in csv')]
csv_path = self._create_csv(
[
['addon_id'],
*[[str(addon.pk)] for addon in addons_to_modify],
*[[str(addon.pk) for addon in addons_to_ignore_promoted]],
]
)
call_command('force_min_android_compatibility', csv_path)
for addon in addons_to_modify:
if hasattr(addon.current_version, '_compatible_apps'):
del addon.current_version._compatible_apps
assert amo.ANDROID in addon.current_version.compatible_apps
assert (
addon.current_version.compatible_apps[amo.ANDROID].min.version
== '119.0a1'
)
assert addon.current_version.compatible_apps[amo.ANDROID].max.version == '*'
assert (
addon.current_version.compatible_apps[amo.ANDROID].originated_from
== amo.APPVERSIONS_ORIGINATED_FROM_MIGRATION
)
assert not addon.current_version.file.reload().strict_compatibility
for addon in addons_to_ignore_promoted:
if hasattr(addon.current_version, '_compatible_apps'):
del addon.current_version._compatible_apps
assert amo.ANDROID in addon.current_version.compatible_apps
assert (
addon.current_version.compatible_apps[amo.ANDROID].min.version == '48.0'
)
assert addon.current_version.compatible_apps[amo.ANDROID].max.version == '*'
assert (
addon.current_version.compatible_apps[amo.ANDROID].originated_from
== amo.APPVERSIONS_ORIGINATED_FROM_UNKNOWN
)
assert not addon.current_version.file.reload().strict_compatibility
for addon in addons_to_ignore_not_in_csv:
if hasattr(addon.current_version, '_compatible_apps'):
del addon.current_version._compatible_apps
assert amo.ANDROID not in addon.current_version.compatible_apps
class TestForceMaxAndroidCompatibility(TestCase):
def setUp(self):
self.min_version_fenix = AppVersion.objects.get_or_create(
application=amo.ANDROID.id,
version=amo.MIN_VERSION_FENIX_GENERAL_AVAILABILITY,
)[0]
self.max_version_fenix = AppVersion.objects.get_or_create(
application=amo.ANDROID.id, version=amo.MAX_VERSION_FENNEC
)[0]
self.some_fenix_version = AppVersion.objects.get_or_create(
application=amo.ANDROID.id, version='99.0'
)[0]
def test_full(self):
addons_to_ignore_promoted = [
addon_factory(
# Actually recommended for both apps to start with, modified
# below to be only for Android.
name='Recommended for Android',
version_kw={
'application': amo.ANDROID.id,
'min_app_version': '48.0',
'max_app_version': '*',
},
promoted=RECOMMENDED,
),
addon_factory(
name='Recommended for All',
version_kw={
'application': amo.ANDROID.id,
'min_app_version': '48.0',
'max_app_version': '*',
},
promoted=RECOMMENDED,
),
addon_factory(
name='Line for all',
version_kw={
'application': amo.ANDROID.id,
'min_app_version': '48.0',
'max_app_version': '*',
},
promoted=LINE,
),
]
addons_to_ignore_119 = [
addon_factory(
name='Recommended for Android 119',
version_kw={
'application': amo.ANDROID.id,
'min_app_version': '119.0a1',
'max_app_version': '*',
},
promoted=RECOMMENDED,
),
addon_factory(
name='Normal for Android 119',
version_kw={
'application': amo.ANDROID.id,
'min_app_version': '119.0a1',
'max_app_version': '*',
},
promoted=RECOMMENDED,
),
]
addons_to_ignore_not_even_listed_extension = [
addon_factory(
name='Theme for Android (!)',
type=amo.ADDON_STATICTHEME,
version_kw={
'application': amo.ANDROID.id,
'min_app_version': '48.0',
'max_app_version': '*',
},
),
addon_factory(
name='Unlisted Extension for Android',
version_kw={
'channel': amo.CHANNEL_UNLISTED,
# Can't be set like that, fixed below.
'application': amo.ANDROID.id,
'min_app_version': '48.0',
'max_app_version': '*',
},
),
]
addons_to_ignore_not_even_compatible_with_android = [
addon_factory(
name='Extension for Firefox',
version_kw={
'min_app_version': '48.0',
'max_app_version': '*',
},
),
]
addons = [
addon_factory(
name='Normal extension for Android 48',
version_kw={
# Can't be set like that, fixed below.
'application': amo.ANDROID.id,
'min_app_version': '48.0',
'max_app_version': '*',
},
),
addon_factory(
# Actually recommended for both apps to start with, modified
# below to be only for Desktop.
name='Recommended extension for Desktop compatible with Android 48',
version_kw={
'application': amo.ANDROID.id,
'min_app_version': '48.0',
'max_app_version': '*',
},
promoted=RECOMMENDED,
),
addon_factory(
name='Spotlight extension compatible with Android 48',
version_kw={
'application': amo.ANDROID.id,
'min_app_version': '48.0',
'max_app_version': '*',
},
promoted=SPOTLIGHT,
),
]
addons_to_drop = [
addon_factory(
name='Normal extension for Android 99.0',
version_kw={
# Can't be set like that, fixed below.
'application': amo.ANDROID.id,
'min_app_version': '48.0',
'max_app_version': '*',
},
),
]
# Manually update the promoted add-ons we want to only recommend for a
# single app..
PromotedAddon.objects.get(addon=addons_to_ignore_promoted[0]).update(
application_id=amo.ANDROID.id
)
PromotedAddon.objects.get(addon=addons[1]).update(application_id=amo.FIREFOX.id)
# Directly creating an add-on compatible with Firefox for Android 99.0
# is no longer possible without being recommended, so manually update
# some ApplicationsVersions that we couldn't set.
ApplicationsVersions.objects.filter(
pk=addons_to_drop[0].current_version.compatible_apps[amo.ANDROID].pk
).update(min=self.some_fenix_version)
ApplicationsVersions.objects.filter(
pk=addons_to_ignore_not_even_listed_extension[1]
.versions.get()
.compatible_apps[amo.ANDROID]
.pk
).update(min=AppVersion.objects.get(application=amo.ANDROID.id, version='48.0'))
ApplicationsVersions.objects.filter(version__addon__in=addons).update(
min=AppVersion.objects.get(application=amo.ANDROID.id, version='48.0')
)
call_command('force_max_android_compatibility')
for addon in addons_to_ignore_promoted:
if hasattr(addon.current_version, '_compatible_apps'):
del addon.current_version._compatible_apps
assert amo.ANDROID in addon.current_version.compatible_apps
assert (
addon.current_version.compatible_apps[amo.ANDROID].min.version == '48.0'
)
assert addon.current_version.compatible_apps[amo.ANDROID].max.version == '*'
assert (
addon.current_version.compatible_apps[amo.ANDROID].originated_from
== amo.APPVERSIONS_ORIGINATED_FROM_UNKNOWN
)
assert not addon.current_version.file.reload().strict_compatibility
for addon in addons_to_ignore_119:
if hasattr(addon.current_version, '_compatible_apps'):
del addon.current_version._compatible_apps
assert amo.ANDROID in addon.current_version.compatible_apps
assert (
addon.current_version.compatible_apps[amo.ANDROID].min.version
== '119.0a1'
)
assert addon.current_version.compatible_apps[amo.ANDROID].max.version == '*'
assert (
addon.current_version.compatible_apps[amo.ANDROID].originated_from
== amo.APPVERSIONS_ORIGINATED_FROM_UNKNOWN
)
assert not addon.current_version.file.reload().strict_compatibility
for addon in addons_to_ignore_not_even_listed_extension:
version = addon.versions.get()
assert amo.ANDROID in version.compatible_apps
assert version.compatible_apps[amo.ANDROID].min.version == '48.0'
assert version.compatible_apps[amo.ANDROID].max.version == '*'
assert (
version.compatible_apps[amo.ANDROID].originated_from
== amo.APPVERSIONS_ORIGINATED_FROM_UNKNOWN
)
assert not version.file.reload().strict_compatibility
for addon in addons_to_ignore_not_even_compatible_with_android:
if hasattr(addon.current_version, '_compatible_apps'):
del addon.current_version._compatible_apps
assert amo.ANDROID not in addon.current_version.compatible_apps
assert (
addon.current_version.compatible_apps[amo.FIREFOX].originated_from
== amo.APPVERSIONS_ORIGINATED_FROM_UNKNOWN
)
assert not addon.current_version.file.reload().strict_compatibility
for addon in addons:
if hasattr(addon.current_version, '_compatible_apps'):
del addon.current_version._compatible_apps
assert amo.ANDROID in addon.current_version.compatible_apps
assert (
addon.current_version.compatible_apps[amo.ANDROID].min.version == '48.0'
)
assert (
addon.current_version.compatible_apps[amo.ANDROID].max.version == '68.*'
)
assert (
addon.current_version.compatible_apps[amo.ANDROID].originated_from
== amo.APPVERSIONS_ORIGINATED_FROM_MIGRATION
)
assert addon.current_version.file.reload().strict_compatibility
for addon in addons_to_drop:
if hasattr(addon.current_version, '_compatible_apps'):
del addon.current_version._compatible_apps
assert amo.ANDROID not in addon.current_version.compatible_apps
assert not addon.current_version.file.reload().strict_compatibility