From c4d66a4a4f60491848032cc58a154c575999a5d5 Mon Sep 17 00:00:00 2001 From: JP Cottin Date: Sun, 4 Oct 2026 09:33:57 -0700 Subject: [PATCH 1/2] Make an emulator channel include the more stable channels `emu-docker create canary ` failed with "No emulator found in channel canary": find_emulator only matched packages tagged with exactly that channel, and the pre-release emulator is published on channel-2 ("dev" in the repository XML), with nothing on channel-3. sdkmanager treats a channel as including the more stable ones below it, and lists that same emulator as canary. Do the same: a channel name now gives the newest emulator published on that channel or a more stable one. "all" still returns every published emulator. --- emu/emu_downloads_menu.py | 22 ++++--- tests/test_find_emulator.py | 113 ++++++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+), 7 deletions(-) create mode 100644 tests/test_find_emulator.py diff --git a/emu/emu_downloads_menu.py b/emu/emu_downloads_menu.py index 5ace94c..b7caf57 100644 --- a/emu/emu_downloads_menu.py +++ b/emu/emu_downloads_menu.py @@ -335,14 +335,22 @@ def find_image(regexpr): def find_emulator(channel): - """Displayes an interactive menu to select a released emulator binary. + """Finds the released emulator binaries for the given channel. - Returns a ImuInfo object with the choice or None if the user aborts.""" - emu_infos = [ - x - for x in get_emus_info() - if "linux" in x.urls and (channel == "all" or x.channel == channel) - ] + As with sdkmanager, a channel includes the more stable channels below it: + "canary" gives the newest emulator published on any channel, "stable" the + newest stable one. "all" gives every published emulator. + + Returns a list of EmuInfo objects.""" + emu_infos = [x for x in get_emus_info() if "linux" in x.urls] + if channel != "all": + channels = list(CHANNEL_MAPPING.values()) + included = channels[: channels.index(channel) + 1] if channel in channels else [] + emu_infos = [x for x in emu_infos if x.channel in included] + # Only the newest one, so a channel still stands for a single emulator. + emu_infos = sorted( + emu_infos, key=lambda x: [int(n) for n in x.version.split(".")], reverse=True + )[:1] logging.info("Found %s matching images: %s", channel, [str(x) for x in emu_infos]) if not emu_infos: raise EmulatorNotFoundException(f"No emulator found in channel {channel}") diff --git a/tests/test_find_emulator.py b/tests/test_find_emulator.py new file mode 100644 index 0000000..65a32c4 --- /dev/null +++ b/tests/test_find_emulator.py @@ -0,0 +1,113 @@ +# Copyright 2026 The Android Open Source Project +# +# 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. +"""Unit tests for find_emulator: a channel includes the more stable channels +below it, as it does for sdkmanager.""" +import unittest.mock as mock + +import pytest +import requests + +import emu.emu_downloads_menu as menu + + +def _emu_pkg(channel, version, hostos="linux"): + """Build an emulator snippet shaped like the real SDK XML.""" + major, minor, micro = version.split(".") + return f""" + + {major}{minor}{micro} + + + + emulator-{version}.zip{hostos} + +""" + + +def _mock_repo(monkeypatch, packages): + xml = """ + + Terms + {} +""".format("".join(packages)) + + def mock_get(url): + return mock.MagicMock(content=xml, status_code=200) + + monkeypatch.setattr(requests, "get", mock_get) + + +@pytest.fixture +def stable_and_dev(monkeypatch): + """What the repository usually holds: the pre-release emulator is published + on channel-2, and nothing on channel-3.""" + _mock_repo( + monkeypatch, + [_emu_pkg("channel-2", "37.3.2"), _emu_pkg("channel-0", "37.2.12")], + ) + + +@pytest.mark.parametrize( + "channel, expected", + [ + ("stable", ["stable 37.2.12"]), + ("beta", ["stable 37.2.12"]), + ("dev", ["dev 37.3.2"]), + ("canary", ["dev 37.3.2"]), + ("all", ["dev 37.3.2", "stable 37.2.12"]), + ], +) +def test_channel_includes_more_stable_channels(stable_and_dev, channel, expected): + assert [str(x) for x in menu.find_emulator(channel)] == expected + + +def test_canary_prefers_newest_release(monkeypatch): + _mock_repo( + monkeypatch, + [ + _emu_pkg("channel-0", "37.2.12"), + _emu_pkg("channel-3", "37.10.1"), + _emu_pkg("channel-2", "37.9.4"), + ], + ) + # Compared as numbers: 37.10.1 is newer than 37.9.4. + assert [str(x) for x in menu.find_emulator("canary")] == ["canary 37.10.1"] + assert [str(x) for x in menu.find_emulator("dev")] == ["dev 37.9.4"] + + +def test_canary_gives_stable_when_it_is_the_newest(monkeypatch): + _mock_repo( + monkeypatch, + [_emu_pkg("channel-3", "37.2.1"), _emu_pkg("channel-0", "37.2.12")], + ) + assert [str(x) for x in menu.find_emulator("canary")] == ["stable 37.2.12"] + + +def test_stable_does_not_include_less_stable_channels(monkeypatch): + _mock_repo(monkeypatch, [_emu_pkg("channel-2", "37.3.2")]) + with pytest.raises(menu.EmulatorNotFoundException): + menu.find_emulator("stable") + + +def test_emulator_without_linux_build_is_skipped(monkeypatch): + _mock_repo( + monkeypatch, + [_emu_pkg("channel-2", "37.3.2", hostos="macosx"), _emu_pkg("channel-0", "37.2.12")], + ) + assert [str(x) for x in menu.find_emulator("canary")] == ["stable 37.2.12"] + + +def test_unknown_channel_finds_nothing(stable_and_dev): + with pytest.raises(menu.EmulatorNotFoundException): + menu.find_emulator("nightly") From 07e47e32173c6f209477567a9bd5c974358a904d Mon Sep 17 00:00:00 2001 From: JP Cottin Date: Sun, 4 Oct 2026 09:42:36 -0700 Subject: [PATCH 2/2] Use made-up version numbers in the find_emulator tests The fixtures used versions that look like current releases, which reads as if the tests depended on what is published. Only the order of the versions and their channel matter. --- tests/test_find_emulator.py | 38 ++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/tests/test_find_emulator.py b/tests/test_find_emulator.py index 65a32c4..663f451 100644 --- a/tests/test_find_emulator.py +++ b/tests/test_find_emulator.py @@ -48,24 +48,28 @@ def mock_get(url): monkeypatch.setattr(requests, "get", mock_get) +# The version numbers below are made up: only their order and the channel they +# are published on matter. + + @pytest.fixture def stable_and_dev(monkeypatch): """What the repository usually holds: the pre-release emulator is published on channel-2, and nothing on channel-3.""" _mock_repo( monkeypatch, - [_emu_pkg("channel-2", "37.3.2"), _emu_pkg("channel-0", "37.2.12")], + [_emu_pkg("channel-2", "2.0.0"), _emu_pkg("channel-0", "1.0.0")], ) @pytest.mark.parametrize( "channel, expected", [ - ("stable", ["stable 37.2.12"]), - ("beta", ["stable 37.2.12"]), - ("dev", ["dev 37.3.2"]), - ("canary", ["dev 37.3.2"]), - ("all", ["dev 37.3.2", "stable 37.2.12"]), + ("stable", ["stable 1.0.0"]), + ("beta", ["stable 1.0.0"]), + ("dev", ["dev 2.0.0"]), + ("canary", ["dev 2.0.0"]), + ("all", ["dev 2.0.0", "stable 1.0.0"]), ], ) def test_channel_includes_more_stable_channels(stable_and_dev, channel, expected): @@ -76,26 +80,26 @@ def test_canary_prefers_newest_release(monkeypatch): _mock_repo( monkeypatch, [ - _emu_pkg("channel-0", "37.2.12"), - _emu_pkg("channel-3", "37.10.1"), - _emu_pkg("channel-2", "37.9.4"), + _emu_pkg("channel-0", "1.0.0"), + _emu_pkg("channel-3", "1.10.0"), + _emu_pkg("channel-2", "1.9.0"), ], ) - # Compared as numbers: 37.10.1 is newer than 37.9.4. - assert [str(x) for x in menu.find_emulator("canary")] == ["canary 37.10.1"] - assert [str(x) for x in menu.find_emulator("dev")] == ["dev 37.9.4"] + # Compared as numbers: 1.10.0 is newer than 1.9.0. + assert [str(x) for x in menu.find_emulator("canary")] == ["canary 1.10.0"] + assert [str(x) for x in menu.find_emulator("dev")] == ["dev 1.9.0"] def test_canary_gives_stable_when_it_is_the_newest(monkeypatch): _mock_repo( monkeypatch, - [_emu_pkg("channel-3", "37.2.1"), _emu_pkg("channel-0", "37.2.12")], + [_emu_pkg("channel-3", "1.0.0"), _emu_pkg("channel-0", "2.0.0")], ) - assert [str(x) for x in menu.find_emulator("canary")] == ["stable 37.2.12"] + assert [str(x) for x in menu.find_emulator("canary")] == ["stable 2.0.0"] def test_stable_does_not_include_less_stable_channels(monkeypatch): - _mock_repo(monkeypatch, [_emu_pkg("channel-2", "37.3.2")]) + _mock_repo(monkeypatch, [_emu_pkg("channel-2", "2.0.0")]) with pytest.raises(menu.EmulatorNotFoundException): menu.find_emulator("stable") @@ -103,9 +107,9 @@ def test_stable_does_not_include_less_stable_channels(monkeypatch): def test_emulator_without_linux_build_is_skipped(monkeypatch): _mock_repo( monkeypatch, - [_emu_pkg("channel-2", "37.3.2", hostos="macosx"), _emu_pkg("channel-0", "37.2.12")], + [_emu_pkg("channel-2", "2.0.0", hostos="macosx"), _emu_pkg("channel-0", "1.0.0")], ) - assert [str(x) for x in menu.find_emulator("canary")] == ["stable 37.2.12"] + assert [str(x) for x in menu.find_emulator("canary")] == ["stable 1.0.0"] def test_unknown_channel_finds_nothing(stable_and_dev):