launchpad.net/~rogpeppe/juju-core/500-errgo-fix@v0.0.0-20140213181702-000000002356/charm/config_test.go (about)

     1  // Copyright 2011, 2012, 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package charm_test
     5  
     6  import (
     7  	"bytes"
     8  	"fmt"
     9  
    10  	gc "launchpad.net/gocheck"
    11  
    12  	"launchpad.net/juju-core/charm"
    13  )
    14  
    15  type ConfigSuite struct {
    16  	config *charm.Config
    17  }
    18  
    19  var _ = gc.Suite(&ConfigSuite{})
    20  
    21  func (s *ConfigSuite) SetUpSuite(c *gc.C) {
    22  	// Just use a single shared config for the whole suite. There's no use case
    23  	// for mutating a config, we we assume that nobody will do so here.
    24  	var err error
    25  	s.config, err = charm.ReadConfig(bytes.NewBuffer([]byte(`
    26  options:
    27    title:
    28      default: My Title
    29      description: A descriptive title used for the service.
    30      type: string
    31    subtitle:
    32      default: ""
    33      description: An optional subtitle used for the service.
    34    outlook:
    35      description: No default outlook.
    36      # type defaults to string in python
    37    username:
    38      default: admin001
    39      description: The name of the initial account (given admin permissions).
    40      type: string
    41    skill-level:
    42      description: A number indicating skill.
    43      type: int
    44    agility-ratio:
    45      description: A number from 0 to 1 indicating agility.
    46      type: float
    47    reticulate-splines:
    48      description: Whether to reticulate splines on launch, or not.
    49      type: boolean
    50  `)))
    51  	c.Assert(err, gc.IsNil)
    52  }
    53  
    54  func (s *ConfigSuite) TestReadSample(c *gc.C) {
    55  	c.Assert(s.config.Options, gc.DeepEquals, map[string]charm.Option{
    56  		"title": {
    57  			Default:     "My Title",
    58  			Description: "A descriptive title used for the service.",
    59  			Type:        "string",
    60  		},
    61  		"subtitle": {
    62  			Default:     "",
    63  			Description: "An optional subtitle used for the service.",
    64  			Type:        "string",
    65  		},
    66  		"username": {
    67  			Default:     "admin001",
    68  			Description: "The name of the initial account (given admin permissions).",
    69  			Type:        "string",
    70  		},
    71  		"outlook": {
    72  			Description: "No default outlook.",
    73  			Type:        "string",
    74  		},
    75  		"skill-level": {
    76  			Description: "A number indicating skill.",
    77  			Type:        "int",
    78  		},
    79  		"agility-ratio": {
    80  			Description: "A number from 0 to 1 indicating agility.",
    81  			Type:        "float",
    82  		},
    83  		"reticulate-splines": {
    84  			Description: "Whether to reticulate splines on launch, or not.",
    85  			Type:        "boolean",
    86  		},
    87  	})
    88  }
    89  
    90  func (s *ConfigSuite) TestDefaultSettings(c *gc.C) {
    91  	c.Assert(s.config.DefaultSettings(), gc.DeepEquals, charm.Settings{
    92  		"title":              "My Title",
    93  		"subtitle":           "",
    94  		"username":           "admin001",
    95  		"outlook":            nil,
    96  		"skill-level":        nil,
    97  		"agility-ratio":      nil,
    98  		"reticulate-splines": nil,
    99  	})
   100  }
   101  
   102  func (s *ConfigSuite) TestFilterSettings(c *gc.C) {
   103  	settings := s.config.FilterSettings(charm.Settings{
   104  		"title":              "something valid",
   105  		"username":           nil,
   106  		"unknown":            "whatever",
   107  		"outlook":            "",
   108  		"skill-level":        5.5,
   109  		"agility-ratio":      true,
   110  		"reticulate-splines": "hullo",
   111  	})
   112  	c.Assert(settings, gc.DeepEquals, charm.Settings{
   113  		"title":    "something valid",
   114  		"username": nil,
   115  		"outlook":  "",
   116  	})
   117  }
   118  
   119  func (s *ConfigSuite) TestValidateSettings(c *gc.C) {
   120  	for i, test := range []struct {
   121  		info   string
   122  		input  charm.Settings
   123  		expect charm.Settings
   124  		err    string
   125  	}{{
   126  		info:   "nil settings are valid",
   127  		expect: charm.Settings{},
   128  	}, {
   129  		info:  "empty settings are valid",
   130  		input: charm.Settings{},
   131  	}, {
   132  		info:  "unknown keys are not valid",
   133  		input: charm.Settings{"foo": nil},
   134  		err:   `unknown option "foo"`,
   135  	}, {
   136  		info: "nil is valid for every value type",
   137  		input: charm.Settings{
   138  			"outlook":            nil,
   139  			"skill-level":        nil,
   140  			"agility-ratio":      nil,
   141  			"reticulate-splines": nil,
   142  		},
   143  	}, {
   144  		info: "correctly-typed values are valid",
   145  		input: charm.Settings{
   146  			"outlook":            "stormy",
   147  			"skill-level":        int64(123),
   148  			"agility-ratio":      0.5,
   149  			"reticulate-splines": true,
   150  		},
   151  	}, {
   152  		info:   "empty string-typed values stay empty",
   153  		input:  charm.Settings{"outlook": ""},
   154  		expect: charm.Settings{"outlook": ""},
   155  	}, {
   156  		info: "almost-correctly-typed values are valid",
   157  		input: charm.Settings{
   158  			"skill-level":   123,
   159  			"agility-ratio": float32(0.5),
   160  		},
   161  		expect: charm.Settings{
   162  			"skill-level":   int64(123),
   163  			"agility-ratio": 0.5,
   164  		},
   165  	}, {
   166  		info:  "bad string",
   167  		input: charm.Settings{"outlook": false},
   168  		err:   `option "outlook" expected string, got false`,
   169  	}, {
   170  		info:  "bad int",
   171  		input: charm.Settings{"skill-level": 123.4},
   172  		err:   `option "skill-level" expected int, got 123.4`,
   173  	}, {
   174  		info:  "bad float",
   175  		input: charm.Settings{"agility-ratio": "cheese"},
   176  		err:   `option "agility-ratio" expected float, got "cheese"`,
   177  	}, {
   178  		info:  "bad boolean",
   179  		input: charm.Settings{"reticulate-splines": 101},
   180  		err:   `option "reticulate-splines" expected boolean, got 101`,
   181  	}} {
   182  		c.Logf("test %d: %s", i, test.info)
   183  		result, err := s.config.ValidateSettings(test.input)
   184  		if test.err != "" {
   185  			c.Check(err, gc.ErrorMatches, test.err)
   186  		} else {
   187  			c.Check(err, gc.IsNil)
   188  			if test.expect == nil {
   189  				c.Check(result, gc.DeepEquals, test.input)
   190  			} else {
   191  				c.Check(result, gc.DeepEquals, test.expect)
   192  			}
   193  		}
   194  	}
   195  }
   196  
   197  var settingsWithNils = charm.Settings{
   198  	"outlook":            nil,
   199  	"skill-level":        nil,
   200  	"agility-ratio":      nil,
   201  	"reticulate-splines": nil,
   202  }
   203  
   204  var settingsWithValues = charm.Settings{
   205  	"outlook":            "whatever",
   206  	"skill-level":        int64(123),
   207  	"agility-ratio":      2.22,
   208  	"reticulate-splines": true,
   209  }
   210  
   211  func (s *ConfigSuite) TestParseSettingsYAML(c *gc.C) {
   212  	for i, test := range []struct {
   213  		info   string
   214  		yaml   string
   215  		key    string
   216  		expect charm.Settings
   217  		err    string
   218  	}{{
   219  		info: "bad structure",
   220  		yaml: "`",
   221  		err:  `cannot parse settings data: .*`,
   222  	}, {
   223  		info: "bad key",
   224  		yaml: "{}",
   225  		key:  "blah",
   226  		err:  `no settings found for "blah"`,
   227  	}, {
   228  		info: "bad settings key",
   229  		yaml: "blah:\n  ping: pong",
   230  		key:  "blah",
   231  		err:  `unknown option "ping"`,
   232  	}, {
   233  		info: "bad type for string",
   234  		yaml: "blah:\n  outlook: 123",
   235  		key:  "blah",
   236  		err:  `option "outlook" expected string, got 123`,
   237  	}, {
   238  		info: "bad type for int",
   239  		yaml: "blah:\n  skill-level: 12.345",
   240  		key:  "blah",
   241  		err:  `option "skill-level" expected int, got 12.345`,
   242  	}, {
   243  		info: "bad type for float",
   244  		yaml: "blah:\n  agility-ratio: blob",
   245  		key:  "blah",
   246  		err:  `option "agility-ratio" expected float, got "blob"`,
   247  	}, {
   248  		info: "bad type for boolean",
   249  		yaml: "blah:\n  reticulate-splines: 123",
   250  		key:  "blah",
   251  		err:  `option "reticulate-splines" expected boolean, got 123`,
   252  	}, {
   253  		info: "bad string for int",
   254  		yaml: "blah:\n  skill-level: cheese",
   255  		key:  "blah",
   256  		err:  `option "skill-level" expected int, got "cheese"`,
   257  	}, {
   258  		info: "bad string for float",
   259  		yaml: "blah:\n  agility-ratio: blob",
   260  		key:  "blah",
   261  		err:  `option "agility-ratio" expected float, got "blob"`,
   262  	}, {
   263  		info: "bad string for boolean",
   264  		yaml: "blah:\n  reticulate-splines: cannonball",
   265  		key:  "blah",
   266  		err:  `option "reticulate-splines" expected boolean, got "cannonball"`,
   267  	}, {
   268  		info:   "empty dict is valid",
   269  		yaml:   "blah: {}",
   270  		key:    "blah",
   271  		expect: charm.Settings{},
   272  	}, {
   273  		info: "nil values are valid",
   274  		yaml: `blah:
   275              outlook: null
   276              skill-level: null
   277              agility-ratio: null
   278              reticulate-splines: null`,
   279  		key:    "blah",
   280  		expect: settingsWithNils,
   281  	}, {
   282  		info: "empty strings for bool options are not accepted",
   283  		yaml: `blah:
   284              outlook: ""
   285              skill-level: 123
   286              agility-ratio: 12.0
   287              reticulate-splines: ""`,
   288  		key: "blah",
   289  		err: `option "reticulate-splines" expected boolean, got ""`,
   290  	}, {
   291  		info: "empty strings for int options are not accepted",
   292  		yaml: `blah:
   293              outlook: ""
   294              skill-level: ""
   295              agility-ratio: 12.0
   296              reticulate-splines: false`,
   297  		key: "blah",
   298  		err: `option "skill-level" expected int, got ""`,
   299  	}, {
   300  		info: "empty strings for float options are not accepted",
   301  		yaml: `blah:
   302              outlook: ""
   303              skill-level: 123
   304              agility-ratio: ""
   305              reticulate-splines: false`,
   306  		key: "blah",
   307  		err: `option "agility-ratio" expected float, got ""`,
   308  	}, {
   309  		info: "appropriate strings are valid",
   310  		yaml: `blah:
   311              outlook: whatever
   312              skill-level: "123"
   313              agility-ratio: "2.22"
   314              reticulate-splines: "true"`,
   315  		key:    "blah",
   316  		expect: settingsWithValues,
   317  	}, {
   318  		info: "appropriate types are valid",
   319  		yaml: `blah:
   320              outlook: whatever
   321              skill-level: 123
   322              agility-ratio: 2.22
   323              reticulate-splines: y`,
   324  		key:    "blah",
   325  		expect: settingsWithValues,
   326  	}} {
   327  		c.Logf("test %d: %s", i, test.info)
   328  		result, err := s.config.ParseSettingsYAML([]byte(test.yaml), test.key)
   329  		if test.err != "" {
   330  			c.Check(err, gc.ErrorMatches, test.err)
   331  		} else {
   332  			c.Check(err, gc.IsNil)
   333  			c.Check(result, gc.DeepEquals, test.expect)
   334  		}
   335  	}
   336  }
   337  
   338  func (s *ConfigSuite) TestParseSettingsStrings(c *gc.C) {
   339  	for i, test := range []struct {
   340  		info   string
   341  		input  map[string]string
   342  		expect charm.Settings
   343  		err    string
   344  	}{{
   345  		info:   "nil map is valid",
   346  		expect: charm.Settings{},
   347  	}, {
   348  		info:   "empty map is valid",
   349  		input:  map[string]string{},
   350  		expect: charm.Settings{},
   351  	}, {
   352  		info:   "empty strings for string options are valid",
   353  		input:  map[string]string{"outlook": ""},
   354  		expect: charm.Settings{"outlook": ""},
   355  	}, {
   356  		info:  "empty strings for non-string options are invalid",
   357  		input: map[string]string{"skill-level": ""},
   358  		err:   `option "skill-level" expected int, got ""`,
   359  	}, {
   360  		info: "strings are converted",
   361  		input: map[string]string{
   362  			"outlook":            "whatever",
   363  			"skill-level":        "123",
   364  			"agility-ratio":      "2.22",
   365  			"reticulate-splines": "true",
   366  		},
   367  		expect: settingsWithValues,
   368  	}, {
   369  		info:  "bad string for int",
   370  		input: map[string]string{"skill-level": "cheese"},
   371  		err:   `option "skill-level" expected int, got "cheese"`,
   372  	}, {
   373  		info:  "bad string for float",
   374  		input: map[string]string{"agility-ratio": "blob"},
   375  		err:   `option "agility-ratio" expected float, got "blob"`,
   376  	}, {
   377  		info:  "bad string for boolean",
   378  		input: map[string]string{"reticulate-splines": "cannonball"},
   379  		err:   `option "reticulate-splines" expected boolean, got "cannonball"`,
   380  	}} {
   381  		c.Logf("test %d: %s", i, test.info)
   382  		result, err := s.config.ParseSettingsStrings(test.input)
   383  		if test.err != "" {
   384  			c.Check(err, gc.ErrorMatches, test.err)
   385  		} else {
   386  			c.Check(err, gc.IsNil)
   387  			c.Check(result, gc.DeepEquals, test.expect)
   388  		}
   389  	}
   390  }
   391  
   392  func (s *ConfigSuite) TestConfigError(c *gc.C) {
   393  	_, err := charm.ReadConfig(bytes.NewBuffer([]byte(`options: {t: {type: foo}}`)))
   394  	c.Assert(err, gc.ErrorMatches, `invalid config: option "t" has unknown type "foo"`)
   395  }
   396  
   397  func (s *ConfigSuite) TestDefaultType(c *gc.C) {
   398  	assertDefault := func(type_ string, value string, expected interface{}) {
   399  		config := fmt.Sprintf(`options: {t: {type: %s, default: %s}}`, type_, value)
   400  		result, err := charm.ReadConfig(bytes.NewBuffer([]byte(config)))
   401  		c.Assert(err, gc.IsNil)
   402  		c.Assert(result.Options["t"].Default, gc.Equals, expected)
   403  	}
   404  
   405  	assertDefault("boolean", "true", true)
   406  	assertDefault("string", "golden grahams", "golden grahams")
   407  	assertDefault("string", `""`, "")
   408  	assertDefault("float", "2.2e11", 2.2e11)
   409  	assertDefault("int", "99", int64(99))
   410  
   411  	assertTypeError := func(type_, str, value string) {
   412  		config := fmt.Sprintf(`options: {t: {type: %s, default: %s}}`, type_, str)
   413  		_, err := charm.ReadConfig(bytes.NewBuffer([]byte(config)))
   414  		expected := fmt.Sprintf(`invalid config default: option "t" expected %s, got %s`, type_, value)
   415  		c.Assert(err, gc.ErrorMatches, expected)
   416  	}
   417  
   418  	assertTypeError("boolean", "henry", `"henry"`)
   419  	assertTypeError("string", "2.5", "2.5")
   420  	assertTypeError("float", "123", "123")
   421  	assertTypeError("int", "true", "true")
   422  }
   423  
   424  // When an empty config is supplied an error should be returned
   425  func (s *ConfigSuite) TestEmptyConfigReturnsError(c *gc.C) {
   426  	config := ""
   427  	result, err := charm.ReadConfig(bytes.NewBuffer([]byte(config)))
   428  	c.Assert(result, gc.IsNil)
   429  	c.Assert(err, gc.ErrorMatches, "invalid config: empty configuration")
   430  }