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  }