From 1084c3e720a2d3265f22b7d760f6cb37a59e129e Mon Sep 17 00:00:00 2001 From: Zeid Zabaneh Date: Mon, 11 Sep 2023 16:13:45 -0400 Subject: [PATCH] fetch_configs, launchers: add Fenix nightlies support (bug 1556042) - add "fenix" as an app option in the cli - update available arch options to include newer ones - Add FenixNightlyConfigMixin and FenixConfig classes - create new helper method `AndroidBrowser.launch_browser` - move `adb.launch_fennec` calls to `adb.launch_application` - create FenixLauncher class - parametrize existing Fennec tests and update to support Fenix - update cli help text --- mozregression/cli.py | 35 +++++++- mozregression/fetch_configs.py | 45 +++++++++++ mozregression/launchers.py | 37 ++++++++- tests/unit/test_fetch_configs.py | 60 ++++++++------ tests/unit/test_launchers.py | 135 +++++++++++++++++++------------ 5 files changed, 232 insertions(+), 80 deletions(-) diff --git a/mozregression/cli.py b/mozregression/cli.py index 8392712df..7c31120cc 100644 --- a/mozregression/cli.py +++ b/mozregression/cli.py @@ -276,9 +276,16 @@ def create_parser(defaults): parser.add_argument( "--arch", - choices=("arm", "x86_64", "aarch64"), + choices=( + "aarch64", + "arm", + "arm64-v8a", + "armeabi-v7a", + "x86", + "x86_64", + ), default=None, - help=("Force alternate build (only applies to GVE app). Default: arm"), + help=("Force alternate build (applies to GVE and Fenix)."), ) parser.add_argument( @@ -554,12 +561,32 @@ def validate(self): """ options = self.options + arch_options = { + "gve": [ + "aarch64", + "arm", + "x86_64", + ], + "fenix": [ + "arm64-v8a", + "armeabi-v7a", + "x86", + "x86_64", + ], + } + user_defined_bits = options.bits is not None options.bits = parse_bits(options.bits or mozinfo.bits) if options.arch is not None: - if options.app != "gve": - self.logger.warning("--arch ignored for non-GVE app.") + if options.app not in ("gve", "fenix"): + self.logger.warning("--arch ignored for non Android apps.") options.arch = None + else: + if options.arch not in arch_options[options.app]: + raise MozRegressionError( + f"Invalid arch ({options.arch}) specified for app ({options.app}). " + f"Valid options are: {', '.join(arch_options[options.app])}." + ) fetch_config = create_config( options.app, mozinfo.os, options.bits, mozinfo.processor, options.arch diff --git a/mozregression/fetch_configs.py b/mozregression/fetch_configs.py index fa91b2042..52df69163 100644 --- a/mozregression/fetch_configs.py +++ b/mozregression/fetch_configs.py @@ -382,6 +382,19 @@ def get_nightly_repo_regex(self, date): return self._get_nightly_repo_regex(date, repo) +class FenixNightlyConfigMixin(NightlyConfigMixin): + nightly_base_repo_name = "fenix" + arch_regex_bits = "" + + def _get_nightly_repo(self, date): + return "fenix" + + def get_nightly_repo_regex(self, date): + repo = self.get_nightly_repo(date) + repo += self.arch_regex_bits # e.g., ".*arm64.*". + return self._get_nightly_repo_regex(date, repo) + + class IntegrationConfigMixin(metaclass=ABCMeta): """ Define the integration-related required configuration. @@ -607,6 +620,38 @@ def available_bits(self): return () +@REGISTRY.register("fenix") +class FenixConfig(CommonConfig, FenixNightlyConfigMixin): + BUILD_TYPES = ("shippable", "opt") + + def build_regex(self): + return r"fenix-.*\.apk" + + def available_bits(self): + return () + + def available_archs(self): + return [ + "arm64-v8a", + "armeabi-v7a", + "x86", + "x86_64", + ] + + def set_arch(self, arch): + CommonConfig.set_arch(self, arch) + mapping = { + "arm64-v8a": "-.+-android-arm64-v8a", + "armeabi-v7a": "-.+-android-armeabi-v7a", + "x86": "-.+-android-x86", + "x86_64": "-.+-android-x86_64", + } + self.arch_regex_bits = mapping.get(self.arch, "") + + def should_use_archive(self): + return True + + @REGISTRY.register("gve") class GeckoViewExampleConfig(CommonConfig, FennecNightlyConfigMixin, FennecIntegrationConfigMixin): BUILD_TYPES = ("shippable", "opt", "debug") diff --git a/mozregression/launchers.py b/mozregression/launchers.py index 735ade008..646506706 100644 --- a/mozregression/launchers.py +++ b/mozregression/launchers.py @@ -493,6 +493,31 @@ def _stop(self): if self.adb.exists(self.remote_profile): self.adb.rm(self.remote_profile, recursive=True) + def launch_browser( + self, + app_name, + activity, + intent="android.intent.action.VIEW", + moz_env=None, + url=None, + wait=True, + fail_if_running=True, + timeout=None, + ): + extras = {} + extras["args"] = f"-profile {self.remote_profile}" + + self.adb.launch_application( + app_name, + activity, + intent, + url=url, + extras=extras, + wait=wait, + fail_if_running=fail_if_running, + timeout=timeout, + ) + def get_app_info(self): return self.app_info @@ -504,7 +529,17 @@ def _get_package_name(self): def _launch(self): LOG.debug("Launching fennec") - self.adb.launch_fennec(self.package_name, extra_args=["-profile", self.remote_profile]) + self.launch_browser(self.package_name, "org.mozilla.gecko.BrowserApp") + + +@REGISTRY.register("fenix") +class FenixLauncher(AndroidLauncher): + def _get_package_name(self): + return "org.mozilla.fenix" + + def _launch(self): + LOG.debug("Launching fenix") + self.launch_browser(self.package_name, ".IntentReceiverActivity") @REGISTRY.register("gve") diff --git a/tests/unit/test_fetch_configs.py b/tests/unit/test_fetch_configs.py index 836cbfa87..3eb27c157 100644 --- a/tests/unit/test_fetch_configs.py +++ b/tests/unit/test_fetch_configs.py @@ -190,31 +190,41 @@ def test_nightly_repo_regex_before_2009_01_09(self): TestThunderbirdConfig.test_nightly_repo_regex_before_2009_01_09(self) -class TestFennecConfig(unittest.TestCase): - def setUp(self): - self.conf = create_config("fennec", "linux", 64, None) - - def test_get_nightly_repo_regex(self): - regex = self.conf.get_nightly_repo_regex(datetime.date(2014, 12, 5)) - self.assertIn("mozilla-central-android", regex) - regex = self.conf.get_nightly_repo_regex(datetime.date(2014, 12, 10)) - self.assertIn("mozilla-central-android-api-10", regex) - regex = self.conf.get_nightly_repo_regex(datetime.date(2015, 1, 1)) - self.assertIn("mozilla-central-android-api-11", regex) - regex = self.conf.get_nightly_repo_regex(datetime.date(2016, 1, 28)) - self.assertIn("mozilla-central-android-api-11", regex) - regex = self.conf.get_nightly_repo_regex(datetime.date(2016, 1, 29)) - self.assertIn("mozilla-central-android-api-15", regex) - regex = self.conf.get_nightly_repo_regex(datetime.date(2017, 8, 30)) - self.assertIn("mozilla-central-android-api-16", regex) - - def test_build_regex(self): - regex = re.compile(self.conf.build_regex()) - self.assertTrue(regex.match("fennec-36.0a1.multi.android-arm.apk")) - - def test_build_info_regex(self): - regex = re.compile(self.conf.build_info_regex()) - self.assertTrue(regex.match("fennec-36.0a1.multi.android-arm.txt")) +@pytest.mark.parametrize("app_name", ["fennec", "fenix"]) +class TestExtendedAndroidConfig: + def test_get_nightly_repo_regex(self, app_name): + if app_name == "fennec": + conf = create_config("fennec", "linux", 64, None) + regex = conf.get_nightly_repo_regex(datetime.date(2014, 12, 5)) + assert "mozilla-central-android" in regex + regex = conf.get_nightly_repo_regex(datetime.date(2014, 12, 10)) + assert "mozilla-central-android-api-10" in regex + regex = conf.get_nightly_repo_regex(datetime.date(2015, 1, 1)) + assert "mozilla-central-android-api-11" in regex + regex = conf.get_nightly_repo_regex(datetime.date(2016, 1, 28)) + assert "mozilla-central-android-api-11" in regex + regex = conf.get_nightly_repo_regex(datetime.date(2016, 1, 29)) + assert "mozilla-central-android-api-15" in regex + regex = conf.get_nightly_repo_regex(datetime.date(2017, 8, 30)) + assert "mozilla-central-android-api-16" in regex + else: + conf = create_config(app_name, "linux", 64, None) + date = datetime.date(2023, 1, 1) + regex = conf.get_nightly_repo_regex(date) + assert regex == f"/{date.isoformat()}-[\\d-]+{app_name}/$" + + def test_build_regex(self, app_name): + conf = create_config(app_name, "linux", 64, None) + regex = re.compile(conf.build_regex()) + assert bool(regex.match(f"{app_name}-110.0b1.multi.android-arm64-v8a.apk")) is True + + def test_build_info_regex(self, app_name): + if app_name != "fennec": + # This test is currently only applicable to Fennec. + return + conf = create_config(app_name, "linux", 64, None) + regex = re.compile(conf.build_info_regex()) + assert bool(regex.match(f"{app_name}-36.0a1.multi.android-arm.txt")) is True class TestGVEConfig(unittest.TestCase): diff --git a/tests/unit/test_launchers.py b/tests/unit/test_launchers.py index 42f9fc2f5..6531d6842 100644 --- a/tests/unit/test_launchers.py +++ b/tests/unit/test_launchers.py @@ -339,87 +339,122 @@ def test_firefox_install( assert _mock_codesign_sign.call_count == sign_call_count -class TestFennecLauncher(unittest.TestCase): +@pytest.mark.parametrize( + "launcher_class,package_name,intended_activity", + [ + (launchers.FennecLauncher, "org.mozilla.fennec", "org.mozilla.gecko.BrowserApp"), + (launchers.FenixLauncher, "org.mozilla.fenix", ".IntentReceiverActivity"), + ], +) +class TestExtendedAndroidLauncher: test_root = "/sdcard/tmp" - def setUp(self): + def setup_method(self): self.profile = Profile() - self.addCleanup(self.profile.cleanup) self.remote_profile_path = self.test_root + "/" + os.path.basename(self.profile.profile) + def teardown_method(self): + self.profile.cleanup() + @patch("mozregression.launchers.mozversion.get_version") @patch("mozregression.launchers.ADBDeviceFactory") - def create_launcher(self, ADBDeviceFactory, get_version, **kwargs): + def create_launcher(self, ADBDeviceFactory, get_version, launcher_class=None, **kwargs): self.adb = Mock(test_root=self.test_root) if kwargs.get("uninstall_error"): self.adb.uninstall_app.side_effect = launchers.ADBError ADBDeviceFactory.return_value = self.adb get_version.return_value = kwargs.get("version_value", {}) - return launchers.FennecLauncher("/binary") + return launcher_class("/binary") - def test_install(self): - self.create_launcher() - self.adb.uninstall_app.assert_called_with("org.mozilla.fennec") + def test_install(self, launcher_class, package_name, intended_activity): + self.create_launcher(launcher_class=launcher_class) + self.adb.uninstall_app.assert_called_with(package_name) self.adb.install_app.assert_called_with("/binary") - @patch("mozregression.launchers.FennecLauncher._create_profile") - def test_start_stop(self, _create_profile): - # Force use of existing profile - _create_profile.return_value = self.profile - launcher = self.create_launcher() - launcher.start(profile="my_profile") - self.adb.exists.assert_called_once_with(self.remote_profile_path) - self.adb.rm.assert_called_once_with(self.remote_profile_path, recursive=True) - self.adb.push.assert_called_once_with(self.profile.profile, self.remote_profile_path) - self.adb.launch_fennec.assert_called_once_with( - "org.mozilla.fennec", extra_args=["-profile", self.remote_profile_path] - ) - # ensure get_app_info returns something - self.assertIsNotNone(launcher.get_app_info()) - launcher.stop() - self.adb.stop_application.assert_called_once_with("org.mozilla.fennec") - - @patch("mozregression.launchers.FennecLauncher._create_profile") - def test_adb_calls_with_custom_package_name(self, _create_profile): - # Force use of existing profile - _create_profile.return_value = self.profile - pkg_name = "org.mozilla.custom" - launcher = self.create_launcher(version_value={"package_name": pkg_name}) - self.adb.uninstall_app.assert_called_once_with(pkg_name) - launcher.start(profile="my_profile") - self.adb.launch_fennec.assert_called_once_with( - pkg_name, extra_args=["-profile", self.remote_profile_path] - ) - launcher.stop() - self.adb.stop_application.assert_called_once_with(pkg_name) + def test_start_stop(self, launcher_class, package_name, intended_activity, **kwargs): + with patch( + f"mozregression.launchers.{launcher_class.__name__}._create_profile" + ) as _create_profile: + # Force use of existing profile + _create_profile.return_value = self.profile + launcher = self.create_launcher(launcher_class=launcher_class) + launcher.start(profile="my_profile") + self.adb.exists.assert_called_once_with(self.remote_profile_path) + self.adb.rm.assert_called_once_with(self.remote_profile_path, recursive=True) + self.adb.push.assert_called_once_with(self.profile.profile, self.remote_profile_path) + self.adb.launch_application.assert_called_once_with( + package_name, + intended_activity, + "android.intent.action.VIEW", + url=None, + extras={"args": f"-profile {self.remote_profile_path}"}, + wait=True, + fail_if_running=True, + timeout=None, + ) + # ensure get_app_info returns something + assert launcher.get_app_info() is not None + launcher.stop() + self.adb.stop_application.assert_called_once_with(package_name) + + def test_adb_calls_with_custom_package_name( + self, launcher_class, package_name, intended_activity + ): + with patch( + f"mozregression.launchers.{launcher_class.__name__}._create_profile" + ) as _create_profile: + # Force use of existing profile + _create_profile.return_value = self.profile + pkg_name = "org.mozilla.custom" + launcher = self.create_launcher( + version_value={"package_name": pkg_name}, launcher_class=launcher_class + ) + self.adb.uninstall_app.assert_called_once_with(pkg_name) + launcher.start(profile="my_profile") + self.adb.launch_application.assert_called_once_with( + pkg_name, + intended_activity, + "android.intent.action.VIEW", + url=None, + extras={"args": f"-profile {self.remote_profile_path}"}, + wait=True, + fail_if_running=True, + timeout=None, + ) + launcher.stop() + self.adb.stop_application.assert_called_once_with(pkg_name) @patch("mozregression.launchers.LOG") - def test_adb_first_uninstall_fail(self, log): - self.create_launcher(uninstall_error=True) + def test_adb_first_uninstall_fail(self, log, launcher_class, package_name, intended_activity): + self.create_launcher(uninstall_error=True, launcher_class=launcher_class) log.warning.assert_called_once_with(ANY) self.adb.install_app.assert_called_once_with(ANY) @patch("mozregression.launchers.ADBHost") - def test_check_is_runnable(self, ADBHost): + def test_check_is_runnable(self, ADBHost, launcher_class, package_name, intended_activity): devices = Mock(return_value=True) ADBHost.return_value = Mock(devices=devices) # this won't raise errors - launchers.FennecLauncher.check_is_runnable() + launcher_class.check_is_runnable() # exception raised if there is no device devices.return_value = False - self.assertRaises(LauncherNotRunnable, launchers.FennecLauncher.check_is_runnable) + with pytest.raises(LauncherNotRunnable): + launcher_class.check_is_runnable() # or if ADBHost().devices() raise an unexpected IOError devices.side_effect = ADBError() - self.assertRaises(LauncherNotRunnable, launchers.FennecLauncher.check_is_runnable) + with pytest.raises(LauncherNotRunnable): + launcher_class.check_is_runnable() @patch("time.sleep") - @patch("mozregression.launchers.FennecLauncher._create_profile") - def test_wait(self, _create_profile, sleep): - # Force use of existing profile - _create_profile.return_value = self.profile - launcher = self.create_launcher() + def test_wait(self, sleep, launcher_class, package_name, intended_activity): + with patch( + f"mozregression.launchers.{launcher_class.__name__}._create_profile" + ) as _create_profile: + # Force use of existing profile + _create_profile.return_value = self.profile + launcher = self.create_launcher(launcher_class=launcher_class) passed = [] @@ -432,7 +467,7 @@ def proc_exists(name): self.adb.process_exist = Mock(side_effect=proc_exists) launcher.start() launcher.wait() - self.adb.process_exist.assert_called_with("org.mozilla.fennec") + self.adb.process_exist.assert_called_with(package_name) class Zipfile(object):