blob: 7c8ea2c901e82940309adc4f2afcfcf55d039290 [file] [log] [blame]
Mike Frysingerd13faeb2013-09-05 16:00:46 -04001# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Unittests for pushimage.py"""
6
Mike Frysingerd7c93092019-10-14 00:12:50 -04007import collections
Mike Frysingerd13faeb2013-09-05 16:00:46 -04008import os
Mike Frysinger166fea02021-02-12 05:30:33 -05009from unittest import mock
Mike Frysingerd13faeb2013-09-05 16:00:46 -040010
Mike Frysinger6791b1d2014-03-04 20:52:22 -050011from chromite.lib import cros_build_lib
Mike Frysingerd13faeb2013-09-05 16:00:46 -040012from chromite.lib import cros_test_lib
Don Garrett9fd20a82014-09-04 11:37:22 -070013from chromite.lib import gs
Mike Frysingerd13faeb2013-09-05 16:00:46 -040014from chromite.lib import gs_unittest
15from chromite.lib import osutils
Mike Frysinger4495b032014-03-05 17:24:03 -050016from chromite.lib import partial_mock
Mike Frysingerd13faeb2013-09-05 16:00:46 -040017from chromite.lib import signing
18from chromite.scripts import pushimage
19
20
Greg Edelstona4c9b3b2020-01-07 17:51:13 -070021# Use our local copy of insns for testing as the main one is not available in
22# the public manifest. Even though _REL is a relative path, this works because
23# os.join leaves absolute paths on the right hand side alone.
24signing.INPUT_INSN_DIR_REL = signing.TEST_INPUT_INSN_DIR
25
26
Mike Frysingerd13faeb2013-09-05 16:00:46 -040027class InputInsnsTest(cros_test_lib.MockTestCase):
28 """Tests for InputInsns"""
29
Don Garrettac529772015-04-29 15:09:26 -070030 def setUp(self):
31 self.StartPatcher(gs_unittest.GSContextMock())
32
Mike Frysingerd13faeb2013-09-05 16:00:46 -040033 def testBasic(self):
34 """Simple smoke test"""
Don Garrettac529772015-04-29 15:09:26 -070035 insns = pushimage.InputInsns('test.board')
36 insns.GetInsnFile('recovery')
37 self.assertEqual(insns.GetChannels(), ['dev', 'canary'])
38 self.assertEqual(insns.GetKeysets(), ['stumpy-mp-v3'])
Mike Frysingerd13faeb2013-09-05 16:00:46 -040039
40 def testGetInsnFile(self):
41 """Verify various inputs result in right insns path"""
42 testdata = (
43 ('UPPER_CAPS', 'UPPER_CAPS'),
44 ('recovery', 'test.board'),
45 ('firmware', 'test.board.firmware'),
46 ('factory', 'test.board.factory'),
47 )
48 insns = pushimage.InputInsns('test.board')
49 for image_type, filename in testdata:
50 ret = insns.GetInsnFile(image_type)
51 self.assertEqual(os.path.basename(ret), '%s.instructions' % (filename))
52
53 def testSplitCfgField(self):
54 """Verify splitting behavior behaves"""
55 testdata = (
56 ('', []),
57 ('a b c', ['a', 'b', 'c']),
58 ('a, b', ['a', 'b']),
59 ('a,b', ['a', 'b']),
60 ('a,\tb', ['a', 'b']),
61 ('a\tb', ['a', 'b']),
62 )
63 for val, exp in testdata:
64 ret = pushimage.InputInsns.SplitCfgField(val)
65 self.assertEqual(ret, exp)
66
67 def testOutputInsnsBasic(self):
68 """Verify output instructions are sane"""
69 exp_content = """[insns]
Mike Frysingerd13faeb2013-09-05 16:00:46 -040070channel = dev canary
Mike Frysingerd84d91e2015-11-05 18:02:24 -050071keyset = stumpy-mp-v3
Mike Frysingerd13faeb2013-09-05 16:00:46 -040072chromeos_shell = false
73ensure_no_password = true
74firmware_update = true
75security_checks = true
76create_nplusone = true
77
78[general]
79"""
80
81 insns = pushimage.InputInsns('test.board')
Mike Frysinger37ccc2b2015-11-11 17:16:51 -050082 self.assertEqual(insns.GetAltInsnSets(), [None])
Mike Frysingerd13faeb2013-09-05 16:00:46 -040083 m = self.PatchObject(osutils, 'WriteFile')
Mike Frysingerd84d91e2015-11-05 18:02:24 -050084 insns.OutputInsns('/bogus', {}, {})
Mike Frysingerd13faeb2013-09-05 16:00:46 -040085 self.assertTrue(m.called)
86 content = m.call_args_list[0][0][1]
87 self.assertEqual(content.rstrip(), exp_content.rstrip())
88
89 def testOutputInsnsReplacements(self):
90 """Verify output instructions can be updated"""
91 exp_content = """[insns]
Mike Frysingerd13faeb2013-09-05 16:00:46 -040092channel = dev
Mike Frysingerd84d91e2015-11-05 18:02:24 -050093keyset = batman
Mike Frysingerd13faeb2013-09-05 16:00:46 -040094chromeos_shell = false
95ensure_no_password = true
96firmware_update = true
97security_checks = true
98create_nplusone = true
99
100[general]
101board = board
102config_board = test.board
103"""
104 sect_insns = {
105 'channel': 'dev',
106 'keyset': 'batman',
107 }
Mike Frysingerd7c93092019-10-14 00:12:50 -0400108 sect_general = collections.OrderedDict((
109 ('board', 'board'),
110 ('config_board', 'test.board'),
111 ))
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400112
113 insns = pushimage.InputInsns('test.board')
114 m = self.PatchObject(osutils, 'WriteFile')
Mike Frysingerd84d91e2015-11-05 18:02:24 -0500115 insns.OutputInsns('/a/file', sect_insns, sect_general)
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400116 self.assertTrue(m.called)
117 content = m.call_args_list[0][0][1]
118 self.assertEqual(content.rstrip(), exp_content.rstrip())
119
Mike Frysinger37ccc2b2015-11-11 17:16:51 -0500120 def testOutputInsnsMergeAlts(self):
121 """Verify handling of alternative insns.xxx sections"""
122 TEMPLATE_CONTENT = """[insns]
123channel = %(channel)s
124chromeos_shell = false
125ensure_no_password = true
126firmware_update = true
127security_checks = true
128create_nplusone = true
129override = sect_insns
130keyset = %(keyset)s
131%(extra)s
132[general]
133board = board
134config_board = test.board
135"""
136
137 exp_alts = ['insns.one', 'insns.two', 'insns.hotsoup']
138 exp_fields = {
139 'one': {'channel': 'dev canary', 'keyset': 'OneKeyset', 'extra': ''},
140 'two': {'channel': 'best', 'keyset': 'TwoKeyset', 'extra': ''},
141 'hotsoup': {
142 'channel': 'dev canary',
143 'keyset': 'ColdKeyset',
144 'extra': 'soup = cheddar\n',
145 },
146 }
147
148 # Make sure this overrides the insn sections.
149 sect_insns = {
150 'override': 'sect_insns',
151 }
152 sect_insns_copy = sect_insns.copy()
Mike Frysingerd7c93092019-10-14 00:12:50 -0400153 sect_general = collections.OrderedDict((
154 ('board', 'board'),
155 ('config_board', 'test.board'),
156 ))
Mike Frysinger37ccc2b2015-11-11 17:16:51 -0500157
158 insns = pushimage.InputInsns('test.multi')
159 self.assertEqual(insns.GetAltInsnSets(), exp_alts)
160 m = self.PatchObject(osutils, 'WriteFile')
161
162 for alt in exp_alts:
163 m.reset_mock()
164 insns.OutputInsns('/a/file', sect_insns, sect_general, insns_merge=alt)
165 self.assertEqual(sect_insns, sect_insns_copy)
166 self.assertTrue(m.called)
167 content = m.call_args_list[0][0][1]
168 exp_content = TEMPLATE_CONTENT % exp_fields[alt[6:]]
169 self.assertEqual(content.rstrip(), exp_content.rstrip())
170
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400171
Mike Frysinger4495b032014-03-05 17:24:03 -0500172class MarkImageToBeSignedTest(gs_unittest.AbstractGSContextTest):
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400173 """Tests for MarkImageToBeSigned()"""
174
175 def setUp(self):
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400176 # Minor optimization -- we call this for logging purposes in the main
177 # code, but don't really care about it for testing. It just slows us.
Mike Frysinger6430d132014-10-27 23:43:30 -0400178 self.PatchObject(cros_build_lib, 'MachineDetails', return_value='1234\n')
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400179
180 def testBasic(self):
181 """Simple smoke test"""
182 tbs_base = 'gs://some-bucket'
183 insns_path = 'chan/board/ver/file.instructions'
184 tbs_file = '%s/tobesigned/90,chan,board,ver,file.instructions' % tbs_base
185 ret = pushimage.MarkImageToBeSigned(self.ctx, tbs_base, insns_path, 90)
186 self.assertEqual(ret, tbs_file)
187
188 def testPriority(self):
189 """Verify diff priority values get used correctly"""
190 for prio, sprio in ((0, '00'), (9, '09'), (35, '35'), (99, '99')):
191 ret = pushimage.MarkImageToBeSigned(self.ctx, '', '', prio)
Mike Frysinger2d589a12019-08-25 14:15:12 -0400192 self.assertEqual(ret, '/tobesigned/%s,' % sprio)
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400193
194 def testBadPriority(self):
195 """Verify we reject bad priority values"""
196 for prio in (-10, -1, 100, 91239):
197 self.assertRaises(ValueError, pushimage.MarkImageToBeSigned, self.ctx,
198 '', '', prio)
199
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400200 def testTbsUpload(self):
201 """Make sure we actually try to upload the file"""
202 pushimage.MarkImageToBeSigned(self.ctx, '', '', 50)
203 self.gs_mock.assertCommandContains(['cp', '--'])
204
205
Mike Frysinger4495b032014-03-05 17:24:03 -0500206class PushImageTests(gs_unittest.AbstractGSContextTest):
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400207 """Tests for PushImage()"""
208
209 def setUp(self):
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400210 self.mark_mock = self.PatchObject(pushimage, 'MarkImageToBeSigned')
211
212 def testBasic(self):
213 """Simple smoke test"""
Don Garrett9459c2f2014-01-22 18:20:24 -0800214 EXPECTED = {
215 'canary': [
Mike Frysingerd6e2df02014-11-26 02:55:04 -0500216 ('gs://chromeos-releases/canary-channel/test.board-hi/5126.0.0/'
217 'ChromeOS-recovery-R34-5126.0.0-test.board-hi.instructions')],
Don Garrett9459c2f2014-01-22 18:20:24 -0800218 'dev': [
Mike Frysingerd6e2df02014-11-26 02:55:04 -0500219 ('gs://chromeos-releases/dev-channel/test.board-hi/5126.0.0/'
220 'ChromeOS-recovery-R34-5126.0.0-test.board-hi.instructions')],
Don Garrett9459c2f2014-01-22 18:20:24 -0800221 }
Don Garrett9fd20a82014-09-04 11:37:22 -0700222 with mock.patch.object(gs.GSContext, 'Exists', return_value=True):
223 urls = pushimage.PushImage('/src', 'test.board', 'R34-5126.0.0',
224 profile='hi')
Don Garrett9459c2f2014-01-22 18:20:24 -0800225
226 self.assertEqual(urls, EXPECTED)
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400227
Amey Deshpande3c487952015-09-03 12:11:13 -0700228 def testBasic_SignTypesEmptyList(self):
229 """Tests PushImage behavior when |sign_types| is empty instead of None.
230
231 As part of the buildbots, PushImage function always receives a tuple for
232 |sign_types| argument. This test checks the behavior for empty tuple.
233 """
234 EXPECTED = {
235 'canary': [
236 ('gs://chromeos-releases/canary-channel/test.board-hi/5126.0.0/'
237 'ChromeOS-recovery-R34-5126.0.0-test.board-hi.instructions')],
238 'dev': [
239 ('gs://chromeos-releases/dev-channel/test.board-hi/5126.0.0/'
240 'ChromeOS-recovery-R34-5126.0.0-test.board-hi.instructions')],
241 }
242 with mock.patch.object(gs.GSContext, 'Exists', return_value=True):
243 urls = pushimage.PushImage('/src', 'test.board', 'R34-5126.0.0',
244 profile='hi', sign_types=())
245
246 self.assertEqual(urls, EXPECTED)
247
Amey Deshpandea936c622015-08-12 17:27:54 -0700248 def testBasic_RealBoardName(self):
249 """Runs a simple smoke test using a real board name."""
250 EXPECTED = {
251 'canary': [
252 ('gs://chromeos-releases/canary-channel/x86-alex/5126.0.0/'
253 'ChromeOS-recovery-R34-5126.0.0-x86-alex.instructions')],
254 'dev': [
255 ('gs://chromeos-releases/dev-channel/x86-alex/5126.0.0/'
256 'ChromeOS-recovery-R34-5126.0.0-x86-alex.instructions')],
257 }
258 with mock.patch.object(gs.GSContext, 'Exists', return_value=True):
259 urls = pushimage.PushImage('/src', 'x86-alex', 'R34-5126.0.0')
260
261 self.assertEqual(urls, EXPECTED)
262
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400263 def testBasicMock(self):
264 """Simple smoke test in mock mode"""
Don Garrett9fd20a82014-09-04 11:37:22 -0700265 with mock.patch.object(gs.GSContext, 'Exists', return_value=True):
266 pushimage.PushImage('/src', 'test.board', 'R34-5126.0.0',
267 dry_run=True, mock=True)
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400268
269 def testBadVersion(self):
270 """Make sure we barf on bad version strings"""
271 self.assertRaises(ValueError, pushimage.PushImage, '', '', 'asdf')
272
273 def testNoInsns(self):
274 """Boards w/out insn files should get skipped"""
Don Garrett9459c2f2014-01-22 18:20:24 -0800275 urls = pushimage.PushImage('/src', 'a bad bad board', 'R34-5126.0.0')
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400276 self.assertEqual(self.gs_mock.call_count, 0)
Don Garrett9459c2f2014-01-22 18:20:24 -0800277 self.assertEqual(urls, None)
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400278
279 def testSignTypesRecovery(self):
280 """Only sign the requested recovery type"""
Don Garrett9459c2f2014-01-22 18:20:24 -0800281 EXPECTED = {
282 'canary': [
Mike Frysingerd6e2df02014-11-26 02:55:04 -0500283 ('gs://chromeos-releases/canary-channel/test.board/5126.0.0/'
284 'ChromeOS-recovery-R34-5126.0.0-test.board.instructions')],
Don Garrett9459c2f2014-01-22 18:20:24 -0800285 'dev': [
Mike Frysingerd6e2df02014-11-26 02:55:04 -0500286 ('gs://chromeos-releases/dev-channel/test.board/5126.0.0/'
287 'ChromeOS-recovery-R34-5126.0.0-test.board.instructions')],
Don Garrett9459c2f2014-01-22 18:20:24 -0800288 }
289
Amey Deshpandea936c622015-08-12 17:27:54 -0700290 with mock.patch.object(gs.GSContext, 'Exists', return_value=True):
291 urls = pushimage.PushImage('/src', 'test.board', 'R34-5126.0.0',
292 sign_types=['recovery'])
Yulan Lin41d2e022022-06-07 22:35:15 +0000293 self.assertEqual(self.gs_mock.call_count, 34)
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400294 self.assertTrue(self.mark_mock.called)
Don Garrett9459c2f2014-01-22 18:20:24 -0800295 self.assertEqual(urls, EXPECTED)
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400296
Amey Deshpandea936c622015-08-12 17:27:54 -0700297 def testSignTypesBase(self):
298 """Only sign the requested recovery type"""
299 EXPECTED = {
300 'canary': [
301 ('gs://chromeos-releases/canary-channel/test.board/5126.0.0/'
302 'ChromeOS-base-R34-5126.0.0-test.board.instructions')],
303 'dev': [
304 ('gs://chromeos-releases/dev-channel/test.board/5126.0.0/'
305 'ChromeOS-base-R34-5126.0.0-test.board.instructions')],
306 }
307
308 with mock.patch.object(gs.GSContext, 'Exists', return_value=True):
309 urls = pushimage.PushImage('/src', 'test.board', 'R34-5126.0.0',
310 sign_types=['base'])
Yulan Lin41d2e022022-06-07 22:35:15 +0000311 self.assertEqual(self.gs_mock.call_count, 36)
LaMont Jones7d6c98f2019-09-27 12:37:33 -0600312 self.assertTrue(self.mark_mock.called)
313 self.assertEqual(urls, EXPECTED)
314
Vadim Bendeburyfe37f282020-11-04 19:05:49 -0800315 def testSignTypesGscFirmware(self):
LaMont Jones7d6c98f2019-09-27 12:37:33 -0600316 """Only sign the requested type"""
317 EXPECTED = {
318 'canary': [
319 ('gs://chromeos-releases/canary-channel/board2/5126.0.0/'
Vadim Bendeburyfe37f282020-11-04 19:05:49 -0800320 'ChromeOS-gsc_firmware-R34-5126.0.0-board2.instructions')],
LaMont Jones7d6c98f2019-09-27 12:37:33 -0600321 'dev': [
322 ('gs://chromeos-releases/dev-channel/board2/5126.0.0/'
Vadim Bendeburyfe37f282020-11-04 19:05:49 -0800323 'ChromeOS-gsc_firmware-R34-5126.0.0-board2.instructions')],
LaMont Jones7d6c98f2019-09-27 12:37:33 -0600324 }
325
326 with mock.patch.object(gs.GSContext, 'Exists', return_value=True):
327 urls = pushimage.PushImage('/src', 'board2', 'R34-5126.0.0',
Vadim Bendeburyfe37f282020-11-04 19:05:49 -0800328 sign_types=['gsc_firmware'])
Yulan Lin41d2e022022-06-07 22:35:15 +0000329 self.assertEqual(self.gs_mock.call_count, 34)
Amey Deshpandea936c622015-08-12 17:27:54 -0700330 self.assertTrue(self.mark_mock.called)
331 self.assertEqual(urls, EXPECTED)
332
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400333 def testSignTypesNone(self):
334 """Verify nothing is signed when we request an unavailable type"""
Don Garrett9459c2f2014-01-22 18:20:24 -0800335 urls = pushimage.PushImage('/src', 'test.board', 'R34-5126.0.0',
336 sign_types=['nononononono'])
Yulan Lin41d2e022022-06-07 22:35:15 +0000337 self.assertEqual(self.gs_mock.call_count, 32)
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400338 self.assertFalse(self.mark_mock.called)
Don Garrett9459c2f2014-01-22 18:20:24 -0800339 self.assertEqual(urls, {})
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400340
Mike Frysinger4495b032014-03-05 17:24:03 -0500341 def testGsError(self):
342 """Verify random GS errors don't make us blow up entirely"""
343 self.gs_mock.AddCmdResult(partial_mock.In('stat'), returncode=1,
344 output='gobblety gook\n')
345 with cros_test_lib.LoggingCapturer('chromite'):
346 self.assertRaises(pushimage.PushError, pushimage.PushImage, '/src',
347 'test.board', 'R34-5126.0.0')
348
Mike Frysingerd84d91e2015-11-05 18:02:24 -0500349 def testMultipleKeysets(self):
350 """Verify behavior when processing an insn w/multiple keysets"""
351 EXPECTED = {
352 'canary': [
353 ('gs://chromeos-releases/canary-channel/test.board/5126.0.0/'
354 'ChromeOS-recovery-R34-5126.0.0-test.board.instructions'),
355 ('gs://chromeos-releases/canary-channel/test.board/5126.0.0/'
356 'ChromeOS-recovery-R34-5126.0.0-test.board-key2.instructions'),
357 ('gs://chromeos-releases/canary-channel/test.board/5126.0.0/'
358 'ChromeOS-recovery-R34-5126.0.0-test.board-key3.instructions'),
359 ],
360 'dev': [
361 ('gs://chromeos-releases/dev-channel/test.board/5126.0.0/'
362 'ChromeOS-recovery-R34-5126.0.0-test.board.instructions'),
363 ('gs://chromeos-releases/dev-channel/test.board/5126.0.0/'
364 'ChromeOS-recovery-R34-5126.0.0-test.board-key2.instructions'),
365 ('gs://chromeos-releases/dev-channel/test.board/5126.0.0/'
366 'ChromeOS-recovery-R34-5126.0.0-test.board-key3.instructions'),
367 ],
368 }
369 with mock.patch.object(gs.GSContext, 'Exists', return_value=True):
370 urls = pushimage.PushImage('/src', 'test.board', 'R34-5126.0.0',
371 force_keysets=('key1', 'key2', 'key3'))
372 self.assertEqual(urls, EXPECTED)
373
Mike Frysinger77912102017-08-30 18:35:46 -0400374 def testForceChannel(self):
375 """Verify behavior when user has specified custom channel"""
376 EXPECTED = {
377 'meep': [
378 ('gs://chromeos-releases/meep-channel/test.board/5126.0.0/'
379 'ChromeOS-recovery-R34-5126.0.0-test.board.instructions'),
380 ],
381 }
382 with mock.patch.object(gs.GSContext, 'Exists', return_value=True):
383 urls = pushimage.PushImage('/src', 'test.board', 'R34-5126.0.0',
384 force_channels=('meep',))
385 self.assertEqual(urls, EXPECTED)
386
Mike Frysinger37ccc2b2015-11-11 17:16:51 -0500387 def testMultipleAltInsns(self):
388 """Verify behavior when processing an insn w/multiple insn overlays"""
389 EXPECTED = {
390 'canary': [
391 ('gs://chromeos-releases/canary-channel/test.multi/1.0.0/'
392 'ChromeOS-recovery-R1-1.0.0-test.multi.instructions'),
393 ('gs://chromeos-releases/canary-channel/test.multi/1.0.0/'
394 'ChromeOS-recovery-R1-1.0.0-test.multi-TwoKeyset.instructions'),
395 ('gs://chromeos-releases/canary-channel/test.multi/1.0.0/'
396 'ChromeOS-recovery-R1-1.0.0-test.multi-ColdKeyset.instructions'),
397 ],
398 'dev': [
399 ('gs://chromeos-releases/dev-channel/test.multi/1.0.0/'
400 'ChromeOS-recovery-R1-1.0.0-test.multi.instructions'),
401 ('gs://chromeos-releases/dev-channel/test.multi/1.0.0/'
402 'ChromeOS-recovery-R1-1.0.0-test.multi-TwoKeyset.instructions'),
403 ('gs://chromeos-releases/dev-channel/test.multi/1.0.0/'
404 'ChromeOS-recovery-R1-1.0.0-test.multi-ColdKeyset.instructions'),
405 ],
406 }
407 with mock.patch.object(gs.GSContext, 'Exists', return_value=True):
408 urls = pushimage.PushImage('/src', 'test.multi', 'R1-1.0.0')
409 self.assertEqual(urls, EXPECTED)
410
Mike Frysingerd13faeb2013-09-05 16:00:46 -0400411
412class MainTests(cros_test_lib.MockTestCase):
413 """Tests for main()"""
414
415 def setUp(self):
416 self.PatchObject(pushimage, 'PushImage')
417
418 def testBasic(self):
419 """Simple smoke test"""
Mike Frysinger09fe0122014-02-09 02:44:05 -0500420 pushimage.main(['--board', 'test.board', '/src', '--yes'])