Permalink
Fetching contributors…
Cannot retrieve contributors at this time
811 lines (781 sloc) 19.9 KB
// Copyright 2014 Canonical Ltd.
// Licensed under the LGPLv3, see LICENCE file for details.
package charm_test
import (
"fmt"
"sort"
"strings"
"github.com/juju/testing"
jc "github.com/juju/testing/checkers"
gc "gopkg.in/check.v1"
"gopkg.in/juju/charm.v5"
)
type bundleDataSuite struct {
testing.IsolationSuite
}
var _ = gc.Suite(&bundleDataSuite{})
const mediawikiBundle = `
series: precise
services:
mediawiki:
charm: "cs:precise/mediawiki-10"
num_units: 1
options:
debug: false
name: Please set name of wiki
skin: vector
annotations:
"gui-x": 609
"gui-y": -15
mysql:
charm: "cs:precise/mysql-28"
num_units: 2
to: [0, mediawiki/0]
options:
"binlog-format": MIXED
"block-size": 5
"dataset-size": "80%"
flavor: distro
"ha-bindiface": eth0
"ha-mcastport": 5411
annotations:
"gui-x": 610
"gui-y": 255
constraints: "mem=8g"
relations:
- ["mediawiki:db", "mysql:db"]
- ["mysql:foo", "mediawiki:bar"]
machines:
0:
constraints: 'arch=amd64 mem=4g'
annotations:
foo: bar
tags:
- super
- awesome
description: |
Everything is awesome. Everything is cool when we work as a team.
Lovely day.
`
var parseTests = []struct {
about string
data string
expectedBD *charm.BundleData
expectedErr string
}{{
about: "mediawiki",
data: mediawikiBundle,
expectedBD: &charm.BundleData{
Series: "precise",
Services: map[string]*charm.ServiceSpec{
"mediawiki": {
Charm: "cs:precise/mediawiki-10",
NumUnits: 1,
Options: map[string]interface{}{
"debug": false,
"name": "Please set name of wiki",
"skin": "vector",
},
Annotations: map[string]string{
"gui-x": "609",
"gui-y": "-15",
},
},
"mysql": {
Charm: "cs:precise/mysql-28",
NumUnits: 2,
To: []string{"0", "mediawiki/0"},
Options: map[string]interface{}{
"binlog-format": "MIXED",
"block-size": 5,
"dataset-size": "80%",
"flavor": "distro",
"ha-bindiface": "eth0",
"ha-mcastport": 5411,
},
Annotations: map[string]string{
"gui-x": "610",
"gui-y": "255",
},
Constraints: "mem=8g",
},
},
Machines: map[string]*charm.MachineSpec{
"0": {
Constraints: "arch=amd64 mem=4g",
Annotations: map[string]string{
"foo": "bar",
},
},
},
Relations: [][]string{
{"mediawiki:db", "mysql:db"},
{"mysql:foo", "mediawiki:bar"},
},
Tags: []string{"super", "awesome"},
Description: `Everything is awesome. Everything is cool when we work as a team.
Lovely day.
`,
},
}, {
about: "relations specified with hyphens",
data: `
relations:
- - "mediawiki:db"
- "mysql:db"
- - "mysql:foo"
- "mediawiki:bar"
`,
expectedBD: &charm.BundleData{
Relations: [][]string{
{"mediawiki:db", "mysql:db"},
{"mysql:foo", "mediawiki:bar"},
},
},
}}
func (*bundleDataSuite) TestParse(c *gc.C) {
for i, test := range parseTests {
c.Logf("test %d: %s", i, test.about)
bd, err := charm.ReadBundleData(strings.NewReader(test.data))
if test.expectedErr != "" {
c.Assert(err, gc.ErrorMatches, test.expectedErr)
continue
}
c.Assert(err, gc.IsNil)
c.Assert(bd, jc.DeepEquals, test.expectedBD)
}
}
var verifyErrorsTests = []struct {
about string
data string
errors []string
}{{
about: "as many errors as possible",
data: `
series: "9wrong"
machines:
0:
constraints: 'bad constraints'
annotations:
foo: bar
series: 'bad series'
bogus:
3:
services:
mediawiki:
charm: "bogus:precise/mediawiki-10"
num_units: -4
options:
debug: false
name: Please set name of wiki
skin: vector
annotations:
"gui-x": 609
"gui-y": -15
mysql:
charm: "cs:precise/mysql-28"
num_units: 2
to: [0, mediawiki/0, nowhere/3, 2, "bad placement"]
options:
"binlog-format": MIXED
"block-size": 5
"dataset-size": "80%"
flavor: distro
"ha-bindiface": eth0
"ha-mcastport": 5411
annotations:
"gui-x": 610
"gui-y": 255
constraints: "bad constraints"
wordpress:
charm: wordpress
relations:
- ["mediawiki:db", "mysql:db"]
- ["mysql:foo", "mediawiki:bar"]
- ["arble:bar"]
- ["arble:bar", "mediawiki:db"]
- ["mysql:foo", "mysql:bar"]
- ["mysql:db", "mediawiki:db"]
- ["mediawiki/db", "mysql:db"]
- ["wordpress", "mysql"]
`,
errors: []string{
`bundle declares an invalid series "9wrong"`,
`machine "3" is not referred to by a placement directive`,
`machine "bogus" is not referred to by a placement directive`,
`invalid machine id "bogus" found in machines`,
`invalid constraints "bad constraints" in machine "0": bad constraint`,
`invalid charm URL in service "mediawiki": charm URL has invalid schema: "bogus:precise/mediawiki-10"`,
`invalid constraints "bad constraints" in service "mysql": bad constraint`,
`negative number of units specified on service "mediawiki"`,
`too many units specified in unit placement for service "mysql"`,
`placement "nowhere/3" refers to a service not defined in this bundle`,
`placement "mediawiki/0" specifies a unit greater than the -4 unit(s) started by the target service`,
`placement "2" refers to a machine not defined in this bundle`,
`relation ["arble:bar"] has 1 endpoint(s), not 2`,
`relation ["arble:bar" "mediawiki:db"] refers to service "arble" not defined in this bundle`,
`relation ["mysql:foo" "mysql:bar"] relates a service to itself`,
`relation ["mysql:db" "mediawiki:db"] is defined more than once`,
`invalid placement syntax "bad placement"`,
`invalid relation syntax "mediawiki/db"`,
`invalid series bad series for machine "0"`,
},
}, {
about: "mediawiki should be ok",
data: mediawikiBundle,
}}
func (*bundleDataSuite) TestVerifyErrors(c *gc.C) {
for i, test := range verifyErrorsTests {
c.Logf("test %d: %s", i, test.about)
assertVerifyWithCharmsErrors(c, test.data, nil, test.errors)
}
}
func assertVerifyWithCharmsErrors(c *gc.C, bundleData string, charms map[string]charm.Charm, expectErrors []string) {
bd, err := charm.ReadBundleData(strings.NewReader(bundleData))
c.Assert(err, gc.IsNil)
err = bd.VerifyWithCharms(func(c string) error {
if c == "bad constraints" {
return fmt.Errorf("bad constraint")
}
return nil
}, charms)
if len(expectErrors) == 0 {
if err == nil {
return
}
// Let the rest of the function deal with the
// error, so that we'll see the actual errors
// that resulted.
}
c.Assert(err, gc.FitsTypeOf, (*charm.VerificationError)(nil))
errors := err.(*charm.VerificationError).Errors
errStrings := make([]string, len(errors))
for i, err := range errors {
errStrings[i] = err.Error()
}
sort.Strings(errStrings)
sort.Strings(expectErrors)
c.Assert(errStrings, jc.DeepEquals, expectErrors)
}
func (*bundleDataSuite) TestVerifyCharmURL(c *gc.C) {
bd, err := charm.ReadBundleData(strings.NewReader(mediawikiBundle))
c.Assert(err, gc.IsNil)
for i, u := range []string{
"wordpress",
"cs:wordpress",
"cs:precise/wordpress",
"precise/wordpress",
"precise/wordpress-2",
"local:foo",
"local:foo-45",
} {
c.Logf("test %d: %s", i, u)
bd.Services["mediawiki"].Charm = u
err := bd.Verify(nil)
c.Assert(err, gc.IsNil, gc.Commentf("charm url %q", u))
}
}
func (*bundleDataSuite) TestVerifyBundleUsingJujuInfoRelation(c *gc.C) {
b := TestCharms.BundleDir("wordpress-with-logging")
bd := b.Data()
charms := map[string]charm.Charm{
"wordpress": TestCharms.CharmDir("wordpress"),
"mysql": TestCharms.CharmDir("mysql"),
"logging": TestCharms.CharmDir("logging"),
}
err := bd.VerifyWithCharms(nil, charms)
c.Assert(err, gc.IsNil)
}
func (*bundleDataSuite) TestRequiredCharms(c *gc.C) {
bd, err := charm.ReadBundleData(strings.NewReader(mediawikiBundle))
c.Assert(err, gc.IsNil)
reqCharms := bd.RequiredCharms()
c.Assert(reqCharms, gc.DeepEquals, []string{"cs:precise/mediawiki-10", "cs:precise/mysql-28"})
}
// testCharm returns a charm with the given name
// and relations. The relations are specified as
// a string of the form:
//
// <provides-relations> | <requires-relations>
//
// Within each section, each white-space separated
// relation is specified as:
/// <relation-name>:<interface>
//
// So, for example:
//
// testCharm("wordpress", "web:http | db:mysql")
//
// is equivalent to a charm with metadata.yaml containing
//
// name: wordpress
// description: wordpress
// provides:
// web:
// interface: http
// requires:
// db:
// interface: mysql
//
func testCharm(name string, relations string) charm.Charm {
var provides, requires string
parts := strings.Split(relations, "|")
provides = parts[0]
if len(parts) > 1 {
requires = parts[1]
}
meta := &charm.Meta{
Name: name,
Summary: name,
Description: name,
Provides: parseRelations(provides, charm.RoleProvider),
Requires: parseRelations(requires, charm.RoleRequirer),
}
configStr := `
options:
title: {default: My Title, description: title, type: string}
skill-level: {description: skill, type: int}
`
config, err := charm.ReadConfig(strings.NewReader(configStr))
if err != nil {
panic(err)
}
return testCharmImpl{
meta: meta,
config: config,
}
}
func parseRelations(s string, role charm.RelationRole) map[string]charm.Relation {
rels := make(map[string]charm.Relation)
for _, r := range strings.Fields(s) {
parts := strings.Split(r, ":")
if len(parts) != 2 {
panic(fmt.Errorf("invalid relation specifier %q", r))
}
name, interf := parts[0], parts[1]
rels[name] = charm.Relation{
Name: name,
Role: role,
Interface: interf,
Scope: charm.ScopeGlobal,
}
}
return rels
}
type testCharmImpl struct {
meta *charm.Meta
config *charm.Config
// Implement charm.Charm, but panic if anything other than
// Meta or Config methods are called.
charm.Charm
}
func (c testCharmImpl) Meta() *charm.Meta {
return c.meta
}
func (c testCharmImpl) Config() *charm.Config {
return c.config
}
var verifyWithCharmsErrorsTests = []struct {
about string
data string
charms map[string]charm.Charm
errors []string
}{{
about: "no charms",
data: mediawikiBundle,
charms: map[string]charm.Charm{},
errors: []string{
`service "mediawiki" refers to non-existent charm "cs:precise/mediawiki-10"`,
`service "mysql" refers to non-existent charm "cs:precise/mysql-28"`,
},
}, {
about: "all present and correct",
data: `
services:
service1:
charm: "test"
service2:
charm: "test"
service3:
charm: "test"
relations:
- ["service1:prova", "service2:reqa"]
- ["service1:reqa", "service3:prova"]
- ["service3:provb", "service2:reqb"]
`,
charms: map[string]charm.Charm{
"test": testCharm("test", "prova:a provb:b | reqa:a reqb:b"),
},
}, {
about: "undefined relations",
data: `
services:
service1:
charm: "test"
service2:
charm: "test"
relations:
- ["service1:prova", "service2:blah"]
- ["service1:blah", "service2:prova"]
`,
charms: map[string]charm.Charm{
"test": testCharm("test", "prova:a provb:b | reqa:a reqb:b"),
},
errors: []string{
`charm "test" used by service "service1" does not define relation "blah"`,
`charm "test" used by service "service2" does not define relation "blah"`,
},
}, {
about: "undefined services",
data: `
services:
service1:
charm: "test"
service2:
charm: "test"
relations:
- ["unknown:prova", "service2:blah"]
- ["service1:blah", "unknown:prova"]
`,
charms: map[string]charm.Charm{
"test": testCharm("test", "prova:a provb:b | reqa:a reqb:b"),
},
errors: []string{
`relation ["service1:blah" "unknown:prova"] refers to service "unknown" not defined in this bundle`,
`relation ["unknown:prova" "service2:blah"] refers to service "unknown" not defined in this bundle`,
},
}, {
about: "equal services",
data: `
services:
service1:
charm: "test"
service2:
charm: "test"
relations:
- ["service2:prova", "service2:reqa"]
`,
charms: map[string]charm.Charm{
"test": testCharm("test", "prova:a provb:b | reqa:a reqb:b"),
},
errors: []string{
`relation ["service2:prova" "service2:reqa"] relates a service to itself`,
},
}, {
about: "provider to provider relation",
data: `
services:
service1:
charm: "test"
service2:
charm: "test"
relations:
- ["service1:prova", "service2:prova"]
`,
charms: map[string]charm.Charm{
"test": testCharm("test", "prova:a provb:b | reqa:a reqb:b"),
},
errors: []string{
`relation "service1:prova" to "service2:prova" relates provider to provider`,
},
}, {
about: "provider to provider relation",
data: `
services:
service1:
charm: "test"
service2:
charm: "test"
relations:
- ["service1:reqa", "service2:reqa"]
`,
charms: map[string]charm.Charm{
"test": testCharm("test", "prova:a provb:b | reqa:a reqb:b"),
},
errors: []string{
`relation "service1:reqa" to "service2:reqa" relates requirer to requirer`,
},
}, {
about: "interface mismatch",
data: `
services:
service1:
charm: "test"
service2:
charm: "test"
relations:
- ["service1:reqa", "service2:provb"]
`,
charms: map[string]charm.Charm{
"test": testCharm("test", "prova:a provb:b | reqa:a reqb:b"),
},
errors: []string{
`mismatched interface between "service2:provb" and "service1:reqa" ("b" vs "a")`,
},
}, {
about: "different charms",
data: `
services:
service1:
charm: "test1"
service2:
charm: "test2"
relations:
- ["service1:reqa", "service2:prova"]
`,
charms: map[string]charm.Charm{
"test1": testCharm("test", "prova:a provb:b | reqa:a reqb:b"),
"test2": testCharm("test", ""),
},
errors: []string{
`charm "test2" used by service "service2" does not define relation "prova"`,
},
}, {
about: "ambiguous relation",
data: `
services:
service1:
charm: "test1"
service2:
charm: "test2"
relations:
- [service1, service2]
`,
charms: map[string]charm.Charm{
"test1": testCharm("test", "prova:a provb:b | reqa:a reqb:b"),
"test2": testCharm("test", "prova:a provb:b | reqa:a reqb:b"),
},
errors: []string{
`cannot infer endpoint between service1 and service2: ambiguous relation: service1 service2 could refer to "service1:prova service2:reqa"; "service1:provb service2:reqb"; "service1:reqa service2:prova"; "service1:reqb service2:provb"`,
},
}, {
about: "relation using juju-info",
data: `
services:
service1:
charm: "provider"
service2:
charm: "requirer"
relations:
- [service1, service2]
`,
charms: map[string]charm.Charm{
"provider": testCharm("provider", ""),
"requirer": testCharm("requirer", "| req:juju-info"),
},
}, {
about: "ambiguous when implicit relations taken into account",
data: `
services:
service1:
charm: "provider"
service2:
charm: "requirer"
relations:
- [service1, service2]
`,
charms: map[string]charm.Charm{
"provider": testCharm("provider", "provdb:db | "),
"requirer": testCharm("requirer", "| reqdb:db reqinfo:juju-info"),
},
}, {
about: "half of relation left open",
data: `
services:
service1:
charm: "provider"
service2:
charm: "requirer"
relations:
- ["service1:prova2", service2]
`,
charms: map[string]charm.Charm{
"provider": testCharm("provider", "prova1:a prova2:a | "),
"requirer": testCharm("requirer", "| reqa:a"),
},
}, {
about: "duplicate relation between open and fully-specified relations",
data: `
services:
service1:
charm: "provider"
service2:
charm: "requirer"
relations:
- ["service1:prova", "service2:reqa"]
- ["service1", "service2"]
`,
charms: map[string]charm.Charm{
"provider": testCharm("provider", "prova:a | "),
"requirer": testCharm("requirer", "| reqa:a"),
},
errors: []string{
`relation ["service1" "service2"] is defined more than once`,
},
}, {
about: "configuration options specified",
data: `
services:
service1:
charm: "test"
options:
title: "some title"
skill-level: 245
service2:
charm: "test"
options:
title: "another title"
`,
charms: map[string]charm.Charm{
"test": testCharm("test", "prova:a provb:b | reqa:a reqb:b"),
},
}, {
about: "invalid type for option",
data: `
services:
service1:
charm: "test"
options:
title: "some title"
skill-level: "too much"
service2:
charm: "test"
options:
title: "another title"
`,
charms: map[string]charm.Charm{
"test": testCharm("test", "prova:a provb:b | reqa:a reqb:b"),
},
errors: []string{
`cannot validate service "service1": option "skill-level" expected int, got "too much"`,
},
}, {
about: "unknown option",
data: `
services:
service1:
charm: "test"
options:
title: "some title"
unknown-option: 2345
`,
charms: map[string]charm.Charm{
"test": testCharm("test", "prova:a provb:b | reqa:a reqb:b"),
},
errors: []string{
`cannot validate service "service1": configuration option "unknown-option" not found in charm "test"`,
},
}, {
about: "multiple config problems",
data: `
services:
service1:
charm: "test"
options:
title: "some title"
unknown-option: 2345
service2:
charm: "test"
options:
title: 123
another-unknown: 2345
`,
charms: map[string]charm.Charm{
"test": testCharm("test", "prova:a provb:b | reqa:a reqb:b"),
},
errors: []string{
`cannot validate service "service1": configuration option "unknown-option" not found in charm "test"`,
`cannot validate service "service2": configuration option "another-unknown" not found in charm "test"`,
`cannot validate service "service2": option "title" expected string, got 123`,
},
}}
func (*bundleDataSuite) TestVerifyWithCharmsErrors(c *gc.C) {
for i, test := range verifyWithCharmsErrorsTests {
c.Logf("test %d: %s", i, test.about)
assertVerifyWithCharmsErrors(c, test.data, test.charms, test.errors)
}
}
var parsePlacementTests = []struct {
placement string
expect *charm.UnitPlacement
expectErr string
}{{
placement: "lxc:service/0",
expect: &charm.UnitPlacement{
ContainerType: "lxc",
Service: "service",
Unit: 0,
},
}, {
placement: "lxc:service",
expect: &charm.UnitPlacement{
ContainerType: "lxc",
Service: "service",
Unit: -1,
},
}, {
placement: "lxc:99",
expect: &charm.UnitPlacement{
ContainerType: "lxc",
Machine: "99",
Unit: -1,
},
}, {
placement: "lxc:new",
expect: &charm.UnitPlacement{
ContainerType: "lxc",
Machine: "new",
Unit: -1,
},
}, {
placement: "service/0",
expect: &charm.UnitPlacement{
Service: "service",
Unit: 0,
},
}, {
placement: "service",
expect: &charm.UnitPlacement{
Service: "service",
Unit: -1,
},
}, {
placement: "service45",
expect: &charm.UnitPlacement{
Service: "service45",
Unit: -1,
},
}, {
placement: "99",
expect: &charm.UnitPlacement{
Machine: "99",
Unit: -1,
},
}, {
placement: "new",
expect: &charm.UnitPlacement{
Machine: "new",
Unit: -1,
},
}, {
placement: ":0",
expectErr: `invalid placement syntax ":0"`,
}, {
placement: "05",
expectErr: `invalid placement syntax "05"`,
}, {
placement: "new/2",
expectErr: `invalid placement syntax "new/2"`,
}}
func (*bundleDataSuite) TestParsePlacement(c *gc.C) {
for i, test := range parsePlacementTests {
c.Logf("test %d: %q", i, test.placement)
up, err := charm.ParsePlacement(test.placement)
if test.expectErr != "" {
c.Assert(err, gc.ErrorMatches, test.expectErr)
} else {
c.Assert(err, gc.IsNil)
c.Assert(up, jc.DeepEquals, test.expect)
}
}
}