github.com/juju/charm/v11@v11.2.0/meta_test.go (about) 1 // Copyright 2011, 2012, 2013 Canonical Ltd. 2 // Licensed under the LGPLv3, see LICENCE file for details. 3 4 package charm_test 5 6 import ( 7 "bytes" 8 "fmt" 9 "io" 10 "io/ioutil" 11 "os" 12 "path/filepath" 13 "strings" 14 15 jc "github.com/juju/testing/checkers" 16 "github.com/juju/version/v2" 17 gc "gopkg.in/check.v1" 18 "gopkg.in/yaml.v2" 19 20 "github.com/juju/charm/v11" 21 "github.com/juju/charm/v11/assumes" 22 "github.com/juju/charm/v11/resource" 23 ) 24 25 func repoMeta(c *gc.C, name string) io.Reader { 26 charmDir := charmDirPath(c, name) 27 file, err := os.Open(filepath.Join(charmDir, "metadata.yaml")) 28 c.Assert(err, gc.IsNil) 29 defer file.Close() 30 data, err := ioutil.ReadAll(file) 31 c.Assert(err, gc.IsNil) 32 return bytes.NewReader(data) 33 } 34 35 type MetaSuite struct{} 36 37 var _ = gc.Suite(&MetaSuite{}) 38 39 func (s *MetaSuite) TestReadMetaVersion1(c *gc.C) { 40 meta, err := charm.ReadMeta(repoMeta(c, "dummy")) 41 c.Assert(err, gc.IsNil) 42 c.Assert(meta.Name, gc.Equals, "dummy") 43 c.Assert(meta.Summary, gc.Equals, "That's a dummy charm.") 44 c.Assert(meta.Description, gc.Equals, 45 "This is a longer description which\npotentially contains multiple lines.\n") 46 c.Assert(meta.Subordinate, gc.Equals, false) 47 } 48 49 func (s *MetaSuite) TestReadMetaVersion2(c *gc.C) { 50 // This checks that we can accept a charm with the 51 // obsolete "format" field, even though we ignore it. 52 meta, err := charm.ReadMeta(repoMeta(c, "format2")) 53 c.Assert(err, gc.IsNil) 54 c.Assert(meta.Name, gc.Equals, "format2") 55 c.Assert(meta.Categories, gc.HasLen, 0) 56 c.Assert(meta.Terms, gc.HasLen, 0) 57 } 58 59 func (s *MetaSuite) TestValidTermFormat(c *gc.C) { 60 valid := []string{ 61 "foobar", 62 "foobar/27", 63 "foo/003", 64 "owner/foobar/27", 65 "owner/foobar", 66 "owner/foo-bar", 67 "own-er/foobar", 68 "ibm/j9-jvm/2", 69 "cs:foobar/27", 70 "cs:foobar", 71 } 72 73 invalid := []string{ 74 "/", 75 "/1", 76 "//", 77 "//2", 78 "27", 79 "owner/foo/foobar", 80 "@les/term/1", 81 "own_er/foobar", 82 } 83 84 for i, s := range valid { 85 c.Logf("valid test %d: %s", i, s) 86 meta := charm.Meta{Terms: []string{s}} 87 err := meta.Check(charm.FormatV1) 88 c.Check(err, jc.ErrorIsNil) 89 } 90 91 for i, s := range invalid { 92 c.Logf("invalid test %d: %s", i, s) 93 meta := charm.Meta{Terms: []string{s}} 94 err := meta.Check(charm.FormatV1) 95 c.Check(err, gc.NotNil) 96 } 97 } 98 99 func (s *MetaSuite) TestTermStringRoundTrip(c *gc.C) { 100 terms := []string{ 101 "foobar", 102 "foobar/27", 103 "owner/foobar/27", 104 "owner/foobar", 105 "owner/foo-bar", 106 "own-er/foobar", 107 "ibm/j9-jvm/2", 108 "cs:foobar/27", 109 } 110 for i, term := range terms { 111 c.Logf("test %d: %s", i, term) 112 id, err := charm.ParseTerm(term) 113 c.Check(err, gc.IsNil) 114 c.Check(id.String(), gc.Equals, term) 115 } 116 } 117 118 func (s *MetaSuite) TestCheckTerms(c *gc.C) { 119 tests := []struct { 120 about string 121 terms []string 122 expectError string 123 }{{ 124 about: "valid terms", 125 terms: []string{"term/1", "term/2", "term-without-revision", "tt/2"}, 126 }, { 127 about: "revision not a number", 128 terms: []string{"term/1", "term/a"}, 129 expectError: `wrong term name format "a"`, 130 }, { 131 about: "negative revision", 132 terms: []string{"term/-1"}, 133 expectError: "negative term revision", 134 }, { 135 about: "wrong format", 136 terms: []string{"term/1", "foobar/term/abc/1"}, 137 expectError: `unknown term id format "foobar/term/abc/1"`, 138 }, { 139 about: "term with owner", 140 terms: []string{"term/1", "term/abc/1"}, 141 }, { 142 about: "term with owner no rev", 143 terms: []string{"term/1", "term/abc"}, 144 }, { 145 about: "term may not contain spaces", 146 terms: []string{"term/1", "term about a term"}, 147 expectError: `wrong term name format "term about a term"`, 148 }, { 149 about: "term name must start with lowercase letter", 150 terms: []string{"Term/1"}, 151 expectError: `wrong term name format "Term"`, 152 }, { 153 about: "term name cannot contain capital letters", 154 terms: []string{"owner/foO-Bar"}, 155 expectError: `wrong term name format "foO-Bar"`, 156 }, { 157 about: "term name cannot contain underscores, that's what dashes are for", 158 terms: []string{"owner/foo_bar"}, 159 expectError: `wrong term name format "foo_bar"`, 160 }, { 161 about: "term name can't end with a dash", 162 terms: []string{"o-/1"}, 163 expectError: `wrong term name format "o-"`, 164 }, { 165 about: "term name can't contain consecutive dashes", 166 terms: []string{"o-oo--ooo---o/1"}, 167 expectError: `wrong term name format "o-oo--ooo---o"`, 168 }, { 169 about: "term name more than a single char", 170 terms: []string{"z/1"}, 171 expectError: `wrong term name format "z"`, 172 }, { 173 about: "term name match the regexp", 174 terms: []string{"term_123-23aAf/1"}, 175 expectError: `wrong term name format "term_123-23aAf"`, 176 }, 177 } 178 for i, test := range tests { 179 c.Logf("running test %v: %v", i, test.about) 180 meta := charm.Meta{Terms: test.terms} 181 err := meta.Check(charm.FormatV1) 182 if test.expectError == "" { 183 c.Check(err, jc.ErrorIsNil) 184 } else { 185 c.Check(err, gc.ErrorMatches, test.expectError) 186 } 187 } 188 } 189 190 func (s *MetaSuite) TestParseTerms(c *gc.C) { 191 tests := []struct { 192 about string 193 term string 194 expectError string 195 expectTerm charm.TermsId 196 }{{ 197 about: "valid term", 198 term: "term/1", 199 expectTerm: charm.TermsId{"", "", "term", 1}, 200 }, { 201 about: "valid term no revision", 202 term: "term", 203 expectTerm: charm.TermsId{"", "", "term", 0}, 204 }, { 205 about: "revision not a number", 206 term: "term/a", 207 expectError: `wrong term name format "a"`, 208 }, { 209 about: "negative revision", 210 term: "term/-1", 211 expectError: "negative term revision", 212 }, { 213 about: "bad revision", 214 term: "owner/term/12a", 215 expectError: `invalid revision number "12a" strconv.Atoi: parsing "12a": invalid syntax`, 216 }, { 217 about: "wrong format", 218 term: "foobar/term/abc/1", 219 expectError: `unknown term id format "foobar/term/abc/1"`, 220 }, { 221 about: "term with owner", 222 term: "term/abc/1", 223 expectTerm: charm.TermsId{"", "term", "abc", 1}, 224 }, { 225 about: "term with owner no rev", 226 term: "term/abc", 227 expectTerm: charm.TermsId{"", "term", "abc", 0}, 228 }, { 229 about: "term may not contain spaces", 230 term: "term about a term", 231 expectError: `wrong term name format "term about a term"`, 232 }, { 233 about: "term name must not start with a number", 234 term: "1Term/1", 235 expectError: `wrong term name format "1Term"`, 236 }, { 237 about: "full term with tenant", 238 term: "tenant:owner/term/1", 239 expectTerm: charm.TermsId{"tenant", "owner", "term", 1}, 240 }, { 241 about: "bad tenant", 242 term: "tenant::owner/term/1", 243 expectError: `wrong owner format ":owner"`, 244 }, { 245 about: "ownerless term with tenant", 246 term: "tenant:term/1", 247 expectTerm: charm.TermsId{"tenant", "", "term", 1}, 248 }, { 249 about: "ownerless revisionless term with tenant", 250 term: "tenant:term", 251 expectTerm: charm.TermsId{"tenant", "", "term", 0}, 252 }, { 253 about: "owner/term with tenant", 254 term: "tenant:owner/term", 255 expectTerm: charm.TermsId{"tenant", "owner", "term", 0}, 256 }, { 257 about: "term with tenant", 258 term: "tenant:term", 259 expectTerm: charm.TermsId{"tenant", "", "term", 0}, 260 }} 261 for i, test := range tests { 262 c.Logf("running test %v: %v", i, test.about) 263 term, err := charm.ParseTerm(test.term) 264 if test.expectError == "" { 265 c.Check(err, jc.ErrorIsNil) 266 c.Check(term, gc.DeepEquals, &test.expectTerm) 267 } else { 268 c.Check(err, gc.ErrorMatches, test.expectError) 269 c.Check(term, gc.IsNil) 270 } 271 } 272 } 273 274 func (s *MetaSuite) TestReadCategory(c *gc.C) { 275 meta, err := charm.ReadMeta(repoMeta(c, "category")) 276 c.Assert(err, gc.IsNil) 277 c.Assert(meta.Categories, jc.DeepEquals, []string{"database"}) 278 } 279 280 func (s *MetaSuite) TestReadTerms(c *gc.C) { 281 meta, err := charm.ReadMeta(repoMeta(c, "terms")) 282 c.Assert(err, jc.ErrorIsNil) 283 err = meta.Check(charm.FormatV1) 284 c.Assert(err, jc.ErrorIsNil) 285 c.Assert(meta.Terms, jc.DeepEquals, []string{"term1/1", "term2", "owner/term3/1"}) 286 } 287 288 var metaDataWithInvalidTermsId = ` 289 name: terms 290 summary: "Sample charm with terms and conditions" 291 description: | 292 That's a boring charm that requires certain terms. 293 terms: ["!!!/abc"] 294 ` 295 296 func (s *MetaSuite) TestCheckReadInvalidTerms(c *gc.C) { 297 reader := strings.NewReader(metaDataWithInvalidTermsId) 298 meta, err := charm.ReadMeta(reader) 299 c.Assert(err, jc.ErrorIsNil) 300 err = meta.Check(charm.FormatV1) 301 c.Assert(err, gc.ErrorMatches, `wrong owner format "!!!"`) 302 } 303 304 func (s *MetaSuite) TestReadTags(c *gc.C) { 305 meta, err := charm.ReadMeta(repoMeta(c, "category")) 306 c.Assert(err, gc.IsNil) 307 c.Assert(meta.Tags, jc.DeepEquals, []string{"openstack", "storage"}) 308 } 309 310 func (s *MetaSuite) TestSubordinate(c *gc.C) { 311 meta, err := charm.ReadMeta(repoMeta(c, "logging")) 312 c.Assert(err, gc.IsNil) 313 c.Assert(meta.Subordinate, gc.Equals, true) 314 } 315 316 func (s *MetaSuite) TestCheckSubordinateWithoutContainerRelation(c *gc.C) { 317 r := repoMeta(c, "dummy") 318 hackYaml := ReadYaml(r) 319 hackYaml["subordinate"] = true 320 meta, err := charm.ReadMeta(hackYaml.Reader()) 321 c.Assert(err, jc.ErrorIsNil) 322 err = meta.Check(charm.FormatV1) 323 c.Assert(err, gc.ErrorMatches, "subordinate charm \"dummy\" lacks \"requires\" relation with container scope") 324 } 325 326 func (s *MetaSuite) TestScopeConstraint(c *gc.C) { 327 meta, err := charm.ReadMeta(repoMeta(c, "logging")) 328 c.Assert(err, gc.IsNil) 329 c.Assert(meta.Provides["logging-client"].Scope, gc.Equals, charm.ScopeGlobal) 330 c.Assert(meta.Requires["logging-directory"].Scope, gc.Equals, charm.ScopeContainer) 331 c.Assert(meta.Subordinate, gc.Equals, true) 332 } 333 334 func (s *MetaSuite) TestParseMetaRelations(c *gc.C) { 335 meta, err := charm.ReadMeta(repoMeta(c, "mysql")) 336 c.Assert(err, gc.IsNil) 337 c.Assert(meta.Provides["server"], gc.Equals, charm.Relation{ 338 Name: "server", 339 Role: charm.RoleProvider, 340 Interface: "mysql", 341 Scope: charm.ScopeGlobal, 342 }) 343 c.Assert(meta.Requires, gc.IsNil) 344 c.Assert(meta.Peers, gc.IsNil) 345 346 meta, err = charm.ReadMeta(repoMeta(c, "riak")) 347 c.Assert(err, gc.IsNil) 348 c.Assert(meta.Provides["endpoint"], gc.Equals, charm.Relation{ 349 Name: "endpoint", 350 Role: charm.RoleProvider, 351 Interface: "http", 352 Scope: charm.ScopeGlobal, 353 }) 354 c.Assert(meta.Provides["admin"], gc.Equals, charm.Relation{ 355 Name: "admin", 356 Role: charm.RoleProvider, 357 Interface: "http", 358 Scope: charm.ScopeGlobal, 359 }) 360 c.Assert(meta.Peers["ring"], gc.Equals, charm.Relation{ 361 Name: "ring", 362 Role: charm.RolePeer, 363 Interface: "riak", 364 Scope: charm.ScopeGlobal, 365 }) 366 c.Assert(meta.Requires, gc.IsNil) 367 368 meta, err = charm.ReadMeta(repoMeta(c, "terracotta")) 369 c.Assert(err, gc.IsNil) 370 c.Assert(meta.Provides["dso"], gc.Equals, charm.Relation{ 371 Name: "dso", 372 Role: charm.RoleProvider, 373 Interface: "terracotta", 374 Optional: true, 375 Scope: charm.ScopeGlobal, 376 }) 377 c.Assert(meta.Peers["server-array"], gc.Equals, charm.Relation{ 378 Name: "server-array", 379 Role: charm.RolePeer, 380 Interface: "terracotta-server", 381 Scope: charm.ScopeGlobal, 382 }) 383 c.Assert(meta.Requires, gc.IsNil) 384 385 meta, err = charm.ReadMeta(repoMeta(c, "wordpress")) 386 c.Assert(err, gc.IsNil) 387 c.Assert(meta.Provides["url"], gc.Equals, charm.Relation{ 388 Name: "url", 389 Role: charm.RoleProvider, 390 Interface: "http", 391 Scope: charm.ScopeGlobal, 392 }) 393 c.Assert(meta.Requires["db"], gc.Equals, charm.Relation{ 394 Name: "db", 395 Role: charm.RoleRequirer, 396 Interface: "mysql", 397 Limit: 1, 398 Scope: charm.ScopeGlobal, 399 }) 400 c.Assert(meta.Requires["cache"], gc.Equals, charm.Relation{ 401 Name: "cache", 402 Role: charm.RoleRequirer, 403 Interface: "varnish", 404 Limit: 2, 405 Optional: true, 406 Scope: charm.ScopeGlobal, 407 }) 408 c.Assert(meta.Peers, gc.IsNil) 409 410 meta, err = charm.ReadMeta(repoMeta(c, "monitoring")) 411 c.Assert(err, gc.IsNil) 412 c.Assert(meta.Provides["monitoring-client"], gc.Equals, charm.Relation{ 413 Name: "monitoring-client", 414 Role: charm.RoleProvider, 415 Interface: "monitoring", 416 Scope: charm.ScopeGlobal, 417 }) 418 c.Assert(meta.Requires["monitoring-port"], gc.Equals, charm.Relation{ 419 Name: "monitoring-port", 420 Role: charm.RoleRequirer, 421 Interface: "monitoring", 422 Scope: charm.ScopeContainer, 423 }) 424 c.Assert(meta.Requires["info"], gc.Equals, charm.Relation{ 425 Name: "info", 426 Role: charm.RoleRequirer, 427 Interface: "juju-info", 428 Scope: charm.ScopeContainer, 429 }) 430 431 c.Assert(meta.Peers, gc.IsNil) 432 } 433 434 func (s *MetaSuite) TestCombinedRelations(c *gc.C) { 435 meta, err := charm.ReadMeta(repoMeta(c, "riak")) 436 c.Assert(err, gc.IsNil) 437 combinedRelations := meta.CombinedRelations() 438 expectedLength := len(meta.Provides) + len(meta.Requires) + len(meta.Peers) 439 c.Assert(combinedRelations, gc.HasLen, expectedLength) 440 c.Assert(combinedRelations, jc.DeepEquals, map[string]charm.Relation{ 441 "endpoint": { 442 Name: "endpoint", 443 Role: charm.RoleProvider, 444 Interface: "http", 445 Scope: charm.ScopeGlobal, 446 }, 447 "admin": { 448 Name: "admin", 449 Role: charm.RoleProvider, 450 Interface: "http", 451 Scope: charm.ScopeGlobal, 452 }, 453 "ring": { 454 Name: "ring", 455 Role: charm.RolePeer, 456 Interface: "riak", 457 Scope: charm.ScopeGlobal, 458 }, 459 }) 460 } 461 462 func (s *MetaSuite) TestParseJujuRelations(c *gc.C) { 463 meta, err := charm.ReadMeta(repoMeta(c, "juju-charm")) 464 c.Assert(err, gc.IsNil) 465 c.Assert(meta.Provides["dashboard"], gc.Equals, charm.Relation{ 466 Name: "dashboard", 467 Role: charm.RoleProvider, 468 Interface: "juju-dashboard", 469 Scope: charm.ScopeGlobal, 470 }) 471 } 472 473 var relationsConstraintsTests = []struct { 474 rels string 475 err string 476 }{ 477 { 478 "provides:\n foo: ping\nrequires:\n foo: pong", 479 `charm "a" using a duplicated relation name: "foo"`, 480 }, { 481 "requires:\n foo: ping\npeers:\n foo: pong", 482 `charm "a" using a duplicated relation name: "foo"`, 483 }, { 484 "peers:\n foo: ping\nprovides:\n foo: pong", 485 `charm "a" using a duplicated relation name: "foo"`, 486 }, { 487 "provides:\n juju: blob", 488 `charm "a" using a reserved relation name: "juju"`, 489 }, { 490 "requires:\n juju: blob", 491 `charm "a" using a reserved relation name: "juju"`, 492 }, { 493 "peers:\n juju: blob", 494 `charm "a" using a reserved relation name: "juju"`, 495 }, { 496 "provides:\n juju-snap: blub", 497 `charm "a" using a reserved relation name: "juju-snap"`, 498 }, { 499 "requires:\n juju-crackle: blub", 500 `charm "a" using a reserved relation name: "juju-crackle"`, 501 }, { 502 "peers:\n juju-pop: blub", 503 `charm "a" using a reserved relation name: "juju-pop"`, 504 }, { 505 "provides:\n innocuous: juju", 506 `charm "a" relation "innocuous" using a reserved interface: "juju"`, 507 }, { 508 "peers:\n innocuous: juju", 509 `charm "a" relation "innocuous" using a reserved interface: "juju"`, 510 }, { 511 "provides:\n innocuous: juju-snap", 512 `charm "a" relation "innocuous" using a reserved interface: "juju-snap"`, 513 }, { 514 "peers:\n innocuous: juju-snap", 515 `charm "a" relation "innocuous" using a reserved interface: "juju-snap"`, 516 }, 517 } 518 519 func (s *MetaSuite) TestCheckRelationsConstraints(c *gc.C) { 520 check := func(s, e string) { 521 meta, err := charm.ReadMeta(strings.NewReader(s)) 522 c.Assert(err, jc.ErrorIsNil) 523 c.Assert(meta, gc.NotNil) 524 err = meta.Check(charm.FormatV1) 525 if e != "" { 526 c.Assert(err, gc.ErrorMatches, e) 527 } else { 528 c.Assert(err, gc.IsNil) 529 } 530 } 531 prefix := "name: a\nsummary: b\ndescription: c\n" 532 for i, t := range relationsConstraintsTests { 533 c.Logf("test %d", i) 534 check(prefix+t.rels, t.err) 535 check(prefix+"subordinate: true\n"+t.rels, t.err) 536 } 537 // The juju-* namespace is accessible to container-scoped require 538 // relations on subordinate charms. 539 check(prefix+` 540 subordinate: true 541 requires: 542 juju-info: 543 interface: juju-info 544 scope: container`, "") 545 // The juju-* interfaces are allowed on any require relation. 546 check(prefix+` 547 requires: 548 innocuous: juju-info`, "") 549 } 550 551 // dummyMetadata contains a minimally valid charm metadata.yaml 552 // for testing valid and invalid series. 553 const dummyMetadata = "name: a\nsummary: b\ndescription: c" 554 555 func (s *MetaSuite) TestSeries(c *gc.C) { 556 // series not specified 557 meta, err := charm.ReadMeta(strings.NewReader(dummyMetadata)) 558 c.Assert(err, gc.IsNil) 559 c.Check(meta.Series, gc.HasLen, 0) 560 charmMeta := fmt.Sprintf("%s\nseries:", dummyMetadata) 561 for _, seriesName := range []string{"precise", "trusty", "plan9"} { 562 charmMeta = fmt.Sprintf("%s\n - %s", charmMeta, seriesName) 563 } 564 meta, err = charm.ReadMeta(strings.NewReader(charmMeta)) 565 c.Assert(err, gc.IsNil) 566 c.Assert(meta.Series, gc.DeepEquals, []string{"precise", "trusty", "plan9"}) 567 } 568 569 func (s *MetaSuite) TestCheckInvalidSeries(c *gc.C) { 570 for _, seriesName := range []string{"pre-c1se", "pre^cise", "cp/m", "OpenVMS"} { 571 err := charm.Meta{ 572 Name: "a", 573 Summary: "b", 574 Description: "c", 575 Series: []string{seriesName}, 576 }.Check(charm.FormatV1) 577 c.Check(err, gc.ErrorMatches, `charm "a" declares invalid series: .*`) 578 } 579 } 580 581 func (s *MetaSuite) TestMinJujuVersion(c *gc.C) { 582 // series not specified 583 meta, err := charm.ReadMeta(strings.NewReader(dummyMetadata)) 584 c.Assert(err, gc.IsNil) 585 c.Check(meta.Series, gc.HasLen, 0) 586 charmMeta := fmt.Sprintf("%s\nmin-juju-version: ", dummyMetadata) 587 vals := []version.Number{ 588 {Major: 1, Minor: 25}, 589 {Major: 1, Minor: 25, Tag: "alpha"}, 590 {Major: 1, Minor: 25, Patch: 1}, 591 } 592 for _, ver := range vals { 593 val := charmMeta + ver.String() 594 meta, err = charm.ReadMeta(strings.NewReader(val)) 595 c.Assert(err, gc.IsNil) 596 c.Assert(meta.MinJujuVersion, gc.Equals, ver) 597 } 598 } 599 600 func (s *MetaSuite) TestInvalidMinJujuVersion(c *gc.C) { 601 _, err := charm.ReadMeta(strings.NewReader(dummyMetadata + "\nmin-juju-version: invalid-version")) 602 603 c.Check(err, gc.ErrorMatches, `invalid min-juju-version: invalid version "invalid-version"`) 604 } 605 606 func (s *MetaSuite) TestNoMinJujuVersion(c *gc.C) { 607 meta, err := charm.ReadMeta(strings.NewReader(dummyMetadata)) 608 c.Assert(err, jc.ErrorIsNil) 609 c.Check(meta.MinJujuVersion, gc.Equals, version.Zero) 610 } 611 612 func (s *MetaSuite) TestCheckMismatchedRelationName(c *gc.C) { 613 // This Check case cannot be covered by the above 614 // TestCheckRelationsConstraints tests. 615 meta := charm.Meta{ 616 Name: "foo", 617 Provides: map[string]charm.Relation{ 618 "foo": { 619 Name: "foo", 620 Role: charm.RolePeer, 621 Interface: "x", 622 Scope: charm.ScopeGlobal, 623 }, 624 }, 625 } 626 err := meta.Check(charm.FormatV1) 627 c.Assert(err, gc.ErrorMatches, `charm "foo" has mismatched role "peer"; expected "provider"`) 628 } 629 630 func (s *MetaSuite) TestCheckMismatchedRole(c *gc.C) { 631 // This Check case cannot be covered by the above 632 // TestCheckRelationsConstraints tests. 633 meta := charm.Meta{ 634 Name: "foo", 635 Provides: map[string]charm.Relation{ 636 "foo": { 637 Role: charm.RolePeer, 638 Interface: "foo", 639 Scope: charm.ScopeGlobal, 640 }, 641 }, 642 } 643 err := meta.Check(charm.FormatV1) 644 c.Assert(err, gc.ErrorMatches, `charm "foo" has mismatched relation name ""; expected "foo"`) 645 } 646 647 func (s *MetaSuite) TestCheckMismatchedExtraBindingName(c *gc.C) { 648 meta := charm.Meta{ 649 Name: "foo", 650 ExtraBindings: map[string]charm.ExtraBinding{ 651 "foo": {Name: "bar"}, 652 }, 653 } 654 err := meta.Check(charm.FormatV1) 655 c.Assert(err, gc.ErrorMatches, `charm "foo" has invalid extra bindings: mismatched extra binding name: got "bar", expected "foo"`) 656 } 657 658 func (s *MetaSuite) TestCheckEmptyNameKeyOrEmptyExtraBindingName(c *gc.C) { 659 meta := charm.Meta{ 660 Name: "foo", 661 ExtraBindings: map[string]charm.ExtraBinding{"": {Name: "bar"}}, 662 } 663 err := meta.Check(charm.FormatV1) 664 expectedError := `charm "foo" has invalid extra bindings: missing binding name` 665 c.Assert(err, gc.ErrorMatches, expectedError) 666 667 meta.ExtraBindings = map[string]charm.ExtraBinding{"bar": {Name: ""}} 668 err = meta.Check(charm.FormatV1) 669 c.Assert(err, gc.ErrorMatches, expectedError) 670 } 671 672 // Test rewriting of a given interface specification into long form. 673 // 674 // InterfaceExpander uses `coerce` to do one of two things: 675 // 676 // - Rewrite shorthand to the long form used for actual storage 677 // - Fills in defaults, including a configurable `limit` 678 // 679 // This test ensures test coverage on each of these branches, along 680 // with ensuring the conversion object properly raises SchemaError 681 // exceptions on invalid data. 682 func (s *MetaSuite) TestIfaceExpander(c *gc.C) { 683 e := charm.IfaceExpander(nil) 684 685 path := []string{"<pa", "th>"} 686 687 // Shorthand is properly rewritten 688 v, err := e.Coerce("http", path) 689 c.Assert(err, gc.IsNil) 690 c.Assert(v, jc.DeepEquals, map[string]interface{}{"interface": "http", "limit": nil, "optional": false, "scope": string(charm.ScopeGlobal)}) 691 692 // Defaults are properly applied 693 v, err = e.Coerce(map[string]interface{}{"interface": "http"}, path) 694 c.Assert(err, gc.IsNil) 695 c.Assert(v, jc.DeepEquals, map[string]interface{}{"interface": "http", "limit": nil, "optional": false, "scope": string(charm.ScopeGlobal)}) 696 697 v, err = e.Coerce(map[string]interface{}{"interface": "http", "limit": 2}, path) 698 c.Assert(err, gc.IsNil) 699 c.Assert(v, jc.DeepEquals, map[string]interface{}{"interface": "http", "limit": int64(2), "optional": false, "scope": string(charm.ScopeGlobal)}) 700 701 v, err = e.Coerce(map[string]interface{}{"interface": "http", "optional": true}, path) 702 c.Assert(err, gc.IsNil) 703 c.Assert(v, jc.DeepEquals, map[string]interface{}{"interface": "http", "limit": nil, "optional": true, "scope": string(charm.ScopeGlobal)}) 704 705 // Invalid data raises an error. 706 _, err = e.Coerce(42, path) 707 c.Assert(err, gc.ErrorMatches, `<path>: expected map, got int\(42\)`) 708 709 _, err = e.Coerce(map[string]interface{}{"interface": "http", "optional": nil}, path) 710 c.Assert(err, gc.ErrorMatches, "<path>.optional: expected bool, got nothing") 711 712 _, err = e.Coerce(map[string]interface{}{"interface": "http", "limit": "none, really"}, path) 713 c.Assert(err, gc.ErrorMatches, "<path>.limit: unexpected value.*") 714 715 // Can change default limit 716 e = charm.IfaceExpander(1) 717 v, err = e.Coerce(map[string]interface{}{"interface": "http"}, path) 718 c.Assert(err, gc.IsNil) 719 c.Assert(v, jc.DeepEquals, map[string]interface{}{"interface": "http", "limit": int64(1), "optional": false, "scope": string(charm.ScopeGlobal)}) 720 } 721 722 func (s *MetaSuite) TestMetaHooks(c *gc.C) { 723 meta, err := charm.ReadMeta(repoMeta(c, "wordpress")) 724 c.Assert(err, gc.IsNil) 725 hooks := meta.Hooks() 726 expectedHooks := map[string]bool{ 727 "install": true, 728 "start": true, 729 "config-changed": true, 730 "upgrade-charm": true, 731 "stop": true, 732 "remove": true, 733 "collect-metrics": true, 734 "meter-status-changed": true, 735 "leader-elected": true, 736 "leader-deposed": true, 737 "leader-settings-changed": true, 738 "update-status": true, 739 "cache-relation-created": true, 740 "cache-relation-joined": true, 741 "cache-relation-changed": true, 742 "cache-relation-departed": true, 743 "cache-relation-broken": true, 744 "db-relation-created": true, 745 "db-relation-joined": true, 746 "db-relation-changed": true, 747 "db-relation-departed": true, 748 "db-relation-broken": true, 749 "logging-dir-relation-created": true, 750 "logging-dir-relation-joined": true, 751 "logging-dir-relation-changed": true, 752 "logging-dir-relation-departed": true, 753 "logging-dir-relation-broken": true, 754 "monitoring-port-relation-created": true, 755 "monitoring-port-relation-joined": true, 756 "monitoring-port-relation-changed": true, 757 "monitoring-port-relation-departed": true, 758 "monitoring-port-relation-broken": true, 759 "pre-series-upgrade": true, 760 "post-series-upgrade": true, 761 "url-relation-created": true, 762 "url-relation-joined": true, 763 "url-relation-changed": true, 764 "url-relation-departed": true, 765 "url-relation-broken": true, 766 "secret-changed": true, 767 "secret-expired": true, 768 "secret-remove": true, 769 "secret-rotate": true, 770 } 771 c.Assert(hooks, jc.DeepEquals, expectedHooks) 772 } 773 774 func (s *MetaSuite) TestCodecRoundTripEmpty(c *gc.C) { 775 for _, codec := range codecs { 776 c.Logf("codec %s", codec.Name) 777 empty_input := charm.Meta{} 778 data, err := codec.Marshal(empty_input) 779 c.Assert(err, gc.IsNil) 780 var empty_output charm.Meta 781 err = codec.Unmarshal(data, &empty_output) 782 c.Assert(err, gc.IsNil) 783 c.Assert(empty_input, jc.DeepEquals, empty_output) 784 } 785 } 786 787 func (s *MetaSuite) TestCodecRoundTrip(c *gc.C) { 788 var input = charm.Meta{ 789 Name: "Foo", 790 Summary: "Bar", 791 Description: "Baz", 792 Subordinate: true, 793 Provides: map[string]charm.Relation{ 794 "qux": { 795 Name: "qux", 796 Role: charm.RoleProvider, 797 Interface: "quxx", 798 Optional: true, 799 Limit: 42, 800 Scope: charm.ScopeGlobal, 801 }, 802 }, 803 Requires: map[string]charm.Relation{ 804 "frob": { 805 Name: "frob", 806 Role: charm.RoleRequirer, 807 Interface: "quxx", 808 Optional: true, 809 Limit: 42, 810 Scope: charm.ScopeContainer, 811 }, 812 }, 813 Peers: map[string]charm.Relation{ 814 "arble": { 815 Name: "arble", 816 Role: charm.RolePeer, 817 Interface: "quxx", 818 Optional: true, 819 Limit: 42, 820 Scope: charm.ScopeGlobal, 821 }, 822 }, 823 ExtraBindings: map[string]charm.ExtraBinding{ 824 "b1": {Name: "b1"}, 825 "b2": {Name: "b2"}, 826 }, 827 Categories: []string{"quxxxx", "quxxxxx"}, 828 Tags: []string{"openstack", "storage"}, 829 Terms: []string{"test-term/1", "test-term/2"}, 830 } 831 for _, codec := range codecs { 832 c.Logf("codec %s", codec.Name) 833 data, err := codec.Marshal(input) 834 c.Assert(err, gc.IsNil) 835 var output charm.Meta 836 err = codec.Unmarshal(data, &output) 837 c.Assert(err, gc.IsNil) 838 c.Assert(output, jc.DeepEquals, input, gc.Commentf("data: %q", data)) 839 } 840 } 841 842 func (s *MetaSuite) TestCodecRoundTripKubernetes(c *gc.C) { 843 var input = charm.Meta{ 844 Name: "Foo", 845 Summary: "Bar", 846 Description: "Baz", 847 Subordinate: true, 848 Provides: map[string]charm.Relation{ 849 "qux": { 850 Name: "qux", 851 Role: charm.RoleProvider, 852 Interface: "quxx", 853 Optional: true, 854 Limit: 42, 855 Scope: charm.ScopeGlobal, 856 }, 857 }, 858 Requires: map[string]charm.Relation{ 859 "frob": { 860 Name: "frob", 861 Role: charm.RoleRequirer, 862 Interface: "quxx", 863 Optional: true, 864 Limit: 42, 865 Scope: charm.ScopeContainer, 866 }, 867 }, 868 Peers: map[string]charm.Relation{ 869 "arble": { 870 Name: "arble", 871 Role: charm.RolePeer, 872 Interface: "quxx", 873 Optional: true, 874 Limit: 42, 875 Scope: charm.ScopeGlobal, 876 }, 877 }, 878 ExtraBindings: map[string]charm.ExtraBinding{ 879 "b1": {Name: "b1"}, 880 "b2": {Name: "b2"}, 881 }, 882 Categories: []string{"quxxxx", "quxxxxx"}, 883 Tags: []string{"openstack", "storage"}, 884 Terms: []string{"test-term/1", "test-term/2"}, 885 Containers: map[string]charm.Container{ 886 "test": { 887 Mounts: []charm.Mount{{ 888 Storage: "test", 889 Location: "/wow/", 890 }}, 891 Resource: "test", 892 }, 893 }, 894 Resources: map[string]resource.Meta{ 895 "test": { 896 Name: "test", 897 Type: resource.TypeContainerImage, 898 }, 899 "test2": { 900 Name: "test2", 901 Type: resource.TypeContainerImage, 902 }, 903 }, 904 Storage: map[string]charm.Storage{ 905 "test": { 906 Name: "test", 907 Type: charm.StorageFilesystem, 908 CountMin: 1, 909 CountMax: 1, 910 }, 911 }, 912 } 913 for _, codec := range codecs { 914 c.Logf("codec %s", codec.Name) 915 data, err := codec.Marshal(input) 916 c.Assert(err, gc.IsNil) 917 var output charm.Meta 918 err = codec.Unmarshal(data, &output) 919 c.Assert(err, gc.IsNil) 920 c.Assert(output, jc.DeepEquals, input, gc.Commentf("data: %q", data)) 921 } 922 } 923 924 var implementedByTests = []struct { 925 ifce string 926 name string 927 role charm.RelationRole 928 scope charm.RelationScope 929 match bool 930 implicit bool 931 }{ 932 {"ifce-pro", "pro", charm.RoleProvider, charm.ScopeGlobal, true, false}, 933 {"blah", "pro", charm.RoleProvider, charm.ScopeGlobal, false, false}, 934 {"ifce-pro", "blah", charm.RoleProvider, charm.ScopeGlobal, false, false}, 935 {"ifce-pro", "pro", charm.RoleRequirer, charm.ScopeGlobal, false, false}, 936 {"ifce-pro", "pro", charm.RoleProvider, charm.ScopeContainer, true, false}, 937 938 {"juju-info", "juju-info", charm.RoleProvider, charm.ScopeGlobal, true, true}, 939 {"blah", "juju-info", charm.RoleProvider, charm.ScopeGlobal, false, false}, 940 {"juju-info", "blah", charm.RoleProvider, charm.ScopeGlobal, false, false}, 941 {"juju-info", "juju-info", charm.RoleRequirer, charm.ScopeGlobal, false, false}, 942 {"juju-info", "juju-info", charm.RoleProvider, charm.ScopeContainer, true, true}, 943 944 {"ifce-req", "req", charm.RoleRequirer, charm.ScopeGlobal, true, false}, 945 {"blah", "req", charm.RoleRequirer, charm.ScopeGlobal, false, false}, 946 {"ifce-req", "blah", charm.RoleRequirer, charm.ScopeGlobal, false, false}, 947 {"ifce-req", "req", charm.RolePeer, charm.ScopeGlobal, false, false}, 948 {"ifce-req", "req", charm.RoleRequirer, charm.ScopeContainer, true, false}, 949 950 {"juju-info", "info", charm.RoleRequirer, charm.ScopeContainer, true, false}, 951 {"blah", "info", charm.RoleRequirer, charm.ScopeContainer, false, false}, 952 {"juju-info", "blah", charm.RoleRequirer, charm.ScopeContainer, false, false}, 953 {"juju-info", "info", charm.RolePeer, charm.ScopeContainer, false, false}, 954 {"juju-info", "info", charm.RoleRequirer, charm.ScopeGlobal, false, false}, 955 956 {"ifce-peer", "peer", charm.RolePeer, charm.ScopeGlobal, true, false}, 957 {"blah", "peer", charm.RolePeer, charm.ScopeGlobal, false, false}, 958 {"ifce-peer", "blah", charm.RolePeer, charm.ScopeGlobal, false, false}, 959 {"ifce-peer", "peer", charm.RoleProvider, charm.ScopeGlobal, false, false}, 960 {"ifce-peer", "peer", charm.RolePeer, charm.ScopeContainer, true, false}, 961 } 962 963 func (s *MetaSuite) TestImplementedBy(c *gc.C) { 964 for i, t := range implementedByTests { 965 c.Logf("test %d", i) 966 r := charm.Relation{ 967 Interface: t.ifce, 968 Name: t.name, 969 Role: t.role, 970 Scope: t.scope, 971 } 972 c.Assert(r.ImplementedBy(&dummyCharm{}), gc.Equals, t.match) 973 c.Assert(r.IsImplicit(), gc.Equals, t.implicit) 974 } 975 } 976 977 var metaYAMLMarshalTests = []struct { 978 about string 979 yaml string 980 }{{ 981 about: "minimal charm", 982 yaml: ` 983 name: minimal 984 description: d 985 summary: s 986 `, 987 }, { 988 about: "charm with lots of stuff", 989 yaml: ` 990 name: big 991 description: d 992 summary: s 993 subordinate: true 994 provides: 995 provideSimple: someinterface 996 provideLessSimple: 997 interface: anotherinterface 998 optional: true 999 scope: container 1000 limit: 3 1001 requires: 1002 requireSimple: someinterface 1003 requireLessSimple: 1004 interface: anotherinterface 1005 optional: true 1006 scope: container 1007 limit: 3 1008 peers: 1009 peerSimple: someinterface 1010 peerLessSimple: 1011 interface: peery 1012 optional: true 1013 extra-bindings: 1014 extraBar: 1015 extraFoo1: 1016 categories: [c1, c1] 1017 tags: [t1, t2] 1018 series: 1019 - someseries 1020 resources: 1021 foo: 1022 description: 'a description' 1023 filename: 'x.zip' 1024 bar: 1025 filename: 'y.tgz' 1026 type: file 1027 `, 1028 }, { 1029 about: "minimal charm with nested assumes block", 1030 yaml: ` 1031 name: minimal-with-assumes 1032 description: d 1033 summary: s 1034 assumes: 1035 - chips 1036 - any-of: 1037 - guacamole 1038 - salsa 1039 - any-of: 1040 - good-weather 1041 - great-music 1042 - all-of: 1043 - table 1044 - lazy-suzan 1045 `, 1046 }} 1047 1048 func (s *MetaSuite) TestYAMLMarshal(c *gc.C) { 1049 for i, test := range metaYAMLMarshalTests { 1050 c.Logf("test %d: %s", i, test.about) 1051 ch, err := charm.ReadMeta(strings.NewReader(test.yaml)) 1052 c.Assert(err, gc.IsNil) 1053 gotYAML, err := yaml.Marshal(ch) 1054 c.Assert(err, gc.IsNil) 1055 gotCh, err := charm.ReadMeta(bytes.NewReader(gotYAML)) 1056 c.Assert(err, gc.IsNil) 1057 c.Assert(gotCh, jc.DeepEquals, ch) 1058 } 1059 } 1060 1061 func (s *MetaSuite) TestYAMLMarshalSimpleRelationOrExtraBinding(c *gc.C) { 1062 // Check that a simple relation / extra-binding gets marshaled as a string. 1063 chYAML := ` 1064 name: minimal 1065 description: d 1066 summary: s 1067 provides: 1068 server: http 1069 requires: 1070 client: http 1071 peers: 1072 me: http 1073 extra-bindings: 1074 foo: 1075 ` 1076 ch, err := charm.ReadMeta(strings.NewReader(chYAML)) 1077 c.Assert(err, gc.IsNil) 1078 gotYAML, err := yaml.Marshal(ch) 1079 c.Assert(err, gc.IsNil) 1080 1081 var x interface{} 1082 err = yaml.Unmarshal(gotYAML, &x) 1083 c.Assert(err, gc.IsNil) 1084 c.Assert(x, jc.DeepEquals, map[interface{}]interface{}{ 1085 "name": "minimal", 1086 "description": "d", 1087 "summary": "s", 1088 "provides": map[interface{}]interface{}{ 1089 "server": "http", 1090 }, 1091 "requires": map[interface{}]interface{}{ 1092 "client": "http", 1093 }, 1094 "peers": map[interface{}]interface{}{ 1095 "me": "http", 1096 }, 1097 "extra-bindings": map[interface{}]interface{}{ 1098 "foo": nil, 1099 }, 1100 }) 1101 } 1102 1103 func (s *MetaSuite) TestDevices(c *gc.C) { 1104 meta, err := charm.ReadMeta(strings.NewReader(` 1105 name: a 1106 summary: b 1107 description: c 1108 devices: 1109 bitcoin-miner1: 1110 description: a big gpu device 1111 type: gpu 1112 countmin: 1 1113 countmax: 1 1114 bitcoin-miner2: 1115 description: a nvdia gpu device 1116 type: nvidia.com/gpu 1117 countmin: 1 1118 countmax: 2 1119 bitcoin-miner3: 1120 description: an amd gpu device 1121 type: amd.com/gpu 1122 countmin: 1 1123 countmax: 2 1124 `)) 1125 c.Assert(err, gc.IsNil) 1126 c.Assert(meta.Devices, gc.DeepEquals, map[string]charm.Device{ 1127 "bitcoin-miner1": { 1128 Name: "bitcoin-miner1", 1129 Description: "a big gpu device", 1130 Type: "gpu", 1131 CountMin: 1, 1132 CountMax: 1, 1133 }, 1134 "bitcoin-miner2": { 1135 Name: "bitcoin-miner2", 1136 Description: "a nvdia gpu device", 1137 Type: "nvidia.com/gpu", 1138 CountMin: 1, 1139 CountMax: 2, 1140 }, 1141 "bitcoin-miner3": { 1142 Name: "bitcoin-miner3", 1143 Description: "an amd gpu device", 1144 Type: "amd.com/gpu", 1145 CountMin: 1, 1146 CountMax: 2, 1147 }, 1148 }, gc.Commentf("meta: %+v", meta)) 1149 } 1150 1151 func (s *MetaSuite) TestDevicesDefaultLimitAndRequest(c *gc.C) { 1152 meta, err := charm.ReadMeta(strings.NewReader(` 1153 name: a 1154 summary: b 1155 description: c 1156 devices: 1157 bitcoin-miner: 1158 description: a big gpu device 1159 type: gpu 1160 `)) 1161 c.Assert(err, gc.IsNil) 1162 c.Assert(meta.Devices, gc.DeepEquals, map[string]charm.Device{ 1163 "bitcoin-miner": { 1164 Name: "bitcoin-miner", 1165 Description: "a big gpu device", 1166 Type: "gpu", 1167 CountMin: 1, 1168 CountMax: 1, 1169 }, 1170 }, gc.Commentf("meta: %+v", meta)) 1171 } 1172 1173 func (s *MetaSuite) TestDeployment(c *gc.C) { 1174 meta, err := charm.ReadMeta(strings.NewReader(` 1175 name: a 1176 summary: b 1177 description: c 1178 series: 1179 - kubernetes 1180 deployment: 1181 type: stateless 1182 mode: operator 1183 service: loadbalancer 1184 min-version: "1.15" 1185 `)) 1186 c.Assert(err, gc.IsNil) 1187 c.Assert(meta.Deployment, gc.DeepEquals, &charm.Deployment{ 1188 DeploymentType: "stateless", 1189 DeploymentMode: "operator", 1190 ServiceType: "loadbalancer", 1191 MinVersion: "1.15", 1192 }, gc.Commentf("meta: %+v", meta)) 1193 } 1194 1195 func (s *MetaSuite) TestCheckDeploymentErrors(c *gc.C) { 1196 prefix := ` 1197 name: a 1198 summary: b 1199 description: c 1200 deployment: 1201 `[1:] 1202 1203 tests := []testErrorPayload{{ 1204 desc: "invalid deployment type", 1205 yaml: " type: foo", 1206 err: `metadata: deployment.type: unexpected value "foo"`, 1207 }, { 1208 desc: "invalid deployment mode", 1209 yaml: " mode: foo", 1210 err: `metadata: deployment.mode: unexpected value "foo"`, 1211 }, { 1212 desc: "invalid service type", 1213 yaml: " service: foo", 1214 err: `metadata: deployment.service: unexpected value "foo"`, 1215 }, { 1216 desc: "invalid service type for series", 1217 yaml: " service: cluster\nseries:\n - xenial", 1218 err: `charms with deployment metadata only supported for "kubernetes"`, 1219 }, { 1220 desc: "missing series", 1221 yaml: " service: cluster", 1222 err: `charm with deployment metadata must declare at least one series`, 1223 }} 1224 1225 testErrors(c, prefix, tests) 1226 } 1227 1228 type testErrorPayload struct { 1229 desc string 1230 yaml string 1231 err string 1232 } 1233 1234 func testErrors(c *gc.C, prefix string, tests []testErrorPayload) { 1235 for i, test := range tests { 1236 c.Logf("test %d: %s", i, test.desc) 1237 c.Logf("\n%s\n", prefix+test.yaml) 1238 _, err := charm.ReadMeta(strings.NewReader(prefix + test.yaml)) 1239 c.Assert(err, gc.ErrorMatches, test.err) 1240 } 1241 } 1242 1243 func testCheckErrors(c *gc.C, prefix string, tests []testErrorPayload) { 1244 for i, test := range tests { 1245 c.Logf("test %d: %s", i, test.desc) 1246 c.Logf("\n%s\n", prefix+test.yaml) 1247 meta, err := charm.ReadMeta(strings.NewReader(prefix + test.yaml)) 1248 c.Assert(err, jc.ErrorIsNil) 1249 err = meta.Check(charm.FormatV1) 1250 c.Assert(err, gc.ErrorMatches, test.err) 1251 } 1252 } 1253 1254 func (s *MetaSuite) TestDevicesErrors(c *gc.C) { 1255 prefix := ` 1256 name: a 1257 summary: b 1258 description: c 1259 devices: 1260 bad-nvidia-gpu: 1261 `[1:] 1262 1263 tests := []testErrorPayload{{ 1264 desc: "invalid device type", 1265 yaml: " countmin: 0", 1266 err: "metadata: devices.bad-nvidia-gpu.type: expected string, got nothing", 1267 }, { 1268 desc: "countmax has to be greater than 0", 1269 yaml: " countmax: -1\n description: a big gpu device\n type: gpu", 1270 err: "metadata: invalid device count -1", 1271 }, { 1272 desc: "countmin has to be greater than 0", 1273 yaml: " countmin: -1\n description: a big gpu device\n type: gpu", 1274 err: "metadata: invalid device count -1", 1275 }} 1276 1277 testErrors(c, prefix, tests) 1278 1279 } 1280 1281 func (s *MetaSuite) TestCheckDevicesErrors(c *gc.C) { 1282 prefix := ` 1283 name: a 1284 summary: b 1285 description: c 1286 devices: 1287 bad-nvidia-gpu: 1288 `[1:] 1289 1290 tests := []testErrorPayload{{ 1291 desc: "countmax can not be smaller than countmin", 1292 yaml: " countmin: 2\n countmax: 1\n description: a big gpu device\n type: gpu", 1293 err: "charm \"a\" device \"bad-nvidia-gpu\": maximum count 1 can not be smaller than minimum count 2", 1294 }} 1295 1296 testCheckErrors(c, prefix, tests) 1297 1298 } 1299 1300 func (s *MetaSuite) TestStorage(c *gc.C) { 1301 // "type" is the only required attribute for storage. 1302 meta, err := charm.ReadMeta(strings.NewReader(` 1303 name: a 1304 summary: b 1305 description: c 1306 storage: 1307 store0: 1308 description: woo tee bix 1309 type: block 1310 store1: 1311 type: filesystem 1312 `)) 1313 c.Assert(err, gc.IsNil) 1314 c.Assert(meta.Storage, gc.DeepEquals, map[string]charm.Storage{ 1315 "store0": { 1316 Name: "store0", 1317 Description: "woo tee bix", 1318 Type: charm.StorageBlock, 1319 CountMin: 1, // singleton 1320 CountMax: 1, 1321 }, 1322 "store1": { 1323 Name: "store1", 1324 Type: charm.StorageFilesystem, 1325 CountMin: 1, // singleton 1326 CountMax: 1, 1327 }, 1328 }) 1329 } 1330 1331 func (s *MetaSuite) TestStorageErrors(c *gc.C) { 1332 prefix := ` 1333 name: a 1334 summary: b 1335 description: c 1336 storage: 1337 store-bad: 1338 `[1:] 1339 1340 tests := []testErrorPayload{{ 1341 desc: "type is required", 1342 yaml: " required: false", 1343 err: "metadata: storage.store-bad.type: unexpected value <nil>", 1344 }, { 1345 desc: "range must be an integer, or integer range (1)", 1346 yaml: " type: filesystem\n multiple:\n range: woat", 1347 err: `metadata: storage.store-bad.multiple.range: value "woat" does not match 'm', 'm-n', or 'm\+'`, 1348 }, { 1349 desc: "range must be an integer, or integer range (2)", 1350 yaml: " type: filesystem\n multiple:\n range: 0-abc", 1351 err: `metadata: storage.store-bad.multiple.range: value "0-abc" does not match 'm', 'm-n', or 'm\+'`, 1352 }, { 1353 desc: "range must be non-negative", 1354 yaml: " type: filesystem\n multiple:\n range: -1", 1355 err: `metadata: storage.store-bad.multiple.range: invalid count -1`, 1356 }, { 1357 desc: "range must be positive", 1358 yaml: " type: filesystem\n multiple:\n range: 0", 1359 err: `metadata: storage.store-bad.multiple.range: invalid count 0`, 1360 }, { 1361 desc: "minimum size must parse correctly", 1362 yaml: " type: block\n minimum-size: foo", 1363 err: `metadata: expected a non-negative number, got "foo"`, 1364 }, { 1365 desc: "minimum size must have valid suffix", 1366 yaml: " type: block\n minimum-size: 10Q", 1367 err: `metadata: invalid multiplier suffix "Q", expected one of MGTPEZY`, 1368 }, { 1369 desc: "properties must contain valid values", 1370 yaml: " type: block\n properties: [transient, foo]", 1371 err: `metadata: .* unexpected value "foo"`, 1372 }} 1373 1374 testErrors(c, prefix, tests) 1375 } 1376 1377 func (s *MetaSuite) TestCheckStorageErrors(c *gc.C) { 1378 prefix := ` 1379 name: a 1380 summary: b 1381 description: c 1382 storage: 1383 store-bad: 1384 `[1:] 1385 1386 tests := []testErrorPayload{{ 1387 desc: "location cannot be specified for block type storage", 1388 yaml: " type: block\n location: /dev/sdc", 1389 err: `charm "a" storage "store-bad": location may not be specified for "type: block"`, 1390 }} 1391 1392 testCheckErrors(c, prefix, tests) 1393 } 1394 1395 func (s *MetaSuite) TestStorageCount(c *gc.C) { 1396 testStorageCount := func(count string, min, max int) { 1397 meta, err := charm.ReadMeta(strings.NewReader(fmt.Sprintf(` 1398 name: a 1399 summary: b 1400 description: c 1401 storage: 1402 store0: 1403 type: filesystem 1404 multiple: 1405 range: %s 1406 `, count))) 1407 c.Assert(err, gc.IsNil) 1408 store := meta.Storage["store0"] 1409 c.Assert(store, gc.NotNil) 1410 c.Assert(store.CountMin, gc.Equals, min) 1411 c.Assert(store.CountMax, gc.Equals, max) 1412 } 1413 testStorageCount("1", 1, 1) 1414 testStorageCount("0-1", 0, 1) 1415 testStorageCount("1-1", 1, 1) 1416 testStorageCount("1+", 1, -1) 1417 // n- is equivalent to n+ 1418 testStorageCount("1-", 1, -1) 1419 } 1420 1421 func (s *MetaSuite) TestStorageLocation(c *gc.C) { 1422 meta, err := charm.ReadMeta(strings.NewReader(` 1423 name: a 1424 summary: b 1425 description: c 1426 storage: 1427 store0: 1428 type: filesystem 1429 location: /var/lib/things 1430 `)) 1431 c.Assert(err, gc.IsNil) 1432 store := meta.Storage["store0"] 1433 c.Assert(store, gc.NotNil) 1434 c.Assert(store.Location, gc.Equals, "/var/lib/things") 1435 } 1436 1437 func (s *MetaSuite) TestStorageMinimumSize(c *gc.C) { 1438 meta, err := charm.ReadMeta(strings.NewReader(` 1439 name: a 1440 summary: b 1441 description: c 1442 storage: 1443 store0: 1444 type: filesystem 1445 minimum-size: 10G 1446 `)) 1447 c.Assert(err, gc.IsNil) 1448 store := meta.Storage["store0"] 1449 c.Assert(store, gc.NotNil) 1450 c.Assert(store.MinimumSize, gc.Equals, uint64(10*1024)) 1451 } 1452 1453 func (s *MetaSuite) TestStorageProperties(c *gc.C) { 1454 meta, err := charm.ReadMeta(strings.NewReader(` 1455 name: a 1456 summary: b 1457 description: c 1458 storage: 1459 store0: 1460 type: filesystem 1461 properties: [transient] 1462 `)) 1463 c.Assert(err, gc.IsNil) 1464 store := meta.Storage["store0"] 1465 c.Assert(store, gc.NotNil) 1466 c.Assert(store.Properties, jc.SameContents, []string{"transient"}) 1467 } 1468 1469 func (s *MetaSuite) TestExtraBindings(c *gc.C) { 1470 meta, err := charm.ReadMeta(strings.NewReader(` 1471 name: a 1472 summary: b 1473 description: c 1474 extra-bindings: 1475 endpoint-1: 1476 foo: 1477 bar-42: 1478 `)) 1479 c.Assert(err, gc.IsNil) 1480 c.Assert(meta.ExtraBindings, gc.DeepEquals, map[string]charm.ExtraBinding{ 1481 "endpoint-1": { 1482 Name: "endpoint-1", 1483 }, 1484 "foo": { 1485 Name: "foo", 1486 }, 1487 "bar-42": { 1488 Name: "bar-42", 1489 }, 1490 }) 1491 } 1492 1493 func (s *MetaSuite) TestExtraBindingsEmptyMapError(c *gc.C) { 1494 meta, err := charm.ReadMeta(strings.NewReader(` 1495 name: a 1496 summary: b 1497 description: c 1498 extra-bindings: 1499 `)) 1500 c.Assert(err, gc.ErrorMatches, "metadata: extra-bindings: expected map, got nothing") 1501 c.Assert(meta, gc.IsNil) 1502 } 1503 1504 func (s *MetaSuite) TestExtraBindingsNonEmptyValueError(c *gc.C) { 1505 meta, err := charm.ReadMeta(strings.NewReader(` 1506 name: a 1507 summary: b 1508 description: c 1509 extra-bindings: 1510 foo: 42 1511 `)) 1512 c.Assert(err, gc.ErrorMatches, `metadata: extra-bindings.foo: expected empty value, got int\(42\)`) 1513 c.Assert(meta, gc.IsNil) 1514 } 1515 1516 func (s *MetaSuite) TestExtraBindingsEmptyNameError(c *gc.C) { 1517 meta, err := charm.ReadMeta(strings.NewReader(` 1518 name: a 1519 summary: b 1520 description: c 1521 extra-bindings: 1522 "": 1523 `)) 1524 c.Assert(err, gc.ErrorMatches, `metadata: extra-bindings: expected non-empty binding name, got string\(""\)`) 1525 c.Assert(meta, gc.IsNil) 1526 } 1527 1528 func (s *MetaSuite) TestPayloadClasses(c *gc.C) { 1529 meta, err := charm.ReadMeta(strings.NewReader(` 1530 name: a 1531 summary: b 1532 description: c 1533 payloads: 1534 monitor: 1535 type: docker 1536 kvm-guest: 1537 type: kvm 1538 `)) 1539 c.Assert(err, gc.IsNil) 1540 1541 c.Check(meta.PayloadClasses, jc.DeepEquals, map[string]charm.PayloadClass{ 1542 "monitor": { 1543 Name: "monitor", 1544 Type: "docker", 1545 }, 1546 "kvm-guest": { 1547 Name: "kvm-guest", 1548 Type: "kvm", 1549 }, 1550 }) 1551 } 1552 1553 func (s *MetaSuite) TestResources(c *gc.C) { 1554 meta, err := charm.ReadMeta(strings.NewReader(` 1555 name: a 1556 summary: b 1557 description: c 1558 resources: 1559 resource-name: 1560 type: file 1561 filename: filename.tgz 1562 description: "One line that is useful when operators need to push it." 1563 other-resource: 1564 type: file 1565 filename: other.zip 1566 image-resource: 1567 type: oci-image 1568 description: "An image" 1569 `)) 1570 c.Assert(err, gc.IsNil) 1571 1572 c.Check(meta.Resources, jc.DeepEquals, map[string]resource.Meta{ 1573 "resource-name": { 1574 Name: "resource-name", 1575 Type: resource.TypeFile, 1576 Path: "filename.tgz", 1577 Description: "One line that is useful when operators need to push it.", 1578 }, 1579 "other-resource": { 1580 Name: "other-resource", 1581 Type: resource.TypeFile, 1582 Path: "other.zip", 1583 }, 1584 "image-resource": { 1585 Name: "image-resource", 1586 Type: resource.TypeContainerImage, 1587 Description: "An image", 1588 }, 1589 }) 1590 } 1591 1592 func (s *MetaSuite) TestParseResourceMetaOkay(c *gc.C) { 1593 name := "my-resource" 1594 data := map[string]interface{}{ 1595 "type": "file", 1596 "filename": "filename.tgz", 1597 "description": "One line that is useful when operators need to push it.", 1598 } 1599 res, err := charm.ParseResourceMeta(name, data) 1600 c.Assert(err, jc.ErrorIsNil) 1601 1602 c.Check(res, jc.DeepEquals, resource.Meta{ 1603 Name: "my-resource", 1604 Type: resource.TypeFile, 1605 Path: "filename.tgz", 1606 Description: "One line that is useful when operators need to push it.", 1607 }) 1608 } 1609 1610 func (s *MetaSuite) TestParseResourceMetaMissingName(c *gc.C) { 1611 name := "" 1612 data := map[string]interface{}{ 1613 "type": "file", 1614 "filename": "filename.tgz", 1615 "description": "One line that is useful when operators need to push it.", 1616 } 1617 res, err := charm.ParseResourceMeta(name, data) 1618 c.Assert(err, jc.ErrorIsNil) 1619 1620 c.Check(res, jc.DeepEquals, resource.Meta{ 1621 Name: "", 1622 Type: resource.TypeFile, 1623 Path: "filename.tgz", 1624 Description: "One line that is useful when operators need to push it.", 1625 }) 1626 } 1627 1628 func (s *MetaSuite) TestParseResourceMetaMissingType(c *gc.C) { 1629 name := "my-resource" 1630 data := map[string]interface{}{ 1631 "filename": "filename.tgz", 1632 "description": "One line that is useful when operators need to push it.", 1633 } 1634 res, err := charm.ParseResourceMeta(name, data) 1635 c.Assert(err, jc.ErrorIsNil) 1636 1637 c.Check(res, jc.DeepEquals, resource.Meta{ 1638 Name: "my-resource", 1639 // Type is the zero value. 1640 Path: "filename.tgz", 1641 Description: "One line that is useful when operators need to push it.", 1642 }) 1643 } 1644 1645 func (s *MetaSuite) TestParseResourceMetaEmptyType(c *gc.C) { 1646 name := "my-resource" 1647 data := map[string]interface{}{ 1648 "type": "", 1649 "filename": "filename.tgz", 1650 "description": "One line that is useful when operators need to push it.", 1651 } 1652 _, err := charm.ParseResourceMeta(name, data) 1653 1654 c.Check(err, gc.ErrorMatches, `unsupported resource type .*`) 1655 } 1656 1657 func (s *MetaSuite) TestParseResourceMetaUnknownType(c *gc.C) { 1658 name := "my-resource" 1659 data := map[string]interface{}{ 1660 "type": "spam", 1661 "filename": "filename.tgz", 1662 "description": "One line that is useful when operators need to push it.", 1663 } 1664 _, err := charm.ParseResourceMeta(name, data) 1665 1666 c.Check(err, gc.ErrorMatches, `unsupported resource type .*`) 1667 } 1668 1669 func (s *MetaSuite) TestParseResourceMetaMissingPath(c *gc.C) { 1670 name := "my-resource" 1671 data := map[string]interface{}{ 1672 "type": "file", 1673 "description": "One line that is useful when operators need to push it.", 1674 } 1675 res, err := charm.ParseResourceMeta(name, data) 1676 c.Assert(err, jc.ErrorIsNil) 1677 1678 c.Check(res, jc.DeepEquals, resource.Meta{ 1679 Name: "my-resource", 1680 Type: resource.TypeFile, 1681 Path: "", 1682 Description: "One line that is useful when operators need to push it.", 1683 }) 1684 } 1685 1686 func (s *MetaSuite) TestParseResourceMetaMissingComment(c *gc.C) { 1687 name := "my-resource" 1688 data := map[string]interface{}{ 1689 "type": "file", 1690 "filename": "filename.tgz", 1691 } 1692 res, err := charm.ParseResourceMeta(name, data) 1693 c.Assert(err, jc.ErrorIsNil) 1694 1695 c.Check(res, jc.DeepEquals, resource.Meta{ 1696 Name: "my-resource", 1697 Type: resource.TypeFile, 1698 Path: "filename.tgz", 1699 Description: "", 1700 }) 1701 } 1702 1703 func (s *MetaSuite) TestParseResourceMetaEmpty(c *gc.C) { 1704 name := "my-resource" 1705 data := make(map[string]interface{}) 1706 res, err := charm.ParseResourceMeta(name, data) 1707 c.Assert(err, jc.ErrorIsNil) 1708 1709 c.Check(res, jc.DeepEquals, resource.Meta{ 1710 Name: "my-resource", 1711 }) 1712 } 1713 1714 func (s *MetaSuite) TestParseResourceMetaNil(c *gc.C) { 1715 name := "my-resource" 1716 var data map[string]interface{} 1717 res, err := charm.ParseResourceMeta(name, data) 1718 c.Assert(err, jc.ErrorIsNil) 1719 1720 c.Check(res, jc.DeepEquals, resource.Meta{ 1721 Name: "my-resource", 1722 }) 1723 } 1724 1725 func (s *MetaSuite) TestContainers(c *gc.C) { 1726 meta, err := charm.ReadMeta(strings.NewReader(` 1727 name: a 1728 summary: b 1729 description: c 1730 containers: 1731 foo: 1732 resource: test-os 1733 mounts: 1734 - storage: a 1735 location: /b/ 1736 resources: 1737 test-os: 1738 type: oci-image 1739 storage: 1740 a: 1741 type: filesystem 1742 `)) 1743 c.Assert(err, gc.IsNil) 1744 c.Assert(meta.Containers, jc.DeepEquals, map[string]charm.Container{ 1745 "foo": { 1746 Resource: "test-os", 1747 Mounts: []charm.Mount{{ 1748 Storage: "a", 1749 Location: "/b/", 1750 }}, 1751 }, 1752 }) 1753 } 1754 1755 func (s *MetaSuite) TestSystemReferencesFileResource(c *gc.C) { 1756 _, err := charm.ReadMeta(strings.NewReader(` 1757 name: a 1758 summary: b 1759 description: c 1760 containers: 1761 foo: 1762 resource: test-os 1763 mounts: 1764 - storage: a 1765 location: /b/ 1766 resources: 1767 test-os: 1768 type: file 1769 filename: test.json 1770 storage: 1771 a: 1772 type: filesystem 1773 `)) 1774 c.Assert(err, gc.ErrorMatches, `parsing containers: referenced resource "test-os" is not a oci-image`) 1775 } 1776 1777 func (s *MetaSuite) TestSystemReferencedMissingResource(c *gc.C) { 1778 _, err := charm.ReadMeta(strings.NewReader(` 1779 name: a 1780 summary: b 1781 description: c 1782 containers: 1783 foo: 1784 resource: test-os 1785 mounts: 1786 - storage: a 1787 location: /b/ 1788 storage: 1789 a: 1790 type: filesystem 1791 `)) 1792 c.Assert(err, gc.ErrorMatches, `parsing containers: referenced resource "test-os" not found`) 1793 } 1794 1795 func (s *MetaSuite) TestMountMissingStorage(c *gc.C) { 1796 _, err := charm.ReadMeta(strings.NewReader(` 1797 name: a 1798 summary: b 1799 description: c 1800 containers: 1801 foo: 1802 resource: test-os 1803 mounts: 1804 - location: /b/ 1805 resources: 1806 test-os: 1807 type: oci-image 1808 storage: 1809 a: 1810 type: filesystem 1811 `)) 1812 c.Assert(err, gc.ErrorMatches, `parsing containers: container "foo": storage must be specifed on mount`) 1813 } 1814 1815 func (s *MetaSuite) TestMountMissingLocation(c *gc.C) { 1816 _, err := charm.ReadMeta(strings.NewReader(` 1817 name: a 1818 summary: b 1819 description: c 1820 containers: 1821 foo: 1822 resource: test-os 1823 mounts: 1824 - storage: a 1825 resources: 1826 test-os: 1827 type: oci-image 1828 storage: 1829 a: 1830 type: filesystem 1831 `)) 1832 c.Assert(err, gc.ErrorMatches, `parsing containers: container "foo": location must be specifed on mount`) 1833 } 1834 1835 func (s *MetaSuite) TestMountIncorrectStorage(c *gc.C) { 1836 _, err := charm.ReadMeta(strings.NewReader(` 1837 name: a 1838 summary: b 1839 description: c 1840 containers: 1841 foo: 1842 resource: test-os 1843 mounts: 1844 - storage: b 1845 location: /b/ 1846 resources: 1847 test-os: 1848 type: oci-image 1849 storage: 1850 a: 1851 type: filesystem 1852 `)) 1853 c.Assert(err, gc.ErrorMatches, `parsing containers: container "foo": storage "b" not valid`) 1854 } 1855 1856 func (s *MetaSuite) TestFormatV1AndV2Mixing(c *gc.C) { 1857 _, err := charm.ReadMeta(strings.NewReader(` 1858 name: a 1859 summary: b 1860 description: c 1861 series: 1862 - focal 1863 containers: 1864 foo: 1865 resource: test-os 1866 mounts: 1867 - storage: a 1868 location: /b/ 1869 resources: 1870 test-os: 1871 type: oci-image 1872 storage: 1873 a: 1874 type: filesystem 1875 `)) 1876 c.Assert(err, gc.ErrorMatches, `ambiguous metadata: keys "series" cannot be used with "containers"`) 1877 } 1878 1879 type dummyCharm struct{} 1880 1881 func (c *dummyCharm) Version() string { 1882 panic("unused") 1883 } 1884 1885 func (c *dummyCharm) Config() *charm.Config { 1886 panic("unused") 1887 } 1888 1889 func (c *dummyCharm) Metrics() *charm.Metrics { 1890 panic("unused") 1891 } 1892 1893 func (c *dummyCharm) Actions() *charm.Actions { 1894 panic("unused") 1895 } 1896 1897 func (c *dummyCharm) LXDProfile() *charm.LXDProfile { 1898 panic("unused") 1899 } 1900 1901 func (c *dummyCharm) Manifest() *charm.Manifest { 1902 panic("unused") 1903 } 1904 1905 func (c *dummyCharm) Revision() int { 1906 panic("unused") 1907 } 1908 1909 func (c *dummyCharm) Meta() *charm.Meta { 1910 return &charm.Meta{ 1911 Provides: map[string]charm.Relation{ 1912 "pro": {Interface: "ifce-pro", Scope: charm.ScopeGlobal}, 1913 }, 1914 Requires: map[string]charm.Relation{ 1915 "req": {Interface: "ifce-req", Scope: charm.ScopeGlobal}, 1916 "info": {Interface: "juju-info", Scope: charm.ScopeContainer}, 1917 }, 1918 Peers: map[string]charm.Relation{ 1919 "peer": {Interface: "ifce-peer", Scope: charm.ScopeGlobal}, 1920 }, 1921 } 1922 } 1923 1924 type FormatMetaSuite struct{} 1925 1926 var _ = gc.Suite(&FormatMetaSuite{}) 1927 1928 func (FormatMetaSuite) TestCheckV1(c *gc.C) { 1929 meta := charm.Meta{} 1930 err := meta.Check(charm.FormatV1) 1931 c.Assert(err, jc.ErrorIsNil) 1932 } 1933 1934 func (FormatMetaSuite) TestCheckV1WithAssumes(c *gc.C) { 1935 meta := charm.Meta{ 1936 Assumes: new(assumes.ExpressionTree), 1937 } 1938 err := meta.Check(charm.FormatV1) 1939 c.Assert(err, gc.ErrorMatches, `assumes in metadata v1 not valid`) 1940 } 1941 1942 func (FormatMetaSuite) TestCheckV1WithContainers(c *gc.C) { 1943 meta := charm.Meta{ 1944 Containers: map[string]charm.Container{ 1945 "foo": { 1946 Resource: "test-os", 1947 Mounts: []charm.Mount{{ 1948 Storage: "a", 1949 Location: "/b/", 1950 }}, 1951 }, 1952 }, 1953 } 1954 err := meta.Check(charm.FormatV1) 1955 c.Assert(err, gc.ErrorMatches, `containers without a manifest.yaml not valid`) 1956 } 1957 1958 func (FormatMetaSuite) TestCheckV1WithContainersWithManifest(c *gc.C) { 1959 meta := charm.Meta{ 1960 Containers: map[string]charm.Container{ 1961 "foo": { 1962 Resource: "test-os", 1963 Mounts: []charm.Mount{{ 1964 Storage: "a", 1965 Location: "/b/", 1966 }}, 1967 }, 1968 }, 1969 } 1970 err := meta.Check(charm.FormatV1, charm.SelectionManifest) 1971 c.Assert(err, gc.ErrorMatches, `containers in metadata v1 not valid`) 1972 } 1973 1974 func (FormatMetaSuite) TestCheckV2(c *gc.C) { 1975 meta := charm.Meta{} 1976 err := meta.Check(charm.FormatV2, charm.SelectionManifest, charm.SelectionBases) 1977 c.Assert(err, jc.ErrorIsNil) 1978 } 1979 1980 func (FormatMetaSuite) TestCheckV2NoReasons(c *gc.C) { 1981 meta := charm.Meta{} 1982 err := meta.Check(charm.FormatV2) 1983 c.Assert(err, gc.ErrorMatches, `metadata v2 without manifest.yaml not valid`) 1984 } 1985 1986 func (FormatMetaSuite) TestCheckV2WithSeries(c *gc.C) { 1987 meta := charm.Meta{ 1988 Series: []string{"bionic"}, 1989 } 1990 err := meta.Check(charm.FormatV2, charm.SelectionManifest, charm.SelectionBases) 1991 c.Assert(err, gc.ErrorMatches, `metadata v2 manifest.yaml with series slice not valid`) 1992 } 1993 1994 func (FormatMetaSuite) TestCheckV2WithSeriesWithoutManifest(c *gc.C) { 1995 meta := charm.Meta{ 1996 Series: []string{"bionic"}, 1997 } 1998 err := meta.Check(charm.FormatV2, charm.SelectionBases) 1999 c.Assert(err, gc.ErrorMatches, `series slice in metadata v2 not valid`) 2000 } 2001 2002 func (FormatMetaSuite) TestCheckV2WithMinJujuVersion(c *gc.C) { 2003 meta := charm.Meta{ 2004 MinJujuVersion: version.MustParse("2.0.0"), 2005 } 2006 err := meta.Check(charm.FormatV2, charm.SelectionManifest, charm.SelectionBases) 2007 c.Assert(err, gc.ErrorMatches, `min-juju-version in metadata v2 not valid`) 2008 } 2009 2010 func (FormatMetaSuite) TestCheckV2WithDeployment(c *gc.C) { 2011 meta := charm.Meta{ 2012 Deployment: &charm.Deployment{}, 2013 } 2014 err := meta.Check(charm.FormatV2, charm.SelectionManifest, charm.SelectionBases) 2015 c.Assert(err, gc.ErrorMatches, `deployment in metadata v2 not valid`) 2016 }