github.com/anonymouse64/snapd@v0.0.0-20210824153203-04c4c42d842d/daemon/api_quotas_test.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2021 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 daemon_test 21 22 import ( 23 "bytes" 24 "encoding/json" 25 "fmt" 26 "net/http" 27 "net/http/httptest" 28 29 "gopkg.in/check.v1" 30 31 "github.com/snapcore/snapd/client" 32 "github.com/snapcore/snapd/daemon" 33 "github.com/snapcore/snapd/gadget/quantity" 34 "github.com/snapcore/snapd/overlord/configstate/config" 35 "github.com/snapcore/snapd/overlord/servicestate" 36 "github.com/snapcore/snapd/overlord/servicestate/servicestatetest" 37 "github.com/snapcore/snapd/overlord/snapstate" 38 "github.com/snapcore/snapd/overlord/state" 39 "github.com/snapcore/snapd/snap/quota" 40 ) 41 42 var _ = check.Suite(&apiQuotaSuite{}) 43 44 type apiQuotaSuite struct { 45 apiBaseSuite 46 47 ensureSoonCalled int 48 } 49 50 func (s *apiQuotaSuite) SetUpTest(c *check.C) { 51 s.apiBaseSuite.SetUpTest(c) 52 s.daemon(c) 53 54 st := s.d.Overlord().State() 55 st.Lock() 56 defer st.Unlock() 57 tr := config.NewTransaction(st) 58 tr.Set("core", "experimental.quota-groups", true) 59 tr.Commit() 60 61 r := servicestate.MockSystemdVersion(248) 62 s.AddCleanup(r) 63 64 // POST requires root 65 s.expectedWriteAccess = daemon.RootAccess{} 66 67 s.ensureSoonCalled = 0 68 _, r = daemon.MockEnsureStateSoon(func(st *state.State) { 69 s.ensureSoonCalled++ 70 }) 71 s.AddCleanup(r) 72 } 73 74 func mockQuotas(st *state.State, c *check.C) { 75 err := servicestatetest.MockQuotaInState(st, "foo", "", nil, 11000) 76 c.Assert(err, check.IsNil) 77 err = servicestatetest.MockQuotaInState(st, "bar", "foo", nil, 6000) 78 c.Assert(err, check.IsNil) 79 err = servicestatetest.MockQuotaInState(st, "baz", "foo", nil, 5000) 80 c.Assert(err, check.IsNil) 81 } 82 83 func (s *apiQuotaSuite) TestPostQuotaUnknownAction(c *check.C) { 84 data, err := json.Marshal(daemon.PostQuotaGroupData{Action: "foo", GroupName: "bar"}) 85 c.Assert(err, check.IsNil) 86 87 req, err := http.NewRequest("POST", "/v2/quotas", bytes.NewBuffer(data)) 88 c.Assert(err, check.IsNil) 89 rspe := s.errorReq(c, req, nil) 90 c.Assert(rspe.Status, check.Equals, 400) 91 c.Check(rspe.Message, check.Equals, `unknown quota action "foo"`) 92 } 93 94 func (s *apiQuotaSuite) TestPostQuotaInvalidGroupName(c *check.C) { 95 data, err := json.Marshal(daemon.PostQuotaGroupData{Action: "ensure", GroupName: "$$$"}) 96 c.Assert(err, check.IsNil) 97 98 req, err := http.NewRequest("POST", "/v2/quotas", bytes.NewBuffer(data)) 99 c.Assert(err, check.IsNil) 100 rspe := s.errorReq(c, req, nil) 101 c.Assert(rspe.Status, check.Equals, 400) 102 c.Check(rspe.Message, check.Matches, `invalid quota group name: .*`) 103 } 104 105 func (s *apiQuotaSuite) TestPostEnsureQuotaUnhappy(c *check.C) { 106 r := daemon.MockServicestateCreateQuota(func(st *state.State, name string, parentName string, snaps []string, memoryLimit quantity.Size) (*state.TaskSet, error) { 107 c.Check(name, check.Equals, "booze") 108 c.Check(parentName, check.Equals, "foo") 109 c.Check(snaps, check.DeepEquals, []string{"bar"}) 110 c.Check(memoryLimit, check.DeepEquals, quantity.Size(1000)) 111 return nil, fmt.Errorf("boom") 112 }) 113 defer r() 114 115 data, err := json.Marshal(daemon.PostQuotaGroupData{ 116 Action: "ensure", 117 GroupName: "booze", 118 Parent: "foo", 119 Snaps: []string{"bar"}, 120 Constraints: client.QuotaValues{Memory: quantity.Size(1000)}, 121 }) 122 c.Assert(err, check.IsNil) 123 124 req, err := http.NewRequest("POST", "/v2/quotas", bytes.NewBuffer(data)) 125 c.Assert(err, check.IsNil) 126 rspe := s.errorReq(c, req, nil) 127 c.Check(rspe.Status, check.Equals, 400) 128 c.Check(rspe.Message, check.Matches, `cannot create quota group: boom`) 129 c.Assert(s.ensureSoonCalled, check.Equals, 0) 130 } 131 132 func (s *apiQuotaSuite) TestPostEnsureQuotaCreateHappy(c *check.C) { 133 var createCalled int 134 r := daemon.MockServicestateCreateQuota(func(st *state.State, name string, parentName string, snaps []string, memoryLimit quantity.Size) (*state.TaskSet, error) { 135 createCalled++ 136 c.Check(name, check.Equals, "booze") 137 c.Check(parentName, check.Equals, "foo") 138 c.Check(snaps, check.DeepEquals, []string{"some-snap"}) 139 c.Check(memoryLimit, check.DeepEquals, quantity.Size(1000)) 140 ts := state.NewTaskSet(st.NewTask("foo-quota", "...")) 141 return ts, nil 142 }) 143 defer r() 144 145 data, err := json.Marshal(daemon.PostQuotaGroupData{ 146 Action: "ensure", 147 GroupName: "booze", 148 Parent: "foo", 149 Snaps: []string{"some-snap"}, 150 Constraints: client.QuotaValues{Memory: quantity.Size(1000)}, 151 }) 152 c.Assert(err, check.IsNil) 153 154 req, err := http.NewRequest("POST", "/v2/quotas", bytes.NewBuffer(data)) 155 c.Assert(err, check.IsNil) 156 rsp := s.asyncReq(c, req, nil) 157 c.Assert(rsp.Status, check.Equals, 202) 158 c.Assert(createCalled, check.Equals, 1) 159 c.Assert(s.ensureSoonCalled, check.Equals, 1) 160 } 161 162 func (s *apiQuotaSuite) TestPostEnsureQuotaCreateQuotaConflicts(c *check.C) { 163 var createCalled int 164 r := daemon.MockServicestateCreateQuota(func(st *state.State, name string, parentName string, snaps []string, memoryLimit quantity.Size) (*state.TaskSet, error) { 165 c.Check(name, check.Equals, "booze") 166 c.Check(parentName, check.Equals, "foo") 167 c.Check(snaps, check.DeepEquals, []string{"some-snap"}) 168 c.Check(memoryLimit, check.DeepEquals, quantity.Size(1000)) 169 170 createCalled++ 171 switch createCalled { 172 case 1: 173 // return a quota conflict as if we were trying to create this quota in 174 // another task 175 return nil, &servicestate.QuotaChangeConflictError{Quota: "booze", ChangeKind: "quota-control"} 176 case 2: 177 // return a snap conflict as if we were trying to disable the 178 // some-snap in the quota group to be created 179 return nil, &snapstate.ChangeConflictError{Snap: "some-snap", ChangeKind: "disable"} 180 default: 181 c.Errorf("test broken") 182 return nil, fmt.Errorf("test broken") 183 } 184 }) 185 defer r() 186 187 data, err := json.Marshal(daemon.PostQuotaGroupData{ 188 Action: "ensure", 189 GroupName: "booze", 190 Parent: "foo", 191 Snaps: []string{"some-snap"}, 192 Constraints: client.QuotaValues{Memory: 1000}, 193 }) 194 c.Assert(err, check.IsNil) 195 196 req, err := http.NewRequest("POST", "/v2/quotas", bytes.NewBuffer(data)) 197 c.Assert(err, check.IsNil) 198 rspe := s.errorReq(c, req, nil) 199 c.Assert(rspe.Status, check.Equals, 409) 200 c.Check(rspe.Message, check.Equals, `quota group "booze" has "quota-control" change in progress`) 201 c.Check(rspe.Value, check.DeepEquals, map[string]interface{}{ 202 "change-kind": "quota-control", 203 "quota-name": "booze", 204 }) 205 206 req, err = http.NewRequest("POST", "/v2/quotas", bytes.NewBuffer(data)) 207 c.Assert(err, check.IsNil) 208 209 rspe = s.errorReq(c, req, nil) 210 c.Assert(rspe.Status, check.Equals, 409) 211 c.Check(rspe.Message, check.Equals, `snap "some-snap" has "disable" change in progress`) 212 c.Check(rspe.Value, check.DeepEquals, map[string]interface{}{ 213 "change-kind": "disable", 214 "snap-name": "some-snap", 215 }) 216 217 c.Assert(createCalled, check.Equals, 2) 218 } 219 220 func (s *apiQuotaSuite) TestPostEnsureQuotaUpdateHappy(c *check.C) { 221 st := s.d.Overlord().State() 222 st.Lock() 223 err := servicestatetest.MockQuotaInState(st, "ginger-ale", "", nil, 5000) 224 st.Unlock() 225 c.Assert(err, check.IsNil) 226 227 r := daemon.MockServicestateCreateQuota(func(st *state.State, name string, parentName string, snaps []string, memoryLimit quantity.Size) (*state.TaskSet, error) { 228 c.Errorf("should not have called create quota") 229 return nil, fmt.Errorf("broken test") 230 }) 231 defer r() 232 233 updateCalled := 0 234 r = daemon.MockServicestateUpdateQuota(func(st *state.State, name string, opts servicestate.QuotaGroupUpdate) (*state.TaskSet, error) { 235 updateCalled++ 236 c.Assert(name, check.Equals, "ginger-ale") 237 c.Assert(opts, check.DeepEquals, servicestate.QuotaGroupUpdate{ 238 AddSnaps: []string{"some-snap"}, 239 NewMemoryLimit: 9000, 240 }) 241 ts := state.NewTaskSet(st.NewTask("foo-quota", "...")) 242 return ts, nil 243 }) 244 defer r() 245 246 data, err := json.Marshal(daemon.PostQuotaGroupData{ 247 Action: "ensure", 248 GroupName: "ginger-ale", 249 Snaps: []string{"some-snap"}, 250 Constraints: client.QuotaValues{Memory: quantity.Size(9000)}, 251 }) 252 c.Assert(err, check.IsNil) 253 254 req, err := http.NewRequest("POST", "/v2/quotas", bytes.NewBuffer(data)) 255 c.Assert(err, check.IsNil) 256 rsp := s.asyncReq(c, req, nil) 257 c.Assert(rsp.Status, check.Equals, 202) 258 c.Assert(updateCalled, check.Equals, 1) 259 c.Assert(s.ensureSoonCalled, check.Equals, 1) 260 } 261 262 func (s *apiQuotaSuite) TestPostEnsureQuotaUpdateConflicts(c *check.C) { 263 st := s.d.Overlord().State() 264 st.Lock() 265 err := servicestatetest.MockQuotaInState(st, "ginger-ale", "", nil, 5000) 266 st.Unlock() 267 c.Assert(err, check.IsNil) 268 269 r := daemon.MockServicestateCreateQuota(func(st *state.State, name string, parentName string, snaps []string, memoryLimit quantity.Size) (*state.TaskSet, error) { 270 c.Errorf("should not have called create quota") 271 return nil, fmt.Errorf("broken test") 272 }) 273 defer r() 274 275 updateCalled := 0 276 r = daemon.MockServicestateUpdateQuota(func(st *state.State, name string, opts servicestate.QuotaGroupUpdate) (*state.TaskSet, error) { 277 updateCalled++ 278 c.Assert(name, check.Equals, "ginger-ale") 279 c.Assert(opts, check.DeepEquals, servicestate.QuotaGroupUpdate{ 280 AddSnaps: []string{"some-snap"}, 281 NewMemoryLimit: 9000, 282 }) 283 switch updateCalled { 284 case 1: 285 // return a quota conflict as if we were trying to update this quota 286 // in another task 287 return nil, &servicestate.QuotaChangeConflictError{Quota: "ginger-ale", ChangeKind: "quota-control"} 288 case 2: 289 // return a snap conflict as if we were trying to disable the 290 // some-snap in the quota group to be added to the group 291 return nil, &snapstate.ChangeConflictError{Snap: "some-snap", ChangeKind: "disable"} 292 default: 293 c.Errorf("test broken") 294 return nil, fmt.Errorf("test broken") 295 } 296 }) 297 defer r() 298 299 data, err := json.Marshal(daemon.PostQuotaGroupData{ 300 Action: "ensure", 301 GroupName: "ginger-ale", 302 Snaps: []string{"some-snap"}, 303 Constraints: client.QuotaValues{Memory: 9000}, 304 }) 305 c.Assert(err, check.IsNil) 306 307 req, err := http.NewRequest("POST", "/v2/quotas", bytes.NewBuffer(data)) 308 c.Assert(err, check.IsNil) 309 rspe := s.errorReq(c, req, nil) 310 c.Assert(rspe.Status, check.Equals, 409) 311 c.Check(rspe.Message, check.Equals, `quota group "ginger-ale" has "quota-control" change in progress`) 312 c.Check(rspe.Value, check.DeepEquals, map[string]interface{}{ 313 "change-kind": "quota-control", 314 "quota-name": "ginger-ale", 315 }) 316 317 req, err = http.NewRequest("POST", "/v2/quotas", bytes.NewBuffer(data)) 318 c.Assert(err, check.IsNil) 319 320 rspe = s.errorReq(c, req, nil) 321 c.Assert(rspe.Status, check.Equals, 409) 322 c.Check(rspe.Message, check.Equals, `snap "some-snap" has "disable" change in progress`) 323 c.Check(rspe.Value, check.DeepEquals, map[string]interface{}{ 324 "change-kind": "disable", 325 "snap-name": "some-snap", 326 }) 327 328 c.Assert(updateCalled, check.Equals, 2) 329 } 330 331 func (s *apiQuotaSuite) TestPostRemoveQuotaHappy(c *check.C) { 332 var removeCalled int 333 r := daemon.MockServicestateRemoveQuota(func(st *state.State, name string) (*state.TaskSet, error) { 334 removeCalled++ 335 c.Check(name, check.Equals, "booze") 336 ts := state.NewTaskSet(st.NewTask("foo-quota", "...")) 337 return ts, nil 338 }) 339 defer r() 340 341 data, err := json.Marshal(daemon.PostQuotaGroupData{ 342 Action: "remove", 343 GroupName: "booze", 344 }) 345 c.Assert(err, check.IsNil) 346 347 req, err := http.NewRequest("POST", "/v2/quotas", bytes.NewBuffer(data)) 348 c.Assert(err, check.IsNil) 349 s.asRootAuth(req) 350 351 rec := httptest.NewRecorder() 352 s.serveHTTP(c, rec, req) 353 c.Assert(rec.Code, check.Equals, 202) 354 c.Assert(removeCalled, check.Equals, 1) 355 c.Assert(s.ensureSoonCalled, check.Equals, 1) 356 } 357 358 func (s *apiQuotaSuite) TestPostRemoveQuotaConflict(c *check.C) { 359 st := s.d.Overlord().State() 360 st.Lock() 361 err := servicestatetest.MockQuotaInState(st, "ginger-ale", "", []string{"some-snap"}, 5000) 362 st.Unlock() 363 c.Assert(err, check.IsNil) 364 365 var removeCalled int 366 r := daemon.MockServicestateRemoveQuota(func(st *state.State, name string) (*state.TaskSet, error) { 367 removeCalled++ 368 c.Check(name, check.Equals, "booze") 369 switch removeCalled { 370 case 1: 371 // return a quota conflict as if we were trying to update this quota 372 // in another task 373 return nil, &servicestate.QuotaChangeConflictError{Quota: "booze", ChangeKind: "quota-control"} 374 case 2: 375 // return a snap conflict as if we were trying to disable the 376 // some-snap in the quota group to be added to the group 377 return nil, &snapstate.ChangeConflictError{Snap: "some-snap", ChangeKind: "disable"} 378 default: 379 c.Errorf("test broken") 380 return nil, fmt.Errorf("test broken") 381 } 382 }) 383 defer r() 384 385 data, err := json.Marshal(daemon.PostQuotaGroupData{ 386 Action: "remove", 387 GroupName: "booze", 388 }) 389 c.Assert(err, check.IsNil) 390 391 req, err := http.NewRequest("POST", "/v2/quotas", bytes.NewBuffer(data)) 392 c.Assert(err, check.IsNil) 393 rspe := s.errorReq(c, req, nil) 394 c.Assert(rspe.Status, check.Equals, 409) 395 c.Check(rspe.Message, check.Equals, `quota group "booze" has "quota-control" change in progress`) 396 c.Check(rspe.Value, check.DeepEquals, map[string]interface{}{ 397 "change-kind": "quota-control", 398 "quota-name": "booze", 399 }) 400 401 req, err = http.NewRequest("POST", "/v2/quotas", bytes.NewBuffer(data)) 402 c.Assert(err, check.IsNil) 403 404 rspe = s.errorReq(c, req, nil) 405 c.Assert(rspe.Status, check.Equals, 409) 406 c.Check(rspe.Message, check.Equals, `snap "some-snap" has "disable" change in progress`) 407 c.Check(rspe.Value, check.DeepEquals, map[string]interface{}{ 408 "change-kind": "disable", 409 "snap-name": "some-snap", 410 }) 411 412 c.Assert(removeCalled, check.Equals, 2) 413 } 414 415 func (s *apiQuotaSuite) TestPostRemoveQuotaUnhappy(c *check.C) { 416 r := daemon.MockServicestateRemoveQuota(func(st *state.State, name string) (*state.TaskSet, error) { 417 c.Check(name, check.Equals, "booze") 418 return nil, fmt.Errorf("boom") 419 }) 420 defer r() 421 422 data, err := json.Marshal(daemon.PostQuotaGroupData{ 423 Action: "remove", 424 GroupName: "booze", 425 }) 426 c.Assert(err, check.IsNil) 427 428 req, err := http.NewRequest("POST", "/v2/quotas", bytes.NewBuffer(data)) 429 c.Assert(err, check.IsNil) 430 rspe := s.errorReq(c, req, nil) 431 c.Check(rspe.Status, check.Equals, 400) 432 c.Check(rspe.Message, check.Matches, `cannot remove quota group: boom`) 433 c.Check(s.ensureSoonCalled, check.Equals, 0) 434 } 435 436 func (s *apiQuotaSuite) TestPostQuotaRequiresRoot(c *check.C) { 437 r := daemon.MockServicestateRemoveQuota(func(st *state.State, name string) (*state.TaskSet, error) { 438 c.Fatalf("remove quota should not get called") 439 return nil, fmt.Errorf("broken test") 440 }) 441 defer r() 442 443 data, err := json.Marshal(daemon.PostQuotaGroupData{ 444 Action: "remove", 445 GroupName: "booze", 446 }) 447 c.Assert(err, check.IsNil) 448 449 req, err := http.NewRequest("POST", "/v2/quotas", bytes.NewBuffer(data)) 450 c.Assert(err, check.IsNil) 451 s.asUserAuth(c, req) 452 453 rec := httptest.NewRecorder() 454 s.serveHTTP(c, rec, req) 455 c.Check(rec.Code, check.Equals, 403) 456 c.Check(s.ensureSoonCalled, check.Equals, 0) 457 } 458 459 func (s *apiQuotaSuite) TestListQuotas(c *check.C) { 460 st := s.d.Overlord().State() 461 st.Lock() 462 mockQuotas(st, c) 463 st.Unlock() 464 465 calls := 0 466 r := daemon.MockGetQuotaMemUsage(func(grp *quota.Group) (quantity.Size, error) { 467 calls++ 468 switch grp.Name { 469 case "bar": 470 return quantity.Size(500), nil 471 case "baz": 472 return quantity.Size(1000), nil 473 case "foo": 474 return quantity.Size(5000), nil 475 default: 476 c.Errorf("unexpected call to get group memory usage for group %q", grp.Name) 477 return 0, fmt.Errorf("broken test") 478 } 479 }) 480 defer r() 481 defer func() { 482 c.Assert(calls, check.Equals, 3) 483 }() 484 485 req, err := http.NewRequest("GET", "/v2/quotas", nil) 486 c.Assert(err, check.IsNil) 487 rsp := s.syncReq(c, req, nil) 488 c.Assert(rsp.Status, check.Equals, 200) 489 c.Assert(rsp.Result, check.FitsTypeOf, []client.QuotaGroupResult{}) 490 res := rsp.Result.([]client.QuotaGroupResult) 491 c.Check(res, check.DeepEquals, []client.QuotaGroupResult{ 492 { 493 GroupName: "bar", 494 Parent: "foo", 495 Constraints: &client.QuotaValues{Memory: quantity.Size(6000)}, 496 Current: &client.QuotaValues{Memory: quantity.Size(500)}, 497 }, 498 { 499 GroupName: "baz", 500 Parent: "foo", 501 Constraints: &client.QuotaValues{Memory: quantity.Size(5000)}, 502 Current: &client.QuotaValues{Memory: quantity.Size(1000)}, 503 }, 504 { 505 GroupName: "foo", 506 Subgroups: []string{"bar", "baz"}, 507 Constraints: &client.QuotaValues{Memory: quantity.Size(11000)}, 508 Current: &client.QuotaValues{Memory: quantity.Size(5000)}, 509 }, 510 }) 511 c.Check(s.ensureSoonCalled, check.Equals, 0) 512 } 513 514 func (s *apiQuotaSuite) TestGetQuota(c *check.C) { 515 st := s.d.Overlord().State() 516 st.Lock() 517 mockQuotas(st, c) 518 st.Unlock() 519 520 calls := 0 521 r := daemon.MockGetQuotaMemUsage(func(grp *quota.Group) (quantity.Size, error) { 522 calls++ 523 c.Assert(grp.Name, check.Equals, "bar") 524 return quantity.Size(500), nil 525 }) 526 defer r() 527 defer func() { 528 c.Assert(calls, check.Equals, 1) 529 }() 530 531 req, err := http.NewRequest("GET", "/v2/quotas/bar", nil) 532 c.Assert(err, check.IsNil) 533 rsp := s.syncReq(c, req, nil) 534 c.Assert(rsp.Status, check.Equals, 200) 535 c.Assert(rsp.Result, check.FitsTypeOf, client.QuotaGroupResult{}) 536 res := rsp.Result.(client.QuotaGroupResult) 537 c.Check(res, check.DeepEquals, client.QuotaGroupResult{ 538 GroupName: "bar", 539 Parent: "foo", 540 Constraints: &client.QuotaValues{Memory: quantity.Size(6000)}, 541 Current: &client.QuotaValues{Memory: quantity.Size(500)}, 542 }) 543 544 c.Check(s.ensureSoonCalled, check.Equals, 0) 545 } 546 547 func (s *apiQuotaSuite) TestGetQuotaInvalidName(c *check.C) { 548 st := s.d.Overlord().State() 549 st.Lock() 550 mockQuotas(st, c) 551 st.Unlock() 552 553 req, err := http.NewRequest("GET", "/v2/quotas/000", nil) 554 c.Assert(err, check.IsNil) 555 rspe := s.errorReq(c, req, nil) 556 c.Check(rspe.Status, check.Equals, 400) 557 c.Check(rspe.Message, check.Matches, `invalid quota group name: .*`) 558 c.Check(s.ensureSoonCalled, check.Equals, 0) 559 } 560 561 func (s *apiQuotaSuite) TestGetQuotaNotFound(c *check.C) { 562 req, err := http.NewRequest("GET", "/v2/quotas/unknown", nil) 563 c.Assert(err, check.IsNil) 564 rspe := s.errorReq(c, req, nil) 565 c.Check(rspe.Status, check.Equals, 404) 566 c.Check(rspe.Message, check.Matches, `cannot find quota group "unknown"`) 567 c.Check(s.ensureSoonCalled, check.Equals, 0) 568 }