Coverage for manila/tests/cmd/test_manage.py: 92%

352 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2026-02-18 22:19 +0000

1# Copyright 2015 Mirantis Inc. 

2# All Rights Reserved. 

3# 

4# Licensed under the Apache License, Version 2.0 (the "License"); you may 

5# not use this file except in compliance with the License. You may obtain 

6# a copy of the License at 

7# 

8# http://www.apache.org/licenses/LICENSE-2.0 

9# 

10# Unless required by applicable law or agreed to in writing, software 

11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 

12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 

13# License for the specific language governing permissions and limitations 

14# under the License. 

15 

16import code 

17import io 

18import readline 

19import sys 

20from unittest import mock 

21import yaml 

22 

23import ddt 

24from oslo_config import cfg 

25from oslo_serialization import jsonutils 

26 

27from manila.cmd import manage as manila_manage 

28from manila import context 

29from manila import db 

30from manila.db import migration 

31from manila import test 

32from manila import utils 

33from manila import version 

34 

35CONF = cfg.CONF 

36 

37 

38@ddt.ddt 

39class ManilaCmdManageTestCase(test.TestCase): 

40 def setUp(self): 

41 super(ManilaCmdManageTestCase, self).setUp() 

42 sys.argv = ['manila-share'] 

43 CONF(sys.argv[1:], project='manila', version=version.version_string()) 

44 self.shell_commands = manila_manage.ShellCommands() 

45 self.host_commands = manila_manage.HostCommands() 

46 self.db_commands = manila_manage.DbCommands() 

47 self.version_commands = manila_manage.VersionCommands() 

48 self.config_commands = manila_manage.ConfigCommands() 

49 self.get_log_cmds = manila_manage.GetLogCommands() 

50 self.service_cmds = manila_manage.ServiceCommands() 

51 self.share_cmds = manila_manage.ShareCommands() 

52 self.server_cmds = manila_manage.ShareServerCommands() 

53 self.list_commands = manila_manage.ListCommand() 

54 

55 @mock.patch.object(manila_manage.ShellCommands, 'run', mock.Mock()) 

56 def test_shell_commands_bpython(self): 

57 self.shell_commands.bpython() 

58 manila_manage.ShellCommands.run.assert_called_once_with('bpython') 

59 

60 @mock.patch.object(manila_manage.ShellCommands, 'run', mock.Mock()) 

61 def test_shell_commands_ipython(self): 

62 self.shell_commands.ipython() 

63 manila_manage.ShellCommands.run.assert_called_once_with('ipython') 

64 

65 @mock.patch.object(manila_manage.ShellCommands, 'run', mock.Mock()) 

66 def test_shell_commands_python(self): 

67 self.shell_commands.python() 

68 manila_manage.ShellCommands.run.assert_called_once_with('python') 

69 

70 @ddt.data({}, {'shell': 'bpython'}) 

71 def test_run_bpython(self, kwargs): 

72 try: 

73 import bpython 

74 except ImportError as e: 

75 self.skipTest(str(e)) 

76 self.mock_object(bpython, 'embed') 

77 self.shell_commands.run(**kwargs) 

78 bpython.embed.assert_called_once_with() 

79 

80 def test_run_bpython_import_error(self): 

81 try: 

82 import bpython 

83 import IPython 

84 except ImportError as e: 

85 self.skipTest(str(e)) 

86 self.mock_object(bpython, 'embed', 

87 mock.Mock(side_effect=ImportError())) 

88 self.mock_object(IPython, 'embed') 

89 

90 self.shell_commands.run(shell='bpython') 

91 

92 IPython.embed.assert_called_once_with() 

93 

94 def test_run(self): 

95 try: 

96 import bpython 

97 except ImportError as e: 

98 self.skipTest(str(e)) 

99 self.mock_object(bpython, 'embed') 

100 

101 self.shell_commands.run() 

102 

103 bpython.embed.assert_called_once_with() 

104 

105 def test_run_ipython(self): 

106 try: 

107 import IPython 

108 except ImportError as e: 

109 self.skipTest(str(e)) 

110 self.mock_object(IPython, 'embed') 

111 

112 self.shell_commands.run(shell='ipython') 

113 

114 IPython.embed.assert_called_once_with() 

115 

116 def test_run_ipython_import_error(self): 

