blob: 440699abc906414324907ae2af8cbf73cbbaa911 [file] [log] [blame]
# Copyright 2013 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Unit tests for xbuddy.py."""
import configparser
import os
import shutil
import tempfile
import time
from unittest import mock
import pytest
from chromite.lib import cros_test_lib
from chromite.lib import gs
from chromite.lib import path_util
from chromite.lib.xbuddy import xbuddy
pytestmark = cros_test_lib.pytestmark_inside_only
# pylint: disable=protected-access
# pylint: disable=no-value-for-parameter
GS_ALTERNATE_DIR = "gs://chromeos-alternate-archive/"
class xBuddyTest(cros_test_lib.TestCase):
"""Regression tests for xbuddy."""
def setUp(self) -> None:
self.static_image_dir = tempfile.mkdtemp("xbuddy_unittest_static")
self.mock_xb = xbuddy.XBuddy(True, static_dir=self.static_image_dir)
self.images_dir = tempfile.mkdtemp("xbuddy_unittest_images")
self.mock_xb.images_dir = self.images_dir
def tearDown(self) -> None:
"""Removes testing files."""
shutil.rmtree(self.static_image_dir)
shutil.rmtree(self.images_dir)
def testParseBoolean(self) -> None:
"""Check that some common True/False strings are handled."""
self.assertEqual(xbuddy.XBuddy.ParseBoolean(None), False)
self.assertEqual(xbuddy.XBuddy.ParseBoolean("false"), False)
self.assertEqual(xbuddy.XBuddy.ParseBoolean("bs"), False)
self.assertEqual(xbuddy.XBuddy.ParseBoolean("true"), True)
self.assertEqual(xbuddy.XBuddy.ParseBoolean("y"), True)
def testGetLatestVersionFromGsDir(self) -> None:
"""Test that we can get the most recent version from gsutil calls."""
mock_data1 = """gs://chromeos-releases/stable-channel/parrot/3701.96.0/
gs://chromeos-releases/stable-channel/parrot/3701.98.0/
gs://chromeos-releases/stable-channel/parrot/3912.100.0/
gs://chromeos-releases/stable-channel/parrot/3912.101.0/
gs://chromeos-releases/stable-channel/parrot/3912.79.0/
gs://chromeos-releases/stable-channel/parrot/3912.79.1/"""
mock_data2 = """\
gs://chromeos-image-archive/parrot-release/R26-3912.101.0
gs://chromeos-image-archive/parrot-release/R27-3912.101.0
gs://chromeos-image-archive/parrot-release/R28-3912.101.0"""
with mock.patch.object(
self.mock_xb,
"_LS",
side_effect=[mock_data1.splitlines(), mock_data2.splitlines()],
) as ls_mock:
self.assertEqual(
self.mock_xb._GetLatestVersionFromGsDir(
"url1", with_release=False
),
"3912.101.0",
)
ls_mock.assert_called_with("url1", list_subdirectory=False)
self.assertEqual(
self.mock_xb._GetLatestVersionFromGsDir(
"url2", list_subdirectory=True, with_release=True
),
"R28-3912.101.0",
)
ls_mock.assert_called_with("url2", list_subdirectory=True)
def testLookupOfficial(self) -> None:
"""Basic test of _LookupOfficial.
Checks that a given suffix is handled.
"""
with mock.patch.object(
gs.GSContext, "Cat", return_value="v"
) as cat_mock:
self.assertEqual(
self.mock_xb._LookupOfficial("b", suffix="-s"), "b-s/v"
)
cat_mock.assert_called_with(
"gs://chromeos-image-archive/b-s/LATEST-main", encoding="utf-8"
)
@mock.patch.object(
xbuddy.XBuddy,
"_GetLatestVersionFromGsDir",
side_effect=["4100.68.0", "R28-4100.68.0"],
)
def testLookupChannel(self, version_mock) -> None:
"""Basic test of _LookupChannel.
Checks that a given suffix is handled.
"""
self.assertEqual(
self.mock_xb._LookupChannel("b", "-release"),
"b-release/R28-4100.68.0",
)
version_mock.assert_called_with(
"gs://chromeos-image-archive/b-release/R*4100.68.0",
list_subdirectory=True,
)
def testLookupAliasPathRewrite(self) -> None:
"""Tests LookupAlias of path rewrite, including keyword substitution."""
path = "remote/BOARD/VERSION/test"
with mock.patch.object(
self.mock_xb.config, "get", side_effect=[configparser.Error, path]
) as get_mock:
self.assertEqual(
("remote/parrot/1.2.3/test", "-release"),
self.mock_xb.LookupAlias(
"foobar", board="parrot", version="1.2.3"
),
)
get_mock.assert_called_with("PATH_REWRITES", "foobar")
def testLookupAliasSuffix(self) -> None:
"""Tests LookupAlias of location suffix."""
with mock.patch.object(
self.mock_xb.config,
"get",
side_effect=["-random", configparser.Error],
) as get_mock:
self.assertEqual(
("foobar", "-random"),
self.mock_xb.LookupAlias(
"foobar", board="parrot", version="1.2.3"
),
)
get_mock.assert_called_with("PATH_REWRITES", "foobar")
def testLookupAliasPathRewriteAndSuffix(self) -> None:
"""Tests LookupAlias with both path rewrite and suffix."""
path = "remote/BOARD/VERSION/test"
with mock.patch.object(
self.mock_xb.config, "get", side_effect=["-random", path]
) as get_mock:
self.assertEqual(
("remote/parrot/1.2.3/test", "-random"),
self.mock_xb.LookupAlias(
"foobar", board="parrot", version="1.2.3"
),
)
get_mock.assert_called_with("PATH_REWRITES", "foobar")
@mock.patch.object(xbuddy.XBuddy, "_LookupOfficial")
def testResolveVersionToBuildIdAndChannel_Official(
self, lookup_mock
) -> None:
"""Test _ResolveVersionToBuildIdAndChannel works for official build."""
board = "chell"
suffix = "-s"
version = "latest-official"
self.mock_xb._ResolveVersionToBuildIdAndChannel(board, suffix, version)
lookup_mock.assert_called_with("chell", "-s", image_dir=None)
self.mock_xb._ResolveVersionToBuildIdAndChannel(
board, suffix, version, image_dir=GS_ALTERNATE_DIR
)
lookup_mock.assert_called_with(
"chell", "-s", image_dir="gs://chromeos-alternate-archive/"
)
version = "latest-official-paladin"
self.mock_xb._ResolveVersionToBuildIdAndChannel(board, suffix, version)
lookup_mock.assert_called_with("chell", "paladin", image_dir=None)
self.mock_xb._ResolveVersionToBuildIdAndChannel(
board, suffix, version, image_dir=GS_ALTERNATE_DIR
)
lookup_mock.assert_called_with(
"chell", "paladin", image_dir="gs://chromeos-alternate-archive/"
)
@mock.patch.object(xbuddy.XBuddy, "_LookupChannel")
def testResolveVersionToBuildIdAndChannel_Channel(
self, lookup_mock
) -> None:
"""Check _ResolveVersionToBuildIdAndChannel support for channels."""
board = "chell"
suffix = "-s"
version = "latest"
self.mock_xb._ResolveVersionToBuildIdAndChannel(board, suffix, version)
lookup_mock.assert_called_with("chell", "-s", image_dir=None)
self.mock_xb._ResolveVersionToBuildIdAndChannel(
board, suffix, version, image_dir=GS_ALTERNATE_DIR
)
lookup_mock.assert_called_with(
"chell", "-s", image_dir="gs://chromeos-alternate-archive/"
)
version = "latest-dev"
self.mock_xb._ResolveVersionToBuildIdAndChannel(board, suffix, version)
lookup_mock.assert_called_with(
"chell", "-s", channel="dev", image_dir=None
)
self.mock_xb._ResolveVersionToBuildIdAndChannel(
board, suffix, version, image_dir=GS_ALTERNATE_DIR
)
lookup_mock.assert_called_with(
"chell",
"-s",
channel="dev",
image_dir="gs://chromeos-alternate-archive/",
)
# TODO(dgarrett): Re-enable when crbug.com/585914 is fixed.
# def testResolveVersionToBuildId_BaseVersion(self):
# """Check _ResolveVersionToBuildId handles a base version."""
# board = 'b'
# suffix = '-s'
# self.mox.StubOutWithMock(self.mock_xb, '_ResolveBuildVersion')
# self.mock_xb._ResolveBuildVersion(board, suffix, '1.2.3').AndReturn(
# 'R12-1.2.3')
# self.mox.StubOutWithMock(self.mock_xb, '_RemoteBuildId')
# self.mock_xb._RemoteBuildId(board, suffix, 'R12-1.2.3')
# self.mox.ReplayAll()
# self.mock_xb._ResolveVersionToBuildId(board, suffix, '1.2.3')
# self.mox.VerifyAll()
def testBasicInterpretPath(self) -> None:
"""Basic checks for splitting a path"""
path = "parrot/R27-2455.0.0/test"
expected = ("test", "parrot", "R27-2455.0.0", True)
self.assertEqual(xbuddy.InterpretPath(path=path), expected)
path = "parrot/R27-2455.0.0/full_payload"
expected = ("full_payload", "parrot", "R27-2455.0.0", True)
self.assertEqual(xbuddy.InterpretPath(path=path), expected)
path = "parrot/R27-2455.0.0"
expected = ("ANY", "parrot", "R27-2455.0.0", True)
self.assertEqual(xbuddy.InterpretPath(path=path), expected)
path = "remote/parrot/R27-2455.0.0"
expected = ("test", "parrot", "R27-2455.0.0", False)
self.assertEqual(xbuddy.InterpretPath(path=path), expected)
path = "local/parrot/R27-2455.0.0"
expected = ("ANY", "parrot", "R27-2455.0.0", True)
self.assertEqual(xbuddy.InterpretPath(path=path), expected)
path = ""
expected = ("ANY", None, "latest", True)
self.assertEqual(xbuddy.InterpretPath(path=path), expected)
path = "local"
expected = ("ANY", None, "latest", True)
self.assertEqual(xbuddy.InterpretPath(path=path), expected)
path = "local/parrot/latest/ANY"
expected = ("ANY", "parrot", "latest", True)
self.assertEqual(xbuddy.InterpretPath(path=path), expected)
def testInterpretPathWithDefaults(self) -> None:
"""Test path splitting with default board/version."""
path = ""
expected = ("ANY", "parrot", "latest", True)
self.assertEqual(
expected,
xbuddy.InterpretPath(path=path, default_board="parrot"),
)
path = ""
expected = ("ANY", None, "1.2.3", True)
self.assertEqual(
expected,
xbuddy.InterpretPath(path=path, default_version="1.2.3"),
)
path = ""
expected = ("ANY", "parrot", "1.2.3", True)
self.assertEqual(
expected,
xbuddy.InterpretPath(
path=path, default_board="parrot", default_version="1.2.3"
),
)
path = "1.2.3"
expected = ("ANY", None, "1.2.3", True)
self.assertEqual(
expected,
xbuddy.InterpretPath(path=path, default_version="1.2.3"),
)
path = "latest"
expected = ("ANY", None, "latest", True)
self.assertEqual(
expected,
xbuddy.InterpretPath(path=path, default_version="1.2.3"),
)
path = "1.2.3"
expected = ("ANY", "parrot", "1.2.3", True)
self.assertEqual(
expected,
xbuddy.InterpretPath(
path=path, default_board="parrot", default_version="1.2.3"
),
)
path = "parrot"
expected = ("ANY", "parrot", "1.2.3", True)
self.assertEqual(
expected,
xbuddy.InterpretPath(path=path, default_version="1.2.3"),
)
path = "parrot"
expected = ("ANY", "parrot", "1.2.3", True)
self.assertEqual(
expected,
xbuddy.InterpretPath(
path=path, default_board="parrot", default_version="1.2.3"
),
)
with mock.patch.object(
path_util,
"DetermineCheckout",
return_value=path_util.CheckoutInfo(
path_util.CheckoutType.GCLIENT, None, None
),
):
path = ""
expected = ("test", "parrot", "1.2.3", False)
self.assertEqual(
expected,
xbuddy.InterpretPath(
path=path, default_board="parrot", default_version="1.2.3"
),
)
def testTimestampsAndList(self) -> None:
"""Creation and listing of builds according to their timestamps."""
# make 3 different timestamp files
b_id11 = "b1/v1"
b_id12 = "b1/v2"
b_id23 = "b2/v3"
xbuddy.update_timestamp(self.mock_xb._timestamp_folder, b_id11)
time.sleep(0.05)
xbuddy.update_timestamp(self.mock_xb._timestamp_folder, b_id12)
time.sleep(0.05)
xbuddy.update_timestamp(self.mock_xb._timestamp_folder, b_id23)
# reference second one again
time.sleep(0.05)
xbuddy.update_timestamp(self.mock_xb._timestamp_folder, b_id12)
# check that list returns the same 3 things, in last referenced order
result = self.mock_xb._ListBuildTimes()
self.assertEqual(result[0][0], b_id12)
self.assertEqual(result[1][0], b_id23)
self.assertEqual(result[2][0], b_id11)
def testSyncRegistry(self) -> None:
# check that there are no builds initially
result = self.mock_xb._ListBuildTimes()
self.assertEqual(len(result), 0)
# set up the stub build/images directory with images
boards = ["a", "b"]
versions = ["v1", "v2"]
for b in boards:
os.makedirs(os.path.join(self.mock_xb.images_dir, b))
for v in versions:
os.makedirs(os.path.join(self.mock_xb.images_dir, b, v))
# Sync and check that they've been added to xBuddy's registry
self.mock_xb._SyncRegistryWithBuildImages()
result = self.mock_xb._ListBuildTimes()
self.assertEqual(len(result), 4)
def testXBuddyCaching(self) -> None:
"""Caching & replacement of timestamp files."""
def _ReleaseOnly(name):
# All non-release URLs are invalid so we can meet expectations.
if name.find("-release") == -1:
raise gs.GSContextException("bad URL")
return name
with mock.patch.object(
gs.GSContext, "LS", side_effect=_ReleaseOnly
) as ls_mock:
with mock.patch.object(self.mock_xb, "_Download") as download_mock:
version = "%s-release/R0"
def _Download(image) -> None:
gs_image = "gs://chromeos-image-archive/" + version
self.mock_xb.Get(("remote", image, "R0", "test"))
ls_mock.assert_called_with(gs_image % image)
download_mock.assert_called_with(
gs_image % image, ["test_image"], version % image
)
time.sleep(0.05)
# Requires default capacity.
self.assertEqual(self.mock_xb._Capacity(), 5)
# Get 6 different images: a,b,c,d,e,f.
images = ["a", "b", "c", "d", "e", "f"]
for image in images:
_Download(image)
# Check that b,c,d,e,f are still stored.
result = self.mock_xb._ListBuildTimes()
self.assertEqual(len(result), 5)
# Flip the list to get reverse chronological order
images.reverse()
for i in range(5):
self.assertEqual(result[i][0], version % images[i])
# Get b,a.
images = ["b", "a"]
for image in images:
_Download(image)
# Check that d,e,f,b,a are still stored.
result = self.mock_xb._ListBuildTimes()
self.assertEqual(len(result), 5)
images_expected = ["a", "b", "f", "e", "d"]
for i in range(5):
self.assertEqual(
result[i][0], "%s-release/R0" % images_expected[i]
)
@mock.patch.object(
xbuddy.XBuddy, "_Download", return_value=[["/path/to/image.bin"]]
)
@mock.patch.object(
xbuddy.XBuddy,
"_ResolveVersionToBuildIdAndChannel",
return_value=("reef/R1-1.2.3", "stable"),
)
# pylint: disable=unused-argument
def testGet(self, resolve_mock, downloader_mock) -> None:
"""Tests _GetArtifact method."""
self.assertEqual(
self.mock_xb.Get(["remote", "reef", "R1-1.2.3", "test"]),
("reef/R1-1.2.3", "/path/to/image.bin"),
)
self.assertEqual(
self.mock_xb.Get(["remote", "reef", "R1-1.2.3"]),
("reef/R1-1.2.3", "/path/to/image.bin"),
)
valid_full_uris = [
(
"xbuddy://strongbad/R109-14893.0.0/test",
xbuddy.XBuddyComponents("test", "strongbad", "R109-14893.0.0", True),
),
(
"xbuddy://betty/R106-14212.0.0/full_payload",
xbuddy.XBuddyComponents(
"full_payload", "betty", "R106-14212.0.0", True
),
),
(
"xbuddy://boardname/R100-14000.0.0",
xbuddy.XBuddyComponents("ANY", "boardname", "R100-14000.0.0", True),
),
(
"xbuddy://remote/strongbad/R109-14893.0.0",
xbuddy.XBuddyComponents("test", "strongbad", "R109-14893.0.0", False),
),
(
"xbuddy://local/betty/R106-14212.0.0",
xbuddy.XBuddyComponents("ANY", "betty", "R106-14212.0.0", True),
),
("xbuddy://", xbuddy.XBuddyComponents("ANY", None, "latest", True)),
(
"xbuddy://local",
xbuddy.XBuddyComponents("ANY", None, "latest", True),
),
(
"xbuddy://remote",
xbuddy.XBuddyComponents("test", None, "latest", False),
),
(
"xbuddy://local/boardname/latest/ANY",
xbuddy.XBuddyComponents("ANY", "boardname", "latest", True),
),
(
"xbuddy://remote/boardname/latest/ANY",
xbuddy.XBuddyComponents("ANY", "boardname", "latest", False),
),
]
@pytest.mark.parametrize("uri,expected", valid_full_uris)
def test_parse_valid_uri(uri, expected) -> None:
"""Basic checks for parsing an XBuddy URI."""
assert xbuddy.parse(uri) == expected
valid_uris_without_schema = [
(
"strongbad/R109-14893.0.0/test",
xbuddy.XBuddyComponents("test", "strongbad", "R109-14893.0.0", True),
),
("", xbuddy.XBuddyComponents("ANY", None, "latest", True)),
]
@pytest.mark.parametrize("uri,expected", valid_uris_without_schema)
def test_parse_uri_with_empty_scheme_not_strict(uri, expected) -> None:
"""Basic checks for parsing an XBuddy URI without a scheme."""
assert xbuddy.parse(uri) == expected
@pytest.mark.parametrize("uri", [tc[0] for tc in valid_uris_without_schema])
def test_parse_uri_with_empty_scheme_strict(uri) -> None:
"""Fail strict scheme checking for URIs without scheme provided."""
with pytest.raises(xbuddy.XBuddyInvalidSchemeException):
xbuddy.parse(uri, strict=True)
@pytest.mark.parametrize(
"uri", ["gs://test/bucket", "usb://", "ftp://path/to/image"]
)
def test_parse_invalid_uri(uri) -> None:
"""Test exception is raised when the scheme is specified and not xbuddy."""
with pytest.raises(xbuddy.XBuddyInvalidSchemeException):
xbuddy.parse(uri)