github.com/hugh712/snapd@v0.0.0-20200910133618-1a99902bd583/overlord/hookstate/ctlcmd/services_test.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2017 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package ctlcmd_test 21 22 import ( 23 "context" 24 "fmt" 25 "sort" 26 27 . "gopkg.in/check.v1" 28 29 "github.com/snapcore/snapd/client" 30 "github.com/snapcore/snapd/dirs" 31 "github.com/snapcore/snapd/overlord/auth" 32 "github.com/snapcore/snapd/overlord/hookstate" 33 "github.com/snapcore/snapd/overlord/hookstate/ctlcmd" 34 "github.com/snapcore/snapd/overlord/hookstate/hooktest" 35 "github.com/snapcore/snapd/overlord/servicestate" 36 "github.com/snapcore/snapd/overlord/snapstate" 37 "github.com/snapcore/snapd/overlord/snapstate/snapstatetest" 38 "github.com/snapcore/snapd/overlord/state" 39 "github.com/snapcore/snapd/snap" 40 "github.com/snapcore/snapd/snap/snaptest" 41 "github.com/snapcore/snapd/store" 42 "github.com/snapcore/snapd/store/storetest" 43 "github.com/snapcore/snapd/systemd" 44 "github.com/snapcore/snapd/testutil" 45 ) 46 47 type fakeStore struct { 48 storetest.Store 49 } 50 51 func (f *fakeStore) SnapAction(_ context.Context, currentSnaps []*store.CurrentSnap, actions []*store.SnapAction, assertQuery store.AssertionQuery, user *auth.UserState, opts *store.RefreshOptions) ([]store.SnapActionResult, []store.AssertionResult, error) { 52 if assertQuery != nil { 53 panic("no assertion query support") 54 } 55 if actions[0].Action == "install" { 56 installs := make([]store.SnapActionResult, 0, len(actions)) 57 for _, a := range actions { 58 snapName, instanceKey := snap.SplitInstanceName(a.InstanceName) 59 if instanceKey != "" { 60 panic(fmt.Sprintf("unexpected instance name %q in snap install action", a.InstanceName)) 61 } 62 63 installs = append(installs, store.SnapActionResult{Info: &snap.Info{ 64 DownloadInfo: snap.DownloadInfo{ 65 Size: 1, 66 }, 67 SideInfo: snap.SideInfo{ 68 RealName: snapName, 69 Revision: snap.R(2), 70 }, 71 Architectures: []string{"all"}, 72 }}) 73 } 74 75 return installs, nil, nil 76 } 77 78 snaps := []store.SnapActionResult{{Info: &snap.Info{ 79 SideInfo: snap.SideInfo{ 80 RealName: "test-snap", 81 Revision: snap.R(2), 82 SnapID: "test-snap-id", 83 }, 84 Architectures: []string{"all"}, 85 }}, {Info: &snap.Info{ 86 SideInfo: snap.SideInfo{ 87 RealName: "other-snap", 88 Revision: snap.R(2), 89 SnapID: "other-snap-id", 90 }, 91 Architectures: []string{"all"}, 92 }}} 93 return snaps, nil, nil 94 } 95 96 type servicectlSuite struct { 97 testutil.BaseTest 98 st *state.State 99 fakeStore fakeStore 100 mockContext *hookstate.Context 101 mockHandler *hooktest.MockHandler 102 } 103 104 var _ = Suite(&servicectlSuite{}) 105 106 const testSnapYaml = `name: test-snap 107 version: 1.0 108 summary: test-snap 109 apps: 110 normal-app: 111 command: bin/dummy 112 test-service: 113 command: bin/service 114 daemon: simple 115 reload-command: bin/reload 116 another-service: 117 command: bin/service 118 daemon: simple 119 reload-command: bin/reload 120 ` 121 122 const otherSnapYaml = `name: other-snap 123 version: 1.0 124 summary: other-snap 125 apps: 126 test-service: 127 command: bin/service 128 daemon: simple 129 reload-command: bin/reload 130 ` 131 132 func mockServiceChangeFunc(testServiceControlInputs func(appInfos []*snap.AppInfo, inst *servicestate.Instruction)) func() { 133 return ctlcmd.MockServicestateControlFunc(func(st *state.State, appInfos []*snap.AppInfo, inst *servicestate.Instruction, context *hookstate.Context) ([]*state.TaskSet, error) { 134 testServiceControlInputs(appInfos, inst) 135 return nil, fmt.Errorf("forced error") 136 }) 137 } 138 139 func (s *servicectlSuite) SetUpTest(c *C) { 140 s.BaseTest.SetUpTest(c) 141 oldRoot := dirs.GlobalRootDir 142 dirs.SetRootDir(c.MkDir()) 143 144 testutil.MockCommand(c, "systemctl", "") 145 146 s.BaseTest.AddCleanup(func() { 147 dirs.SetRootDir(oldRoot) 148 }) 149 s.BaseTest.AddCleanup(snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {})) 150 151 s.mockHandler = hooktest.NewMockHandler() 152 153 s.st = state.New(nil) 154 s.st.Lock() 155 defer s.st.Unlock() 156 157 snapstate.ReplaceStore(s.st, &s.fakeStore) 158 159 // mock installed snaps 160 info1 := snaptest.MockSnapCurrent(c, string(testSnapYaml), &snap.SideInfo{ 161 Revision: snap.R(1), 162 }) 163 info2 := snaptest.MockSnapCurrent(c, string(otherSnapYaml), &snap.SideInfo{ 164 Revision: snap.R(1), 165 }) 166 snapstate.Set(s.st, info1.InstanceName(), &snapstate.SnapState{ 167 Active: true, 168 Sequence: []*snap.SideInfo{ 169 { 170 RealName: info1.SnapName(), 171 Revision: info1.Revision, 172 SnapID: "test-snap-id", 173 }, 174 }, 175 Current: info1.Revision, 176 }) 177 snapstate.Set(s.st, info2.InstanceName(), &snapstate.SnapState{ 178 Active: true, 179 Sequence: []*snap.SideInfo{ 180 { 181 RealName: info2.SnapName(), 182 Revision: info2.Revision, 183 SnapID: "other-snap-id", 184 }, 185 }, 186 Current: info2.Revision, 187 }) 188 189 task := s.st.NewTask("test-task", "my test task") 190 setup := &hookstate.HookSetup{Snap: "test-snap", Revision: snap.R(1), Hook: "test-hook"} 191 192 var err error 193 s.mockContext, err = hookstate.NewContext(task, task.State(), setup, s.mockHandler, "") 194 c.Assert(err, IsNil) 195 196 s.st.Set("seeded", true) 197 s.st.Set("refresh-privacy-key", "privacy-key") 198 s.AddCleanup(snapstatetest.UseFallbackDeviceModel()) 199 } 200 201 func (s *servicectlSuite) TestStopCommand(c *C) { 202 var serviceChangeFuncCalled bool 203 restore := mockServiceChangeFunc(func(appInfos []*snap.AppInfo, inst *servicestate.Instruction) { 204 serviceChangeFuncCalled = true 205 c.Assert(appInfos, HasLen, 1) 206 c.Assert(appInfos[0].Name, Equals, "test-service") 207 c.Assert(inst, DeepEquals, &servicestate.Instruction{ 208 Action: "stop", 209 Names: []string{"test-snap.test-service"}, 210 StopOptions: client.StopOptions{ 211 Disable: false, 212 }, 213 }, 214 ) 215 }) 216 defer restore() 217 _, _, err := ctlcmd.Run(s.mockContext, []string{"stop", "test-snap.test-service"}, 0) 218 c.Assert(err, NotNil) 219 c.Check(err, ErrorMatches, "forced error") 220 c.Assert(serviceChangeFuncCalled, Equals, true) 221 } 222 223 func (s *servicectlSuite) TestStopCommandUnknownService(c *C) { 224 var serviceChangeFuncCalled bool 225 restore := mockServiceChangeFunc(func(appInfos []*snap.AppInfo, inst *servicestate.Instruction) { 226 serviceChangeFuncCalled = true 227 }) 228 defer restore() 229 _, _, err := ctlcmd.Run(s.mockContext, []string{"stop", "test-snap.fooservice"}, 0) 230 c.Assert(err, NotNil) 231 c.Assert(err, ErrorMatches, `unknown service: "test-snap.fooservice"`) 232 c.Assert(serviceChangeFuncCalled, Equals, false) 233 } 234 235 func (s *servicectlSuite) TestStopCommandFailsOnOtherSnap(c *C) { 236 var serviceChangeFuncCalled bool 237 restore := mockServiceChangeFunc(func(appInfos []*snap.AppInfo, inst *servicestate.Instruction) { 238 serviceChangeFuncCalled = true 239 }) 240 defer restore() 241 // verify that snapctl is not allowed to control services of other snaps (only the one of its hook) 242 _, _, err := ctlcmd.Run(s.mockContext, []string{"stop", "other-snap.test-service"}, 0) 243 c.Check(err, NotNil) 244 c.Assert(err, ErrorMatches, `unknown service: "other-snap.test-service"`) 245 c.Assert(serviceChangeFuncCalled, Equals, false) 246 } 247 248 func (s *servicectlSuite) TestStartCommand(c *C) { 249 var serviceChangeFuncCalled bool 250 restore := mockServiceChangeFunc(func(appInfos []*snap.AppInfo, inst *servicestate.Instruction) { 251 serviceChangeFuncCalled = true 252 c.Assert(appInfos, HasLen, 1) 253 c.Assert(appInfos[0].Name, Equals, "test-service") 254 c.Assert(inst, DeepEquals, &servicestate.Instruction{ 255 Action: "start", 256 Names: []string{"test-snap.test-service"}, 257 StartOptions: client.StartOptions{ 258 Enable: false, 259 }, 260 }, 261 ) 262 }) 263 defer restore() 264 _, _, err := ctlcmd.Run(s.mockContext, []string{"start", "test-snap.test-service"}, 0) 265 c.Check(err, NotNil) 266 c.Check(err, ErrorMatches, "forced error") 267 c.Assert(serviceChangeFuncCalled, Equals, true) 268 } 269 270 func (s *servicectlSuite) TestRestartCommand(c *C) { 271 var serviceChangeFuncCalled bool 272 restore := mockServiceChangeFunc(func(appInfos []*snap.AppInfo, inst *servicestate.Instruction) { 273 serviceChangeFuncCalled = true 274 c.Assert(appInfos, HasLen, 1) 275 c.Assert(appInfos[0].Name, Equals, "test-service") 276 c.Assert(inst, DeepEquals, &servicestate.Instruction{ 277 Action: "restart", 278 Names: []string{"test-snap.test-service"}, 279 RestartOptions: client.RestartOptions{ 280 Reload: false, 281 }, 282 }, 283 ) 284 }) 285 defer restore() 286 _, _, err := ctlcmd.Run(s.mockContext, []string{"restart", "test-snap.test-service"}, 0) 287 c.Check(err, NotNil) 288 c.Check(err, ErrorMatches, "forced error") 289 c.Assert(serviceChangeFuncCalled, Equals, true) 290 } 291 292 func (s *servicectlSuite) TestConflictingChange(c *C) { 293 s.st.Lock() 294 task := s.st.NewTask("link-snap", "conflicting task") 295 snapsup := snapstate.SnapSetup{ 296 SideInfo: &snap.SideInfo{ 297 RealName: "test-snap", 298 SnapID: "test-snap-id-1", 299 Revision: snap.R(1), 300 }, 301 } 302 task.Set("snap-setup", snapsup) 303 chg := s.st.NewChange("conflicting change", "install change") 304 chg.AddTask(task) 305 s.st.Unlock() 306 307 _, _, err := ctlcmd.Run(s.mockContext, []string{"start", "test-snap.test-service"}, 0) 308 c.Check(err, NotNil) 309 c.Check(err, ErrorMatches, `snap "test-snap" has "conflicting change" change in progress`) 310 } 311 312 var ( 313 installTaskKinds = []string{ 314 "prerequisites", 315 "download-snap", 316 "validate-snap", 317 "mount-snap", 318 "copy-snap-data", 319 "setup-profiles", 320 "link-snap", 321 "auto-connect", 322 "set-auto-aliases", 323 "setup-aliases", 324 "run-hook[install]", 325 "start-snap-services", 326 "run-hook[configure]", 327 "run-hook[check-health]", 328 } 329 330 refreshTaskKinds = []string{ 331 "prerequisites", 332 "download-snap", 333 "validate-snap", 334 "mount-snap", 335 "run-hook[pre-refresh]", 336 "stop-snap-services", 337 "remove-aliases", 338 "unlink-current-snap", 339 "copy-snap-data", 340 "setup-profiles", 341 "link-snap", 342 "auto-connect", 343 "set-auto-aliases", 344 "setup-aliases", 345 "run-hook[post-refresh]", 346 "start-snap-services", 347 "cleanup", 348 "run-hook[configure]", 349 "run-hook[check-health]", 350 } 351 ) 352 353 func (s *servicectlSuite) TestQueuedCommands(c *C) { 354 s.st.Lock() 355 356 chg := s.st.NewChange("install change", "install change") 357 installed, tts, err := snapstate.InstallMany(s.st, []string{"one", "two"}, 0) 358 c.Assert(err, IsNil) 359 c.Check(installed, DeepEquals, []string{"one", "two"}) 360 c.Assert(tts, HasLen, 2) 361 c.Assert(taskKinds(tts[0].Tasks()), DeepEquals, installTaskKinds) 362 c.Assert(taskKinds(tts[1].Tasks()), DeepEquals, installTaskKinds) 363 chg.AddAll(tts[0]) 364 chg.AddAll(tts[1]) 365 366 s.st.Unlock() 367 368 for _, ts := range tts { 369 tsTasks := ts.Tasks() 370 // assumes configure task is last 371 task := tsTasks[len(tsTasks)-1] 372 c.Assert(task.Kind(), Equals, "run-hook") 373 setup := &hookstate.HookSetup{Snap: "test-snap", Revision: snap.R(1), Hook: "configure"} 374 context, err := hookstate.NewContext(task, task.State(), setup, s.mockHandler, "") 375 c.Assert(err, IsNil) 376 377 _, _, err = ctlcmd.Run(context, []string{"stop", "test-snap.test-service"}, 0) 378 c.Check(err, IsNil) 379 _, _, err = ctlcmd.Run(context, []string{"start", "test-snap.test-service"}, 0) 380 c.Check(err, IsNil) 381 _, _, err = ctlcmd.Run(context, []string{"restart", "test-snap.test-service"}, 0) 382 c.Check(err, IsNil) 383 } 384 385 s.st.Lock() 386 defer s.st.Unlock() 387 388 expectedTaskKinds := append(installTaskKinds, "exec-command", "exec-command", "exec-command") 389 for i := 1; i <= 2; i++ { 390 laneTasks := chg.LaneTasks(i) 391 c.Assert(taskKinds(laneTasks), DeepEquals, expectedTaskKinds) 392 c.Check(laneTasks[12].Summary(), Matches, `Run configure hook of .* snap if present`) 393 c.Check(laneTasks[14].Summary(), Equals, "stop of [test-snap.test-service]") 394 c.Check(laneTasks[15].Summary(), Equals, "start of [test-snap.test-service]") 395 c.Check(laneTasks[16].Summary(), Equals, "restart of [test-snap.test-service]") 396 } 397 } 398 399 func (s *servicectlSuite) testQueueCommandsOrdering(c *C, finalTaskKind string) { 400 s.st.Lock() 401 402 chg := s.st.NewChange("seeding change", "seeding change") 403 finalTask := s.st.NewTask(finalTaskKind, "") 404 chg.AddTask(finalTask) 405 configure := s.st.NewTask("run-hook", "") 406 chg.AddTask(configure) 407 408 s.st.Unlock() 409 410 setup := &hookstate.HookSetup{Snap: "test-snap", Revision: snap.R(1), Hook: "configure"} 411 context, err := hookstate.NewContext(configure, configure.State(), setup, s.mockHandler, "") 412 c.Assert(err, IsNil) 413 414 _, _, err = ctlcmd.Run(context, []string{"stop", "test-snap.test-service"}, 0) 415 c.Check(err, IsNil) 416 _, _, err = ctlcmd.Run(context, []string{"start", "test-snap.test-service"}, 0) 417 c.Check(err, IsNil) 418 419 s.st.Lock() 420 defer s.st.Unlock() 421 422 finalTaskWt := finalTask.WaitTasks() 423 c.Assert(finalTaskWt, HasLen, 2) 424 425 for _, t := range finalTaskWt { 426 // mark-seeded tasks should wait for both exec-command tasks 427 c.Check(t.Kind(), Equals, "exec-command") 428 var argv []string 429 c.Assert(t.Get("argv", &argv), IsNil) 430 c.Check(argv, HasLen, 3) 431 432 commandWt := make(map[string]bool) 433 for _, wt := range t.WaitTasks() { 434 commandWt[wt.Kind()] = true 435 } 436 // exec-command for "stop" should wait for configure hook task, "start" should wait for "stop" and "configure" hook task. 437 switch argv[1] { 438 case "stop": 439 c.Check(commandWt, DeepEquals, map[string]bool{"run-hook": true}) 440 case "start": 441 c.Check(commandWt, DeepEquals, map[string]bool{"run-hook": true, "exec-command": true}) 442 default: 443 c.Fatalf("unexpected command: %q", argv[1]) 444 } 445 } 446 c.Check(finalTask.HaltTasks(), HasLen, 0) 447 } 448 449 func (s *servicectlSuite) TestQueuedCommandsRunBeforeMarkSeeded(c *C) { 450 s.testQueueCommandsOrdering(c, "mark-seeded") 451 } 452 453 func (s *servicectlSuite) TestQueuedCommandsRunBeforeSetModel(c *C) { 454 s.testQueueCommandsOrdering(c, "set-model") 455 } 456 457 func (s *servicectlSuite) TestQueuedCommandsUpdateMany(c *C) { 458 oldAutoAliases := snapstate.AutoAliases 459 snapstate.AutoAliases = func(*state.State, *snap.Info) (map[string]string, error) { 460 return nil, nil 461 } 462 defer func() { snapstate.AutoAliases = oldAutoAliases }() 463 464 s.st.Lock() 465 466 chg := s.st.NewChange("update many change", "update change") 467 installed, tts, err := snapstate.UpdateMany(context.Background(), s.st, []string{"test-snap", "other-snap"}, 0, nil) 468 c.Assert(err, IsNil) 469 sort.Strings(installed) 470 c.Check(installed, DeepEquals, []string{"other-snap", "test-snap"}) 471 c.Assert(tts, HasLen, 3) 472 c.Assert(taskKinds(tts[0].Tasks()), DeepEquals, refreshTaskKinds) 473 c.Assert(taskKinds(tts[1].Tasks()), DeepEquals, refreshTaskKinds) 474 c.Assert(taskKinds(tts[2].Tasks()), DeepEquals, []string{"check-rerefresh"}) 475 c.Assert(tts[2].Tasks()[0].Kind(), Equals, "check-rerefresh") 476 chg.AddAll(tts[0]) 477 chg.AddAll(tts[1]) 478 479 s.st.Unlock() 480 481 for _, ts := range tts[:2] { 482 tsTasks := ts.Tasks() 483 // assumes configure task is last 484 task := tsTasks[len(tsTasks)-1] 485 c.Assert(task.Kind(), Equals, "run-hook") 486 setup := &hookstate.HookSetup{Snap: "test-snap", Revision: snap.R(1), Hook: "configure"} 487 context, err := hookstate.NewContext(task, task.State(), setup, s.mockHandler, "") 488 c.Assert(err, IsNil) 489 490 _, _, err = ctlcmd.Run(context, []string{"stop", "test-snap.test-service"}, 0) 491 c.Check(err, IsNil) 492 _, _, err = ctlcmd.Run(context, []string{"start", "test-snap.test-service"}, 0) 493 c.Check(err, IsNil) 494 _, _, err = ctlcmd.Run(context, []string{"restart", "test-snap.test-service"}, 0) 495 c.Check(err, IsNil) 496 } 497 498 s.st.Lock() 499 defer s.st.Unlock() 500 501 expectedTaskKinds := append(refreshTaskKinds, "exec-command", "exec-command", "exec-command") 502 for i := 1; i <= 2; i++ { 503 laneTasks := chg.LaneTasks(i) 504 c.Assert(taskKinds(laneTasks), DeepEquals, expectedTaskKinds) 505 c.Check(laneTasks[17].Summary(), Matches, `Run configure hook of .* snap if present`) 506 c.Check(laneTasks[19].Summary(), Equals, "stop of [test-snap.test-service]") 507 c.Check(laneTasks[20].Summary(), Equals, "start of [test-snap.test-service]") 508 c.Check(laneTasks[21].Summary(), Equals, "restart of [test-snap.test-service]") 509 } 510 } 511 512 func (s *servicectlSuite) TestQueuedCommandsSingleLane(c *C) { 513 s.st.Lock() 514 515 chg := s.st.NewChange("install change", "install change") 516 ts, err := snapstate.Install(context.Background(), s.st, "one", &snapstate.RevisionOptions{Revision: snap.R(1)}, 0, snapstate.Flags{}) 517 c.Assert(err, IsNil) 518 c.Assert(taskKinds(ts.Tasks()), DeepEquals, installTaskKinds) 519 chg.AddAll(ts) 520 521 s.st.Unlock() 522 523 tsTasks := ts.Tasks() 524 // assumes configure task is last 525 task := tsTasks[len(tsTasks)-1] 526 c.Assert(task.Kind(), Equals, "run-hook") 527 setup := &hookstate.HookSetup{Snap: "test-snap", Revision: snap.R(1), Hook: "configure"} 528 context, err := hookstate.NewContext(task, task.State(), setup, s.mockHandler, "") 529 c.Assert(err, IsNil) 530 531 _, _, err = ctlcmd.Run(context, []string{"stop", "test-snap.test-service"}, 0) 532 c.Check(err, IsNil) 533 _, _, err = ctlcmd.Run(context, []string{"start", "test-snap.test-service"}, 0) 534 c.Check(err, IsNil) 535 _, _, err = ctlcmd.Run(context, []string{"restart", "test-snap.test-service"}, 0) 536 c.Check(err, IsNil) 537 538 s.st.Lock() 539 defer s.st.Unlock() 540 541 laneTasks := chg.LaneTasks(0) 542 c.Assert(taskKinds(laneTasks), DeepEquals, append(installTaskKinds, "exec-command", "exec-command", "exec-command")) 543 c.Check(laneTasks[12].Summary(), Matches, `Run configure hook of .* snap if present`) 544 c.Check(laneTasks[14].Summary(), Equals, "stop of [test-snap.test-service]") 545 c.Check(laneTasks[15].Summary(), Equals, "start of [test-snap.test-service]") 546 c.Check(laneTasks[16].Summary(), Equals, "restart of [test-snap.test-service]") 547 } 548 549 func (s *servicectlSuite) TestTwoServices(c *C) { 550 restore := systemd.MockSystemctl(func(args ...string) (buf []byte, err error) { 551 c.Assert(args[0], Equals, "show") 552 c.Check(args[2], Matches, `snap\.test-snap\.\w+-service\.service`) 553 return []byte(fmt.Sprintf(`Id=%s 554 Type=simple 555 ActiveState=active 556 UnitFileState=enabled 557 `, args[2])), nil 558 }) 559 defer restore() 560 561 stdout, stderr, err := ctlcmd.Run(s.mockContext, []string{"services"}, 0) 562 c.Assert(err, IsNil) 563 c.Check(string(stdout), Equals, ` 564 Service Startup Current Notes 565 test-snap.another-service enabled active - 566 test-snap.test-service enabled active - 567 `[1:]) 568 c.Check(string(stderr), Equals, "") 569 } 570 571 func (s *servicectlSuite) TestServices(c *C) { 572 restore := systemd.MockSystemctl(func(args ...string) (buf []byte, err error) { 573 c.Assert(args[0], Equals, "show") 574 c.Check(args[2], Equals, "snap.test-snap.test-service.service") 575 return []byte(`Id=snap.test-snap.test-service.service 576 Type=simple 577 ActiveState=active 578 UnitFileState=enabled 579 `), nil 580 }) 581 defer restore() 582 583 stdout, stderr, err := ctlcmd.Run(s.mockContext, []string{"services", "test-snap.test-service"}, 0) 584 c.Assert(err, IsNil) 585 c.Check(string(stdout), Equals, ` 586 Service Startup Current Notes 587 test-snap.test-service enabled active - 588 `[1:]) 589 c.Check(string(stderr), Equals, "") 590 }