117 try: 

118 import IPython 

119 if not hasattr(IPython, 'Shell'): 

120 setattr(IPython, 'Shell', mock.Mock()) 

121 setattr(IPython.Shell, 'IPShell', 

122 mock.Mock(side_effect=ImportError())) 

123 except ImportError as e: 

124 self.skipTest(str(e)) 

125 self.mock_object(IPython, 'embed', 

126 mock.Mock(side_effect=ImportError())) 

127 self.mock_object(readline, 'parse_and_bind') 

128 self.mock_object(code, 'interact') 

129 shell = IPython.embed.return_value 

130 

131 self.shell_commands.run(shell='ipython') 

132 IPython.Shell.IPShell.assert_called_once_with(argv=[]) 

133 self.assertFalse(shell.mainloop.called) 

134 self.assertTrue(readline.parse_and_bind.called) 

135 code.interact.assert_called_once_with() 

136 

137 def test_run_python(self): 

138 self.mock_object(readline, 'parse_and_bind') 

139 self.mock_object(code, 'interact') 

140 

141 self.shell_commands.run(shell='python') 

142 

143 readline.parse_and_bind.assert_called_once_with("tab:complete") 

144 code.interact.assert_called_once_with() 

145 

146 def test_run_python_import_error(self): 

147 self.mock_object(readline, 'parse_and_bind') 

148 self.mock_object(code, 'interact') 

149 

150 self.shell_commands.run(shell='python') 

151 

152 readline.parse_and_bind.assert_called_once_with("tab:complete") 

153 code.interact.assert_called_once_with() 

154 

155 @mock.patch('builtins.print') 

156 def test_list(self, print_mock): 

157 serv_1 = { 

158 'host': 'fake_host1', 

159 'availability_zone': {'name': 'avail_zone1'}, 

160 } 

161 serv_2 = { 

162 'host': 'fake_host2', 

163 'availability_zone': {'name': 'avail_zone2'}, 

164 } 

165 self.mock_object(db, 'service_get_all', 

166 mock.Mock(return_value=[serv_1, serv_2])) 

167 self.mock_object(context, 'get_admin_context', 

168 mock.Mock(return_value='admin_ctxt')) 

169 

170 self.host_commands.list(zone='avail_zone1') 

171 context.get_admin_context.assert_called_once_with() 

172 db.service_get_all.assert_called_once_with('admin_ctxt') 

173 print_mock.assert_has_calls([ 

174 mock.call(u'host \tzone '), 

175 mock.call('fake_host1 \tavail_zone1 ')]) 

176 

177 @mock.patch('builtins.print') 

178 def test_list_zone_is_none(self, print_mock): 

179 serv_1 = { 

180 'host': 'fake_host1', 

181 'availability_zone': {'name': 'avail_zone1'}, 

182 } 

183 serv_2 = { 

184 'host': 'fake_host2', 

185 'availability_zone': {'name': 'avail_zone2'}, 

186 } 

187 self.mock_object(db, 'service_get_all', 

188 mock.Mock(return_value=[serv_1, serv_2])) 

189 self.mock_object(context, 'get_admin_context', 

190 mock.Mock(return_value='admin_ctxt')) 

191 

192 self.host_commands.list() 

193 context.get_admin_context.assert_called_once_with() 

194 db.service_get_all.assert_called_once_with('admin_ctxt') 

195 print_mock.assert_has_calls([ 

196 mock.call(u'host \tzone '), 

197 mock.call('fake_host1 \tavail_zone1 '), 

198 mock.call('fake_host2 \tavail_zone2 ')]) 

199 

200 def test_sync(self): 

201 self.mock_object(migration, 'upgrade') 

202 self.db_commands.sync(version='123') 

203 migration.upgrade.assert_called_once_with('123') 

204 

205 def test_version(self): 

206 self.mock_object(migration, 'version') 

207 self.db_commands.version() 

208 migration.version.assert_called_once_with() 

209 

210 def test_downgrade(self): 

211 self.mock_object(migration, 'downgrade') 

212 self.db_commands.downgrade(version='123') 

213 migration.downgrade.assert_called_once_with('123') 

214 

215 def test_revision(self): 

216 self.mock_object(migration, 'revision') 

217 self.db_commands.revision('message', True) 

