github.com/hugh712/snapd@v0.0.0-20200910133618-1a99902bd583/overlord/configstate/config/transaction_test.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2016 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 config_test 21 22 import ( 23 "bytes" 24 "encoding/json" 25 "fmt" 26 "strings" 27 "testing" 28 29 . "gopkg.in/check.v1" 30 31 "github.com/snapcore/snapd/jsonutil" 32 "github.com/snapcore/snapd/overlord/configstate/config" 33 "github.com/snapcore/snapd/overlord/state" 34 ) 35 36 func TestT(t *testing.T) { TestingT(t) } 37 38 type transactionSuite struct { 39 state *state.State 40 transaction *config.Transaction 41 } 42 43 var _ = Suite(&transactionSuite{}) 44 45 func (s *transactionSuite) SetUpTest(c *C) { 46 s.state = state.New(nil) 47 s.state.Lock() 48 defer s.state.Unlock() 49 s.transaction = config.NewTransaction(s.state) 50 } 51 52 type setGetOp string 53 54 func (op setGetOp) kind() string { 55 return strings.Fields(string(op))[0] 56 } 57 58 func (op setGetOp) list() []string { 59 args := strings.Fields(string(op)) 60 return args[1:] 61 } 62 63 func (op setGetOp) args() map[string]interface{} { 64 m := make(map[string]interface{}) 65 args := strings.Fields(string(op)) 66 for _, pair := range args[1:] { 67 if pair == "=>" { 68 break 69 } 70 kv := strings.SplitN(pair, "=", 2) 71 var v interface{} 72 if err := jsonutil.DecodeWithNumber(strings.NewReader(kv[1]), &v); err != nil { 73 v = kv[1] 74 } 75 m[kv[0]] = v 76 } 77 return m 78 } 79 80 func (op setGetOp) error() string { 81 if i := strings.Index(string(op), " => "); i >= 0 { 82 return string(op[i+4:]) 83 } 84 return "" 85 } 86 87 func (op setGetOp) fails() bool { 88 return op.error() != "" 89 } 90 91 var setGetTests = [][]setGetOp{{ 92 // Basics. 93 `get foo=-`, 94 `getroot => snap "core" has no configuration`, 95 `set one=1 two=2`, 96 `set big=1234567890`, 97 `setunder three=3 big=9876543210`, 98 `get one=1 big=1234567890 two=2 three=-`, 99 `getunder one=- two=- three=3 big=9876543210`, 100 `changes core.big core.one core.two`, 101 `commit`, 102 `getunder one=1 two=2 three=3`, 103 `get one=1 two=2 three=3`, 104 `set two=22 four=4 big=1234567890`, 105 `changes core.big core.four core.two`, 106 `get one=1 two=22 three=3 four=4 big=1234567890`, 107 `getunder one=1 two=2 three=3 four=-`, 108 `commit`, 109 `getunder one=1 two=22 three=3 four=4`, 110 }, { 111 // Trivial full doc. 112 `set doc={"one":1,"two":2}`, 113 `get doc={"one":1,"two":2}`, 114 `changes core.doc.one core.doc.two`, 115 }, { 116 // Nulls via dotted path 117 `set doc={"one":1,"two":2}`, 118 `commit`, 119 `set doc.one=null`, 120 `changes core.doc.one`, 121 `get doc={"two":2}`, 122 `getunder doc={"one":1,"two":2}`, 123 `commit`, 124 `get doc={"two":2}`, 125 `getroot ={"doc":{"two":2}}`, 126 `getunder doc={"two":2}`, // nils are not committed to state 127 }, { 128 // Nulls via dotted path, resuling in empty map 129 `set doc={"one":{"three":3},"two":2}`, 130 `set doc.one.three=null`, 131 `changes core.doc.one.three core.doc.two`, 132 `get doc={"one":{},"two":2}`, 133 `commit`, 134 `get doc={"one":{},"two":2}`, 135 `getunder doc={"one":{},"two":2}`, // nils are not committed to state 136 }, { 137 // Nulls via dotted path in a doc 138 `set doc={"one":1,"two":2}`, 139 `set doc.three={"four":4}`, 140 `get doc={"one":1,"two":2,"three":{"four":4}}`, 141 `set doc.three={"four":null}`, 142 `changes core.doc.one core.doc.three.four core.doc.two`, 143 `get doc={"one":1,"two":2,"three":{}}`, 144 `commit`, 145 `get doc={"one":1,"two":2,"three":{}}`, 146 `getunder doc={"one":1,"two":2,"three":{}}`, // nils are not committed to state 147 }, { 148 // Nulls nested in a document 149 `set doc={"one":{"three":3,"two":2}}`, 150 `changes core.doc.one.three core.doc.one.two`, 151 `set doc={"one":{"three":null,"two":2}}`, 152 `changes core.doc.one.three core.doc.one.two`, 153 `get doc={"one":{"two":2}}`, 154 `commit`, 155 `get doc={"one":{"two":2}}`, 156 `getunder doc={"one":{"two":2}}`, // nils are not committed to state 157 }, { 158 // Nulls with mutating 159 `set doc={"one":{"two":2}}`, 160 `set doc.one.two=null`, 161 `changes core.doc.one.two`, 162 `set doc.one="foo"`, 163 `get doc.one="foo"`, 164 `commit`, 165 `get doc={"one":"foo"}`, 166 `getunder doc={"one":"foo"}`, // nils are not committed to state 167 }, { 168 // Nulls, intermediate temporary maps 169 `set doc={"one":{"two":2}}`, 170 `commit`, 171 `set doc.one.three.four.five=null`, 172 `get doc={"one":{"two":2,"three":{"four":{}}}}`, 173 `commit`, 174 `get doc={"one":{"two":2,"three":{"four":{}}}}`, 175 `getrootunder ={"doc":{"one":{"two":2,"three":{"four":{}}}}}`, // nils are not committed to state 176 }, { 177 // Nulls, same transaction 178 `set doc={"one":{"two":2}}`, 179 `set doc.one.three.four.five=null`, 180 `changes core.doc.one.three.four.five core.doc.one.two`, 181 `get doc={"one":{"two":2,"three":{"four":{}}}}`, 182 `commit`, 183 `get doc={"one":{"two":2,"three":{"four":{}}}}`, 184 `getrootunder ={"doc":{"one":{"two":2,"three":{"four":{}}}}}`, // nils are not committed to state 185 }, { 186 // Null leading to empty doc 187 `set doc={"one":1}`, 188 `set doc.one=null`, 189 `changes core.doc.one`, 190 `commit`, 191 `get doc={}`, 192 }, { 193 // Nulls leading to no snap configuration 194 `set doc="foo"`, 195 `set doc=null`, 196 `changes core.doc`, 197 `commit`, 198 `get doc=-`, 199 `getroot => snap "core" has no configuration`, 200 }, { 201 // set null over non-existing path 202 `set x.y.z=null`, 203 `changes core.x.y.z`, 204 `commit`, 205 `get x.y.z=-`, 206 }, { 207 // set null over non-existing path with initial config 208 `set foo=bar`, 209 `commit`, 210 `set x=null`, 211 `changes core.x`, 212 `commit`, 213 `get x=-`, 214 }, { 215 // Root doc 216 `set doc={"one":1,"two":2}`, 217 `changes core.doc.one core.doc.two`, 218 `getroot ={"doc":{"one":1,"two":2}}`, 219 `commit`, 220 `getroot ={"doc":{"one":1,"two":2}}`, 221 `getrootunder ={"doc":{"one":1,"two":2}}`, 222 }, { 223 // Nested mutations. 224 `set one.two.three=3`, 225 `changes core.one.two.three`, 226 `set one.five=5`, 227 `changes core.one.five core.one.two.three`, 228 `setunder one={"two":{"four":4}}`, 229 `get one={"two":{"three":3},"five":5}`, 230 `get one.two={"three":3}`, 231 `get one.two.three=3`, 232 `get one.five=5`, 233 `commit`, 234 `getunder one={"two":{"three":3,"four":4},"five":5}`, 235 `get one={"two":{"three":3,"four":4},"five":5}`, 236 `get one.two={"three":3,"four":4}`, 237 `get one.two.three=3`, 238 `get one.two.four=4`, 239 `get one.five=5`, 240 }, { 241 // Nested partial update with full get 242 `set one={"two":2,"three":3}`, 243 `commit`, 244 // update just one subkey 245 `set one.two=0`, 246 // both subkeys are returned 247 `get one={"two":0,"three":3}`, 248 `getroot ={"one":{"two":0,"three":3}}`, 249 `get one.two=0`, 250 `get one.three=3`, 251 `getunder one={"two":2,"three":3}`, 252 `changes core.one.two`, 253 `commit`, 254 `getroot ={"one":{"two":0,"three":3}}`, 255 `get one={"two":0,"three":3}`, 256 `getunder one={"two":0,"three":3}`, 257 }, { 258 // Replacement with nested mutation. 259 `set one={"two":{"three":3}}`, 260 `changes core.one.two.three`, 261 `set one.five=5`, 262 `changes core.one.five core.one.two.three`, 263 `get one={"two":{"three":3},"five":5}`, 264 `get one.two={"three":3}`, 265 `get one.two.three=3`, 266 `get one.five=5`, 267 `setunder one={"two":{"four":4},"six":6}`, 268 `commit`, 269 `getunder one={"two":{"three":3},"five":5}`, 270 }, { 271 // Cannot go through known scalar implicitly. 272 `set one.two=2`, 273 `changes core.one.two`, 274 `set one.two.three=3 => snap "core" option "one\.two" is not a map`, 275 `get one.two.three=3 => snap "core" option "one\.two" is not a map`, 276 `get one={"two":2}`, 277 `commit`, 278 `set one.two.three=3 => snap "core" option "one\.two" is not a map`, 279 `get one.two.three=3 => snap "core" option "one\.two" is not a map`, 280 `get one={"two":2}`, 281 `getunder one={"two":2}`, 282 }, { 283 // Unknown scalars may be overwritten though. 284 `setunder one={"two":2}`, 285 `set one.two.three=3`, 286 `changes core.one.two.three`, 287 `commit`, 288 `getunder one={"two":{"three":3}}`, 289 }, { 290 // Invalid option names. 291 `set BAD=1 => invalid option name: "BAD"`, 292 `set 42=1 => invalid option name: "42"`, 293 `set .bad=1 => invalid option name: ""`, 294 `set bad.=1 => invalid option name: ""`, 295 `set bad..bad=1 => invalid option name: ""`, 296 `set one.bad--bad.two=1 => invalid option name: "bad--bad"`, 297 `set one.-bad.two=1 => invalid option name: "-bad"`, 298 `set one.bad-.two=1 => invalid option name: "bad-"`, 299 }} 300 301 func (s *transactionSuite) TestSetGet(c *C) { 302 s.state.Lock() 303 defer s.state.Unlock() 304 305 for _, test := range setGetTests { 306 c.Logf("-----") 307 s.state.Set("config", map[string]interface{}{}) 308 t := config.NewTransaction(s.state) 309 snap := "core" 310 for _, op := range test { 311 c.Logf("%s", op) 312 switch op.kind() { 313 case "set": 314 for k, v := range op.args() { 315 err := t.Set(snap, k, v) 316 if op.fails() { 317 c.Assert(err, ErrorMatches, op.error()) 318 } else { 319 c.Assert(err, IsNil) 320 } 321 } 322 323 case "get": 324 for k, expected := range op.args() { 325 var obtained interface{} 326 err := t.Get(snap, k, &obtained) 327 if op.fails() { 328 c.Assert(err, ErrorMatches, op.error()) 329 var nothing interface{} 330 c.Assert(t.GetMaybe(snap, k, ¬hing), ErrorMatches, op.error()) 331 c.Assert(nothing, IsNil) 332 continue 333 } 334 if expected == "-" { 335 if !config.IsNoOption(err) { 336 c.Fatalf("Expected %q key to not exist, but it has value %v", k, obtained) 337 } 338 c.Assert(err, ErrorMatches, fmt.Sprintf("snap %q has no %q configuration option", snap, k)) 339 var nothing interface{} 340 c.Assert(t.GetMaybe(snap, k, ¬hing), IsNil) 341 c.Assert(nothing, IsNil) 342 continue 343 } 344 c.Assert(err, IsNil) 345 c.Assert(obtained, DeepEquals, expected) 346 347 obtained = nil 348 c.Assert(t.GetMaybe(snap, k, &obtained), IsNil) 349 c.Assert(obtained, DeepEquals, expected) 350 } 351 352 case "commit": 353 t.Commit() 354 355 case "changes": 356 expected := op.list() 357 obtained := t.Changes() 358 c.Check(obtained, DeepEquals, expected) 359 360 case "setunder": 361 var config map[string]map[string]interface{} 362 s.state.Get("config", &config) 363 if config == nil { 364 config = make(map[string]map[string]interface{}) 365 } 366 if config[snap] == nil { 367 config[snap] = make(map[string]interface{}) 368 } 369 for k, v := range op.args() { 370 if v == "-" { 371 delete(config[snap], k) 372 if len(config[snap]) == 0 { 373 delete(config[snap], snap) 374 } 375 } else { 376 config[snap][k] = v 377 } 378 } 379 s.state.Set("config", config) 380 381 case "getunder": 382 var config map[string]map[string]*json.RawMessage 383 s.state.Get("config", &config) 384 for k, expected := range op.args() { 385 obtained, ok := config[snap][k] 386 if expected == "-" { 387 if ok { 388 c.Fatalf("Expected %q key to not exist, but it has value %v", k, obtained) 389 } 390 continue 391 } 392 var cfg interface{} 393 c.Assert(jsonutil.DecodeWithNumber(bytes.NewReader(*obtained), &cfg), IsNil) 394 c.Assert(cfg, DeepEquals, expected) 395 } 396 case "getroot": 397 var obtained interface{} 398 err := t.Get(snap, "", &obtained) 399 if op.fails() { 400 c.Assert(err, ErrorMatches, op.error()) 401 continue 402 } 403 c.Assert(err, IsNil) 404 c.Assert(obtained, DeepEquals, op.args()[""]) 405 case "getrootunder": 406 var config map[string]*json.RawMessage 407 s.state.Get("config", &config) 408 for _, expected := range op.args() { 409 obtained, ok := config[snap] 410 c.Assert(ok, Equals, true) 411 var cfg interface{} 412 c.Assert(jsonutil.DecodeWithNumber(bytes.NewReader(*obtained), &cfg), IsNil) 413 c.Assert(cfg, DeepEquals, expected) 414 } 415 default: 416 panic("unknown test op kind: " + op.kind()) 417 } 418 } 419 } 420 } 421 422 type brokenType struct { 423 on string 424 } 425 426 func (b *brokenType) UnmarshalJSON(data []byte) error { 427 if b.on == string(data) { 428 return fmt.Errorf("BAM!") 429 } 430 return nil 431 } 432 433 func (s *transactionSuite) TestGetUnmarshalError(c *C) { 434 s.state.Lock() 435 defer s.state.Unlock() 436 c.Check(s.transaction.Set("test-snap", "foo", "good"), IsNil) 437 s.transaction.Commit() 438 439 tr := config.NewTransaction(s.state) 440 c.Check(tr.Set("test-snap", "foo", "break"), IsNil) 441 442 // Pristine state is good, value in the transaction breaks. 443 broken := brokenType{`"break"`} 444 err := tr.Get("test-snap", "foo", &broken) 445 c.Assert(err, ErrorMatches, ".*BAM!.*") 446 447 // Pristine state breaks, nothing in the transaction. 448 tr.Commit() 449 err = tr.Get("test-snap", "foo", &broken) 450 c.Assert(err, ErrorMatches, ".*BAM!.*") 451 } 452 453 func (s *transactionSuite) TestNoConfiguration(c *C) { 454 s.state.Lock() 455 defer s.state.Unlock() 456 457 var res interface{} 458 tr := config.NewTransaction(s.state) 459 err := tr.Get("some-snap", "", &res) 460 c.Assert(err, NotNil) 461 c.Assert(config.IsNoOption(err), Equals, true) 462 c.Assert(err, ErrorMatches, `snap "some-snap" has no configuration`) 463 } 464 465 func (s *transactionSuite) TestState(c *C) { 466 s.state.Lock() 467 defer s.state.Unlock() 468 469 tr := config.NewTransaction(s.state) 470 c.Check(tr.State(), DeepEquals, s.state) 471 } 472 473 func (s *transactionSuite) TestPristineIsNotTainted(c *C) { 474 s.state.Lock() 475 defer s.state.Unlock() 476 477 tr := config.NewTransaction(s.state) 478 c.Check(tr.Set("test-snap", "foo.a.a", "a"), IsNil) 479 tr.Commit() 480 481 var data interface{} 482 var result interface{} 483 tr = config.NewTransaction(s.state) 484 c.Check(tr.Set("test-snap", "foo.b", "b"), IsNil) 485 c.Check(tr.Set("test-snap", "foo.a.a", "b"), IsNil) 486 c.Assert(tr.Get("test-snap", "foo", &result), IsNil) 487 c.Check(result, DeepEquals, map[string]interface{}{"a": map[string]interface{}{"a": "b"}, "b": "b"}) 488 489 pristine := tr.PristineConfig() 490 c.Assert(json.Unmarshal([]byte(*pristine["test-snap"]["foo"]), &data), IsNil) 491 c.Assert(data, DeepEquals, map[string]interface{}{"a": map[string]interface{}{"a": "a"}}) 492 } 493 494 func (s *transactionSuite) TestPristineGet(c *C) { 495 s.state.Lock() 496 defer s.state.Unlock() 497 498 // start with a pristine config 499 s.state.Set("config", map[string]map[string]interface{}{ 500 "some-snap": {"opt1": "pristine-value"}, 501 }) 502 503 // change the config 504 var res interface{} 505 tr := config.NewTransaction(s.state) 506 err := tr.Set("some-snap", "opt1", "changed-value") 507 c.Assert(err, IsNil) 508 509 // and get will get the changed value 510 err = tr.Get("some-snap", "opt1", &res) 511 c.Assert(err, IsNil) 512 c.Assert(res, Equals, "changed-value") 513 514 // but GetPristine will get the pristine value 515 err = tr.GetPristine("some-snap", "opt1", &res) 516 c.Assert(err, IsNil) 517 c.Assert(res, Equals, "pristine-value") 518 519 // and GetPristine errors for options that don't exist in pristine 520 var res2 interface{} 521 err = tr.Set("some-snap", "opt2", "other-value") 522 c.Assert(err, IsNil) 523 err = tr.GetPristine("some-snap", "opt2", &res2) 524 c.Assert(err, ErrorMatches, `snap "some-snap" has no "opt2" configuration option`) 525 // but GetPristineMaybe does not error but also give no value 526 err = tr.GetPristineMaybe("some-snap", "opt2", &res2) 527 c.Assert(err, IsNil) 528 c.Assert(res2, IsNil) 529 // but regular get works 530 err = tr.Get("some-snap", "opt2", &res2) 531 c.Assert(err, IsNil) 532 c.Assert(res2, Equals, "other-value") 533 }