Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions emu/emu_downloads_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Expand Down
117 changes: 117 additions & 0 deletions tests/test_find_emulator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# 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 <remotePackage> snippet shaped like the real SDK XML."""
major, minor, micro = version.split(".")
return f"""
<remotePackage path="emulator">
<revision><major>{major}</major><minor>{minor}</minor><micro>{micro}</micro></revision>
<uses-license ref="android-sdk-license"/>
<channelRef ref="{channel}"/>
<archives>
<archive><complete><url>emulator-{version}.zip</url></complete><host-os>{hostos}</host-os></archive>
</archives>
</remotePackage>"""


def _mock_repo(monkeypatch, packages):
xml = """<?xml version="1.0" ?>
<sdk-repository>
<license id="android-sdk-license" type="text">Terms</license>
{}
</sdk-repository>""".format("".join(packages))

def mock_get(url):
return mock.MagicMock(content=xml, status_code=200)

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", "2.0.0"), _emu_pkg("channel-0", "1.0.0")],
)


@pytest.mark.parametrize(
"channel, expected",
[
("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):
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", "1.0.0"),
_emu_pkg("channel-3", "1.10.0"),
_emu_pkg("channel-2", "1.9.0"),
],
)
# 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", "1.0.0"), _emu_pkg("channel-0", "2.0.0")],
)
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", "2.0.0")])
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", "2.0.0", hostos="macosx"), _emu_pkg("channel-0", "1.0.0")],
)
assert [str(x) for x in menu.find_emulator("canary")] == ["stable 1.0.0"]


def test_unknown_channel_finds_nothing(stable_and_dev):
with pytest.raises(menu.EmulatorNotFoundException):
menu.find_emulator("nightly")
Loading