218 migration.revision.assert_called_once_with('message', True) 

219 

220 def test_stamp(self): 

221 self.mock_object(migration, 'stamp') 

222 self.db_commands.stamp(version='123') 

223 migration.stamp.assert_called_once_with('123') 

224 

225 def test_version_commands_list(self): 

226 self.mock_object(version, 'version_string', 

227 mock.Mock(return_value='123')) 

228 with mock.patch('sys.stdout', new=io.StringIO()) as fake_out: 

229 self.version_commands.list() 

230 version.version_string.assert_called_once_with() 

231 self.assertEqual('123\n', fake_out.getvalue()) 

232 

233 def test_version_commands_call(self): 

234 self.mock_object(version, 'version_string', 

235 mock.Mock(return_value='123')) 

236 with mock.patch('sys.stdout', new=io.StringIO()) as fake_out: 

237 self.version_commands() 

238 version.version_string.assert_called_once_with() 

239 self.assertEqual('123\n', fake_out.getvalue()) 

240 

241 def test_get_log_commands_no_errors(self): 

242 with mock.patch('sys.stdout', new=io.StringIO()) as fake_out: 

243 CONF.set_override('log_dir', None) 

244 expected_out = 'No errors in logfiles!\n' 

245 

246 self.get_log_cmds.errors() 

247 

248 self.assertEqual(expected_out, fake_out.getvalue()) 

249 

250 @mock.patch('builtins.open') 

251 @mock.patch('os.listdir') 

252 def test_get_log_commands_errors(self, listdir, open): 

253 CONF.set_override('log_dir', 'fake-dir') 

254 listdir.return_value = ['fake-error.log'] 

255 

256 with mock.patch('sys.stdout', new=io.StringIO()) as fake_out: 

257 open.return_value = io.StringIO( 

258 '[ ERROR ] fake-error-message') 

259 expected_out = ('fake-dir/fake-error.log:-\n' 

260 'Line 1 : [ ERROR ] fake-error-message\n') 

261 self.get_log_cmds.errors() 

262 

263 self.assertEqual(expected_out, fake_out.getvalue()) 

264 open.assert_called_once_with('fake-dir/fake-error.log', 'r') 

265 listdir.assert_called_once_with(CONF.log_dir) 

266 

267 @mock.patch('builtins.open') 

268 @mock.patch('os.path.exists') 

269 def test_get_log_commands_syslog_no_log_file(self, path_exists, open): 

270 path_exists.return_value = False 

271 exit = self.assertRaises(SystemExit, self.get_log_cmds.syslog) 

272 self.assertEqual(1, exit.code) 

273 path_exists.assert_any_call('/var/log/syslog') 

274 path_exists.assert_any_call('/var/log/messages') 

275 

276 @mock.patch('manila.utils.service_is_up') 

277 @mock.patch('manila.db.service_get_all') 

278 @mock.patch('manila.context.get_admin_context') 

279 def test_service_commands_list(self, get_admin_context, service_get_all, 

280 service_is_up): 

281 ctxt = context.RequestContext('fake-user', 'fake-project') 

282 get_admin_context.return_value = ctxt 

283 service = {'binary': 'manila-binary', 

284 'host': 'fake-host.fake-domain', 

285 'availability_zone': {'name': 'fake-zone'}, 

286 'updated_at': '2014-06-30 11:22:33', 

287 'disabled': False} 

288 service_get_all.return_value = [service] 

289 service_is_up.return_value = True 

290 with mock.patch('sys.stdout', new=io.StringIO()) as fake_out: 

291 format = "%-16s %-36s %-16s %-10s %-5s %-10s" 

292 print_format = format % ('Binary', 

293 'Host', 

294 'Zone', 

295 'Status', 

296 'State', 

297 'Updated at') 

298 service_format = format % (service['binary'], 

299 service['host'].partition('.')[0], 

300 service['availability_zone']['name'], 

301 'enabled', 

302 ':-)', 

303 service['updated_at']) 

304 expected_out = print_format + '\n' + service_format + '\n' 

305 self.service_cmds.list(format_output='table') 

306 self.assertEqual(expected_out, fake_out.getvalue()) 

307 get_admin_context.assert_called_with() 

308 service_get_all.assert_called_with(ctxt) 

309 service_is_up.assert_called_with(service) 

