blob: 2838dff3119191032469b4d13cd2ecea6bd1566b [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.
"""Unittests for pushimage.py"""
import collections
import os
from unittest import mock
from chromite.lib import cros_build_lib
from chromite.lib import cros_test_lib
from chromite.lib import gs
from chromite.lib import gs_unittest
from chromite.lib import osutils
from chromite.lib import partial_mock
from chromite.lib import signing
from chromite.scripts import pushimage
# Use our local copy of insns for testing as the main one is not available in
# the public manifest. Even though _REL is a relative path, this works because
# os.join leaves absolute paths on the right hand side alone.
signing.INPUT_INSN_DIR_REL = signing.TEST_INPUT_INSN_DIR
class InputInsnsTest(cros_test_lib.MockTestCase):
"""Tests for InputInsns"""
def setUp(self) -> None:
self.StartPatcher(gs_unittest.GSContextMock())
def testBasic(self) -> None:
"""Simple smoke test"""
insns = pushimage.InputInsns("test.board")
insns.GetInsnFile("recovery")
self.assertEqual(insns.GetChannels(), ["dev", "canary"])
self.assertEqual(insns.GetKeysets(), ["stumpy-mp-v3"])
def testGetInsnFile(self) -> None:
"""Verify various inputs result in right insns path"""
testdata = (
("UPPER_CAPS", "UPPER_CAPS"),
("recovery", "test.board"),
("firmware", "test.board.firmware"),
("factory", "test.board.factory"),
)
insns = pushimage.InputInsns("test.board")
for image_type, filename in testdata:
ret = insns.GetInsnFile(image_type)
self.assertEqual(
os.path.basename(ret), "%s.instructions" % (filename)
)
def testSplitCfgField(self) -> None:
"""Verify splitting behavior behaves"""
testdata = (
("", []),
("a b c", ["a", "b", "c"]),
("a, b", ["a", "b"]),
("a,b", ["a", "b"]),
("a,\tb", ["a", "b"]),
("a\tb", ["a", "b"]),
)
for val, exp in testdata:
ret = pushimage.InputInsns.SplitCfgField(val)
self.assertEqual(ret, exp)
def testOutputInsnsBasic(self) -> None:
"""Verify output instructions are correct"""
exp_content = """[insns]
channel = dev canary
keyset = stumpy-mp-v3
chromeos_shell = false
ensure_no_password = true
firmware_update = true
security_checks = true
create_nplusone = true
[general]
"""
insns = pushimage.InputInsns("test.board")
self.assertEqual(insns.GetAltInsnSets(), [None])
m = self.PatchObject(osutils, "WriteFile")
insns.OutputInsns("/bogus", {}, {})
self.assertTrue(m.called)
content = m.call_args_list[0][0][1]
self.assertEqual(content.rstrip(), exp_content.rstrip())
def testOutputInsnsReplacements(self) -> None:
"""Verify output instructions can be updated"""
exp_content = """[insns]
channel = dev
keyset = batman
chromeos_shell = false
ensure_no_password = true
firmware_update = true
security_checks = true
create_nplusone = true
[general]
board = board
config_board = test.board
"""
sect_insns = {
"channel": "dev",
"keyset": "batman",
}
sect_general = collections.OrderedDict(
(
("board", "board"),
("config_board", "test.board"),
)
)
insns = pushimage.InputInsns("test.board")
m = self.PatchObject(osutils, "WriteFile")
insns.OutputInsns("/a/file", sect_insns, sect_general)
self.assertTrue(m.called)
content = m.call_args_list[0][0][1]
self.assertEqual(content.rstrip(), exp_content.rstrip())
def testOutputInsnsMergeAlts(self) -> None:
"""Verify handling of alternative insns.xxx sections"""
TEMPLATE_CONTENT = """[insns]
channel = %(channel)s
chromeos_shell = false
ensure_no_password = true
firmware_update = true
security_checks = true
create_nplusone = true
override = sect_insns
keyset = %(keyset)s
%(extra)s
[general]
board = board
config_board = test.board
"""
exp_alts = ["insns.one", "insns.two", "insns.hotsoup"]
exp_fields = {
"one": {
"channel": "dev canary",
"keyset": "OneKeyset",
"extra": "",
},
"two": {"channel": "best", "keyset": "TwoKeyset", "extra": ""},
"hotsoup": {
"channel": "dev canary",
"keyset": "ColdKeyset",
"extra": "soup = cheddar\n",
},
}
# Make sure this overrides the insn sections.
sect_insns = {
"override": "sect_insns",
}
sect_insns_copy = sect_insns.copy()
sect_general = collections.OrderedDict(
(
("board", "board"),
("config_board", "test.board"),
)
)
insns = pushimage.InputInsns("test.multi")
self.assertEqual(insns.GetAltInsnSets(), exp_alts)
m = self.PatchObject(osutils, "WriteFile")
for alt in exp_alts:
m.reset_mock()
insns.OutputInsns(
"/a/file", sect_insns, sect_general, insns_merge=alt
)
self.assertEqual(sect_insns, sect_insns_copy)
self.assertTrue(m.called)
content = m.call_args_list[0][0][1]
exp_content = TEMPLATE_CONTENT % exp_fields[alt[6:]]
self.assertEqual(content.rstrip(), exp_content.rstrip())
class MarkImageToBeSignedTest(gs_unittest.AbstractGSContextTest):
"""Tests for MarkImageToBeSigned()"""
def setUp(self) -> None:
# Minor optimization -- we call this for logging purposes in the main
# code, but don't really care about it for testing. It just slows us.
self.PatchObject(
cros_build_lib, "MachineDetails", return_value="1234\n"
)
def testBasic(self) -> None:
"""Simple smoke test"""
tbs_base = "gs://some-bucket"
insns_path = "chan/board/ver/file.instructions"
tbs_file = (
"%s/tobesigned/90,chan,board,ver,file.instructions" % tbs_base
)
ret = pushimage.MarkImageToBeSigned(self.ctx, tbs_base, insns_path, 90)
self.assertEqual(ret, tbs_file)
def testPriority(self) -> None:
"""Verify diff priority values get used correctly"""
for prio, sprio in ((0, "00"), (9, "09"), (35, "35"), (99, "99")):
ret = pushimage.MarkImageToBeSigned(self.ctx, "", "", prio)
self.assertEqual(ret, "/tobesigned/%s," % sprio)
def testBadPriority(self) -> None:
"""Verify we reject bad priority values"""
for prio in (-10, -1, 100, 91239):
self.assertRaises(
ValueError,
pushimage.MarkImageToBeSigned,
self.ctx,
"",
"",
prio,
)
def testTbsUpload(self) -> None:
"""Make sure we actually try to upload the file"""
pushimage.MarkImageToBeSigned(self.ctx, "", "", 50)
self.gs_mock.assertCommandContains(["cp", "--"])
class PushImageTests(gs_unittest.AbstractGSContextTest):
"""Tests for PushImage()"""
def setUp(self) -> None:
self.mark_mock = self.PatchObject(pushimage, "MarkImageToBeSigned")
def testBasic(self) -> None:
"""Simple smoke test"""
EXPECTED = {
"canary": [
"gs://chromeos-releases/canary-channel/test.board-hi/5126.0.0/"
"ChromeOS-recovery-R34-5126.0.0-test.board-hi.instructions"
],
"dev": [
"gs://chromeos-releases/dev-channel/test.board-hi/5126.0.0/"
"ChromeOS-recovery-R34-5126.0.0-test.board-hi.instructions"
],
}
with mock.patch.object(gs.GSContext, "Exists", return_value=True):
urls = pushimage.PushImage(
"/src", "test.board", "R34-5126.0.0", profile="hi"
)
self.assertEqual(urls, EXPECTED)
def testBasic_SignTypesEmptyList(self) -> None:
"""Tests PushImage behavior when |sign_types| is empty instead of None.
As part of the buildbots, PushImage function always receives a tuple for
|sign_types| argument. This test checks the behavior for empty tuple.
"""
EXPECTED = {
"canary": [
"gs://chromeos-releases/canary-channel/test.board-hi/5126.0.0/"
"ChromeOS-recovery-R34-5126.0.0-test.board-hi.instructions"
],
"dev": [
"gs://chromeos-releases/dev-channel/test.board-hi/5126.0.0/"
"ChromeOS-recovery-R34-5126.0.0-test.board-hi.instructions"
],
}
with mock.patch.object(gs.GSContext, "Exists", return_value=True):
urls = pushimage.PushImage(
"/src",
"test.board",
"R34-5126.0.0",
profile="hi",
sign_types=(),
)
self.assertEqual(urls, EXPECTED)
def testBasic_RealBoardName(self) -> None:
"""Runs a simple smoke test using a real board name."""
EXPECTED = {
"canary": [
"gs://chromeos-releases/canary-channel/x86-alex/5126.0.0/"
"ChromeOS-recovery-R34-5126.0.0-x86-alex.instructions"
],
"dev": [
"gs://chromeos-releases/dev-channel/x86-alex/5126.0.0/"
"ChromeOS-recovery-R34-5126.0.0-x86-alex.instructions"
],
}
with mock.patch.object(gs.GSContext, "Exists", return_value=True):
urls = pushimage.PushImage("/src", "x86-alex", "R34-5126.0.0")
self.assertEqual(urls, EXPECTED)
def testBasicMock(self) -> None:
"""Simple smoke test in mock mode"""
with mock.patch.object(gs.GSContext, "Exists", return_value=True):
pushimage.PushImage(
"/src", "test.board", "R34-5126.0.0", dryrun=True, mock=True
)
def testBadVersion(self) -> None:
"""Make sure we barf on bad version strings"""
self.assertRaises(ValueError, pushimage.PushImage, "", "", "asdf")
def testNoInsns(self) -> None:
"""Boards w/out insn files should get skipped"""
urls = pushimage.PushImage("/src", "a bad bad board", "R34-5126.0.0")
self.assertEqual(self.gs_mock.call_count, 0)
self.assertEqual(urls, None)
def testSignTypesRecovery(self) -> None:
"""Only sign the requested recovery type"""
EXPECTED = {
"canary": [
"gs://chromeos-releases/canary-channel/test.board/5126.0.0/"
"ChromeOS-recovery-R34-5126.0.0-test.board.instructions"
],
"dev": [
"gs://chromeos-releases/dev-channel/test.board/5126.0.0/"
"ChromeOS-recovery-R34-5126.0.0-test.board.instructions"
],
}
with mock.patch.object(gs.GSContext, "Exists", return_value=True):
urls = pushimage.PushImage(
"/src", "test.board", "R34-5126.0.0", sign_types=["recovery"]
)
self.assertEqual(self.gs_mock.call_count, 26)
self.assertTrue(self.mark_mock.called)
self.assertEqual(urls, EXPECTED)
def testSignTypesBase(self) -> None:
"""Only sign the requested recovery type"""
EXPECTED = {
"canary": [
"gs://chromeos-releases/canary-channel/test.board/5126.0.0/"
"ChromeOS-base-R34-5126.0.0-test.board.instructions"
],
"dev": [
"gs://chromeos-releases/dev-channel/test.board/5126.0.0/"
"ChromeOS-base-R34-5126.0.0-test.board.instructions"
],
}
with mock.patch.object(gs.GSContext, "Exists", return_value=True):
urls = pushimage.PushImage(
"/src", "test.board", "R34-5126.0.0", sign_types=["base"]
)
self.assertEqual(self.gs_mock.call_count, 28)
self.assertTrue(self.mark_mock.called)
self.assertEqual(urls, EXPECTED)
def testSignTypesGscFirmware(self) -> None:
"""Only sign the requested type"""
EXPECTED = {
"canary": [
"gs://chromeos-releases/canary-channel/board2/5126.0.0/"
"ChromeOS-gsc_firmware-R34-5126.0.0-board2.instructions"
],
"dev": [
"gs://chromeos-releases/dev-channel/board2/5126.0.0/"
"ChromeOS-gsc_firmware-R34-5126.0.0-board2.instructions"
],
}
with mock.patch.object(gs.GSContext, "Exists", return_value=True):
urls = pushimage.PushImage(
"/src", "board2", "R34-5126.0.0", sign_types=["gsc_firmware"]
)
self.assertEqual(self.gs_mock.call_count, 28)
self.assertTrue(self.mark_mock.called)
self.assertEqual(urls, EXPECTED)
def testSignTypesNone(self) -> None:
"""Verify nothing is signed when we request an unavailable type"""
urls = pushimage.PushImage(
"/src", "test.board", "R34-5126.0.0", sign_types=["nononononono"]
)
self.assertEqual(self.gs_mock.call_count, 24)
self.assertFalse(self.mark_mock.called)
self.assertEqual(urls, {})
def testGsError(self) -> None:
"""Verify random GS errors don't make us blow up entirely"""
self.gs_mock.AddCmdResult(
partial_mock.In("stat"), returncode=1, stdout="gobblety gook\n"
)
with cros_test_lib.LoggingCapturer("chromite"):
self.assertRaises(
pushimage.PushError,
pushimage.PushImage,
"/src",
"test.board",
"R34-5126.0.0",
)
def testMultipleKeysets(self) -> None:
"""Verify behavior when processing an insn w/multiple keysets"""
EXPECTED = {
"canary": [
(
"gs://chromeos-releases/canary-channel/test.board/5126.0.0/"
"ChromeOS-recovery-R34-5126.0.0-test.board.instructions"
),
(
"gs://chromeos-releases/canary-channel/test.board/5126.0.0/"
"ChromeOS-recovery-R34-5126.0.0-test.board-key2."
"instructions"
),
(
"gs://chromeos-releases/canary-channel/test.board/5126.0.0/"
"ChromeOS-recovery-R34-5126.0.0-test.board-key3."
"instructions"
),
],
"dev": [
(
"gs://chromeos-releases/dev-channel/test.board/5126.0.0/"
"ChromeOS-recovery-R34-5126.0.0-test.board.instructions"
),
(
"gs://chromeos-releases/dev-channel/test.board/5126.0.0/"
"ChromeOS-recovery-R34-5126.0.0-test.board-key2."
"instructions"
),
(
"gs://chromeos-releases/dev-channel/test.board/5126.0.0/"
"ChromeOS-recovery-R34-5126.0.0-test.board-key3."
"instructions"
),
],
}
with mock.patch.object(gs.GSContext, "Exists", return_value=True):
urls = pushimage.PushImage(
"/src",
"test.board",
"R34-5126.0.0",
force_keysets=("key1", "key2", "key3"),
)
self.assertEqual(urls, EXPECTED)
def testForceChannel(self) -> None:
"""Verify behavior when user has specified custom channel"""
EXPECTED = {
"meep": [
(
"gs://chromeos-releases/meep-channel/test.board/5126.0.0/"
"ChromeOS-recovery-R34-5126.0.0-test.board.instructions"
),
],
}
with mock.patch.object(gs.GSContext, "Exists", return_value=True):
urls = pushimage.PushImage(
"/src", "test.board", "R34-5126.0.0", force_channels=("meep",)
)
self.assertEqual(urls, EXPECTED)
def testMultipleAltInsns(self) -> None:
"""Verify behavior when processing an insn w/multiple insn overlays"""
EXPECTED = {
"canary": [
(
"gs://chromeos-releases/canary-channel/test.multi/1.0.0/"
"ChromeOS-recovery-R1-1.0.0-test.multi.instructions"
),
(
"gs://chromeos-releases/canary-channel/test.multi/1.0.0/"
"ChromeOS-recovery-R1-1.0.0-test.multi-TwoKeyset."
"instructions"
),
(
"gs://chromeos-releases/canary-channel/test.multi/1.0.0/"
"ChromeOS-recovery-R1-1.0.0-test.multi-ColdKeyset."
"instructions"
),
],
"dev": [
(
"gs://chromeos-releases/dev-channel/test.multi/1.0.0/"
"ChromeOS-recovery-R1-1.0.0-test.multi.instructions"
),
(
"gs://chromeos-releases/dev-channel/test.multi/1.0.0/"
"ChromeOS-recovery-R1-1.0.0-test.multi-TwoKeyset."
"instructions"
),
(
"gs://chromeos-releases/dev-channel/test.multi/1.0.0/"
"ChromeOS-recovery-R1-1.0.0-test.multi-ColdKeyset."
"instructions"
),
],
}
with mock.patch.object(gs.GSContext, "Exists", return_value=True):
urls = pushimage.PushImage("/src", "test.multi", "R1-1.0.0")
self.assertEqual(urls, EXPECTED)
class MainTests(cros_test_lib.MockTestCase):
"""Tests for main()"""
def setUp(self) -> None:
self.PatchObject(pushimage, "PushImage")
def testBasic(self) -> None:
"""Simple smoke test"""
pushimage.main(["--board", "test.board", "/src", "--yes"])