310 

311 @ddt.data('json', 'yaml') 

312 def test_service_commands_list_format(self, format_output): 

313 ctxt = context.RequestContext('fake-user', 'fake-project') 

314 format_method_name = f'list_{format_output}' 

315 mock_list_method = self.mock_object( 

316 self.service_cmds, format_method_name) 

317 get_admin_context = self.mock_object(context, 'get_admin_context') 

318 service_get_all = self.mock_object(db, 'service_get_all') 

319 service_is_up = self.mock_object(utils, 'service_is_up') 

320 get_admin_context.return_value = ctxt 

321 service = {'binary': 'manila-binary', 

322 'host': 'fake-host.fake-domain', 

323 'availability_zone': {'name': 'fake-zone'}, 

324 'updated_at': '2014-06-30 11:22:33', 

325 'disabled': False} 

326 services = [service] 

327 service_get_all.return_value = services 

328 service_is_up.return_value = True 

329 

330 with mock.patch('sys.stdout', new=io.StringIO()): 

331 self.service_cmds.list(format_output=format_output) 

332 get_admin_context.assert_called_with() 

333 service_get_all.assert_called_with(ctxt) 

334 service_is_up.assert_called_with(service) 

335 service_format = { 

336 'binary': service['binary'], 

337 'host': service['host'].partition('.')[0], 

338 'zone': service['availability_zone']['name'], 

339 'status': 'enabled', 

340 'state': ':-)', 

341 'updated_at': service['updated_at'], 

342 } 

343 mock_list_method.assert_called_once_with( 

344 'services', [service_format]) 

345 

346 @ddt.data(True, False) 

347 def test_service_commands_cleanup(self, service_is_up): 

348 ctxt = context.RequestContext('fake-user', 'fake-project') 

349 self.mock_object(context, 'get_admin_context', 

350 mock.Mock(return_value=ctxt)) 

351 service = {'id': 17, 

352 'binary': 'manila-binary', 

353 'host': 'fake-host.fake-domain', 

354 'availability_zone': {'name': 'fake-zone'}, 

355 'updated_at': '2020-06-17 07:22:33', 

356 'disabled': False} 

357 self.mock_object(db, 'service_get_all', 

358 mock.Mock(return_value=[service])) 

359 self.mock_object(db, 'service_destroy') 

360 self.mock_object(utils, 'service_is_up', 

361 mock.Mock(return_value=service_is_up)) 

362 

363 with mock.patch('sys.stdout', new=io.StringIO()) as fake_out: 

364 if not service_is_up: 

365 expected_out = "Cleaned up service %s" % service['host'] 

366 else: 

367 expected_out = '' 

368 self.service_cmds.cleanup() 

369 

370 self.assertEqual(expected_out, fake_out.getvalue().strip()) 

371 context.get_admin_context.assert_called_with() 

372 db.service_get_all.assert_called_with(ctxt) 

373 utils.service_is_up.assert_called_with(service) 

374 if not service_is_up: 

375 db.service_destroy.assert_called_with(ctxt, service['id']) 

376 else: 

377 self.assertFalse(db.service_destroy.called) 

378 

379 def test_methods_of(self): 

380 obj = type('Fake', (object,), 

381 {name: lambda: 'fake_' for name in ('_a', 'b', 'c')}) 

382 expected = [('b', obj.b), ('c', obj.c)] 

383 self.assertEqual(expected, manila_manage.methods_of(obj)) 

384 

385 @mock.patch('oslo_config.cfg.ConfigOpts.register_cli_opt') 

386 def test_main_argv_lt_2(self, register_cli_opt): 

387 script_name = 'manila-manage' 

388 sys.argv = [script_name] 

389 CONF(sys.argv[1:], project='manila', version=version.version_string()) 

390 exit = self.assertRaises(SystemExit, manila_manage.main) 

391 

392 self.assertTrue(register_cli_opt.called) 

393 self.assertEqual(2, exit.code) 

394 

395 @mock.patch('oslo_config.cfg.ConfigOpts.__call__') 

396 @mock.patch('oslo_log.log.register_options') 

397 @mock.patch('oslo_log.log.setup') 

398 @mock.patch('oslo_config.cfg.ConfigOpts.register_cli_opt') 

399 def test_main_sudo_failed(self, register_cli_opt, log_setup, 

400 register_log_opts, config_opts_call): 

401 script_name = 'manila-manage' 

402 sys.argv = [script_name, 'fake_category', 'fake_action'] 

403 config_opts_call.side_effect = cfg.ConfigFilesNotFoundError( 

404 mock.sentinel._namespace) 

405 

406 exit = self.assertRaises(SystemExit, manila_manage.main) 

407 

408 self.assertTrue(register_cli_opt.called) 

409 register_log_opts.assert_called_once_with(CONF) 

410 config_opts_call.assert_called_once_with( 

411 sys.argv[1:], project='manila', 

412 version=version.version_string()) 

413 self.assertFalse(log_setup.called) 

414 self.assertEqual(2, exit.code) 

415 

416 @mock.patch('oslo_config.cfg.ConfigOpts.__call__') 

417 @mock.patch('oslo_config.cfg.ConfigOpts.register_cli_opt') 

418 @mock.patch('oslo_log.log.register_options') 

419 def test_main(self, register_log_opts, register_cli_opt, config_opts_call): 

420 script_name = 'manila-manage' 

421 sys.argv = [script_name, 'config', 'list'] 

422 action_fn = mock.MagicMock() 

423 CONF.category = mock.MagicMock(action_fn=action_fn) 

424 

425 manila_manage.main() 

426 

427 self.assertTrue(register_cli_opt.called) 

428 register_log_opts.assert_called_once_with(CONF) 

429 config_opts_call.assert_called_once_with( 

430 sys.argv[1:], project='manila', version=version.version_string()) 

431 self.assertTrue(action_fn.called) 

432 

433 @ddt.data('bar', '-bar', '--bar') 

434 def test_get_arg_string(self, arg): 

435 parsed_arg = manila_manage.get_arg_string(arg) 

436 self.assertEqual('bar', parsed_arg) 

437 

438 @ddt.data({'current_host': 'controller-0@fancystore01#pool100', 

439 'new_host': 'controller-0@fancystore01'}, 

440 {'current_host': 'controller-0@fancystore01', 

441 'new_host': 'controller-0'}) 

442 @ddt.unpack 

443 def test_share_update_host_fail_validation(self, current_host, new_host): 

444 self.mock_object(context, 'get_admin_context', 

445 mock.Mock(return_value='admin_ctxt')) 

446 self.mock_object(db, 'share_resources_host_update') 

447 

448 self.assertRaises(SystemExit, 

449 self.share_cmds.update_host, 

450 current_host, new_host) 

451 

452 self.assertFalse(db.share_resources_host_update.called) 

453 

454 @ddt.data({'current_host': 'controller-0@fancystore01#pool100', 

455 'new_host': 'controller-0@fancystore02#pool0'}, 

456 {'current_host': 'controller-0@fancystore01', 

457 'new_host': 'controller-1@fancystore01'}, 

458 {'current_host': 'controller-0', 

459 'new_host': 'controller-1'}, 

460 {'current_host': 'controller-0@fancystore01#pool100', 

461 'new_host': 'controller-1@fancystore02', 'force': True}) 

462 @ddt.unpack 

463 def test_share_update_host(self, current_host, new_host, force=False): 

464 db_op = {'instances': 3, 'groups': 4, 'servers': 2} 

465 self.mock_object(context, 'get_admin_context', 

466 mock.Mock(return_value='admin_ctxt')) 

467 self.mock_object(db, 'share_resources_host_update', 

468 mock.Mock(return_value=db_op)) 

469 

470 with mock.patch('sys.stdout', new=io.StringIO()) as intercepted_op: 

471 self.share_cmds.update_host(current_host, new_host, force) 

472 

473 expected_op = ("Updated host of 3 share instances, 4 share groups and " 

474 "2 share servers on %(chost)s to %(nhost)s." % 

475 {'chost': current_host, 'nhost': new_host}) 

476 self.assertEqual(expected_op, intercepted_op.getvalue().strip()) 

477 db.share_resources_host_update.assert_called_once_with( 

478 'admin_ctxt', current_host, new_host) 

479 

480 def test_share_delete(self): 

481 share_id = "fake_share_id" 

482 share = { 

483 'id': share_id, 

484 'instances': [ 

485 {'id': 'instance_id1', 'replica_state': 'active'}, 

486 {'id': 'instance_id2', 'replica_state': 'error'}, 

487 {'id': 'instance_id3', 'replica_state': 'active'}, 

488 ] 

489 } 

490 self.mock_object(context, 'get_admin_context', 

491 mock.Mock(return_value='admin_ctxt')) 

492 self.mock_object(db, 'share_get', 

493 mock.Mock(return_value=share)) 

494 self.mock_object(db, 'share_instance_delete', 

495 mock.Mock(return_value=None)) 

496 

497 self.share_cmds.delete(share_id) 

498 

499 db.share_instance_delete.assert_has_calls([ 

500 mock.call('admin_ctxt', 'instance_id2'), 

501 mock.call('admin_ctxt', 'instance_id1'), 

502 mock.call('admin_ctxt', 'instance_id3'), 

503 ]) 

504 

505 self.assertEqual(3, db.share_instance_delete.call_count) 

506 

507 def test_share_server_update_capability(self): 

508 self.mock_object(context, 'get_admin_context', 

509 mock.Mock(return_value='admin_ctxt')) 

510 self.mock_object(db, 'share_servers_update') 

511 share_servers = 'server_id_a,server_id_b' 

512 share_server_list = [server.strip() 

513 for server in share_servers.split(",")] 

514 capabilities = "security_service_update_support" \ 

515 ",network_allocation_update_support" 

516 capabilities_list = capabilities.split(",") 

517 values_to_update = [ 

518 {capabilities_list[0]: True, 

519 capabilities_list[1]: True}] 

520 

521 with mock.patch('sys.stdout', new=io.StringIO()) as output: 

522 self.server_cmds.update_share_server_capabilities( 

523 share_servers, capabilities, True) 

524 

525 expected_op = ("The capability(ies) %(cap)s of the following share " 

526 "server(s) %(servers)s was(were) updated to " 

527 "%(value)s.") % { 

528 'cap': capabilities_list, 

529 'servers': share_server_list, 

530 'value': True, 

531 } 

532 

533 self.assertEqual(expected_op, output.getvalue().strip()) 

534 db.share_servers_update.assert_called_once_with( 

535 'admin_ctxt', share_server_list, values_to_update[0]) 

536 

537 def test_share_server_update_capability_not_supported(self): 

538 share_servers = 'server_id_a' 

539 capabilities = 'invalid_capability' 

540 

541 exit = self.assertRaises( 

542 SystemExit, 

543 self.server_cmds.update_share_server_capabilities, 

544 share_servers, 

545 capabilities, 

546 True) 

547 

548 self.assertEqual(1, exit.code) 

549 

550 @mock.patch('builtins.print') 

551 def test_list_commands_json(self, mock_print): 

552 resource_name = 'service' 

553 service_format = [{ 

554 'binary': 'manila-binary', 

555 'host': 'fake-host', 

556 'availability_zone': 'fakeaz', 

557 'status': 'enabled', 

558 'state': ':-)', 

559 'updated_at': '13 04:57:49 PM -03 2023' 

560 }] 

561 

562 mock_json_dumps = self.mock_object( 

563 jsonutils, 'dumps', mock.Mock(return_value=service_format[0])) 

564 services = {resource_name: service_format} 

565 

566 self.list_commands.list_json('service', service_format) 

567 

568 mock_json_dumps.assert_called_once_with( 

569 services, indent=4) 

570 mock_print.assert_called_once_with(service_format[0]) 

571 

572 @mock.patch('builtins.print') 

573 def test_list_commands_yaml(self, mock_print): 

574 resource_name = 'service' 

575 service_format = [{ 

576 'binary': 'manila-binary', 

577 'host': 'fake-host', 

578 'availability_zone': 'fakeaz', 

579 'status': 'enabled', 

580 'state': ':-)', 

581 'updated_at': '13 04:57:49 PM -03 2023' 

582 }] 

583 

584 mock_yaml_dump = self.mock_object( 

585 yaml, 'dump', mock.Mock(return_value=service_format[0])) 

586 services = {resource_name: service_format} 

587 

588 self.list_commands.list_yaml('service', service_format) 

589 

590 mock_yaml_dump.assert_called_once_with( 

591 services) 

592 mock_print.assert_called_once_with(service_format[0])