Updated README. Fixed FF whitespace escaping. Renamed Decode* to Load*.
diff --git a/README.md b/README.md index b551d65..c0015cf 100644 --- a/README.md +++ b/README.md
@@ -1,22 +1,20 @@ Overview ======== -goproperties is a Go library for parsing Java property files. +goproperties is a Go library for reading and writing Java property files. + +It supports reading properties from multiple files and Spring style property +expansion of expressions of '${key}' to their corresponding value. The current version supports reading both ISO-8859-1 and UTF-8 encoded data. -A future version will also support Spring Framework style property expansion like +Install +------- - key = value - key2 = ${key} - -History -======= - -v0.9 - Initial release + $ go get github.com/magiconair/goproperties Usage -===== +----- buf, err := ioutil.ReadFile(filename) if err != nil { @@ -29,10 +27,36 @@ } value, ok := p.Get("key") + if ok { + fmt.Println(value) + } -Import -====== +or - go get github.com/magiconair/goproperties + p, err := goproperties.DecodeString("key = value") + if err != nil { + // handle error + } + value, ok := p.Get("key") + if ok { + fmt.Println(value) + } +History +------- + +v0.9, 17 Dec 2013 - Initial release + +License +------- + +2 clause BSD license. See LICENSE file for details. + +Parts of the lexer are taken from the template/text/parser package +For these parts the following applies: + +Copyright 2011 The Go Authors. All rights reserved. +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file of the go 1.2 +distribution.
diff --git a/decoder.go b/decoder.go deleted file mode 100644 index 9a8b90d..0000000 --- a/decoder.go +++ /dev/null
@@ -1,50 +0,0 @@ -// Copyright 2013 Frank Schroeder. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package goproperties - -import ( - "fmt" -) - -type encoding uint - -const ( - enc_utf8 encoding = 1 << iota - enc_iso_8859_1 -) - -// Decodes an ISO-8859-1 encoded buffer into a Properties struct. -func Decode(buf []byte) (Properties, error) { - return decodeWithEncoding(buf, enc_iso_8859_1) -} - -// Decodes an UTF-8 string into a Properties struct. -func DecodeFromString(input string) (Properties, error) { - return decodeWithEncoding([]byte(input), enc_utf8) -} - -// Decodes either an ISO-8859-1 or an UTF-8 encoded string into a Properties struct. -func decodeWithEncoding(buf []byte, enc encoding) (Properties, error) { - return newParser().Parse(convert(buf, enc)) -} - -// The Java properties spec says that .properties files must be ISO-8859-1 -// encoded. Since the first 256 unicode code points cover ISO-8859-1 we -// can convert each byte straight into a rune and use the resulting string -// as UTF-8 input for the parser. -func convert(buf []byte, enc encoding) string { - switch enc { - case enc_utf8: - return string(buf) - case enc_iso_8859_1: - runes := make([]rune, len(buf)) - for i, b := range buf { - runes[i] = rune(b) - } - return string(runes) - default: - panic(fmt.Sprintf("unsupported encoding %v", enc)) - } -}
diff --git a/doc.go b/doc.go index 17992fd..f3ea1fe 100644 --- a/doc.go +++ b/doc.go
@@ -2,66 +2,19 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// Package goproperties reads Java properties files. +// goproperties provides functions for reading and writing +// Java properties files and has support for Spring like +// property expansion. // -// Java properties files contain key/value pairs in one of the following form: +// By default, if a value contains a reference '${key}' then +// getting the value will recursively expand the key to its value. +// The format is configurable and circular references are not allowed. // -// key value -// key = value -// key : value +// See one of the following links for a description of the properties +// file format. // -// The value is optional and ends with EOF or a new line which can either be '\n', '\r' or "\r\n". -// Therefore, the following expression is legal and results in a key with an empty value: +// http://en.wikipedia.org/wiki/.properties // -// key -// -// Whitespace before the key and around the delimiter is ignored. Whitespace at the end of the value is part of the value. -// Besides the space ' ' (U+0020) character the TAB (U+0009) and FF (U+000C) characters are also treated as whitespace. -// Therefore, the following expressions are equal: -// -// key=value -// key=value -// key= value -// key =value -// key = value -// key = value -// key\f=\fvalue -// key\t=\tvalue -// -// Blank lines and comment lines starting with '#' or '!' and are ignored until the end of the line. -// -// # the next line is empty and will be ignored -// -// ! this is a comment -// key = value -// -// If the delimiter characters '=' and ':' appear in either key or value then -// they must be escaped with a backslash. Because of this the backslash must -// also be escaped. The characters '\n', '\r' or '\t' can be part of both key -// or value and must be escaped. For all other characters the backslash is -// silently dropped. -// -// # key:1 = value=2 -// key\:1 = value\=2 -// -// # key = value with tabs -// key = value\twith\ttabs -// -// # key = value with silently dropped backslash -// key = v\alu\e with silently dropped backslash -// -// Values can span multiple lines by using a backslash before the newline character. -// All subsequent whitespace on the following line is ignored. Comment lines cannot be -// extended like this. -// -// # key = value continued -// key = value \ -// continued -// -// Java properties files are ISO-8559-1 encoded and can have Unicode literals for -// characters outside the character set. Unicode literals are specified as \uXXXX. -// -// # key = value with € -// key = value with \u20AC +// http://docs.oracle.com/javase/7/docs/api/java/util/Properties.html#load%28java.io.Reader%29 // package goproperties
diff --git a/example_test.go b/example_test.go index 1c128cc..d8ee275 100644 --- a/example_test.go +++ b/example_test.go
@@ -6,13 +6,14 @@ import ( "fmt" + "log" ) -func ExampleDecode() { +func ExampleLoad() { buf := []byte("key = ISO-8859-1 value with unicode literal \\u2318 and umlaut ") buf = append(buf, 0xE4) // 0xE4 == ä - p, _ := Decode(buf) - v, ok := p["key"] + p, _ := Load(buf) + v, ok := p.Get("key") fmt.Println(ok) fmt.Println(v) // Output: @@ -20,12 +21,61 @@ // ISO-8859-1 value with unicode literal ⌘ and umlaut ä } -func ExampleDecodeFromString() { - p, _ := DecodeFromString("key = UTF-8 value with unicode character ⌘ and umlaut ä") - v, ok := p["key"] +func ExampleLoadString() { + p, _ := LoadString("key = UTF-8 value with unicode character ⌘ and umlaut ä") + v, ok := p.Get("key") fmt.Println(ok) fmt.Println(v) // Output: // true // UTF-8 value with unicode character ⌘ and umlaut ä } + +func Example_Properties_GetDefault() { + p, _ := LoadString("key=value") + v := p.GetDefault("another key", "default value") + fmt.Println(v) + // Output: + // default value +} + +func Example() { + // Decode some key/value pairs with expressions + p, err := LoadString("key=value\nkey2=${key}") + if err != nil { + log.Fatal(err) + } + + // Get a valid key + if v, ok := p.Get("key"); ok { + fmt.Println(v) + } + + // Get an invalid key + if _, ok := p.Get("does not exist"); !ok { + fmt.Println("invalid key") + } + + // Get a key with a default value + v := p.GetDefault("does not exist", "some value") + fmt.Println(v) + + // Dump the expanded key/value pairs of the Properties + fmt.Println("Expanded key/value pairs") + fmt.Println(p) + + // Dump the raw key/value pairs. + fmt.Println("Raw key/value pairs") + fmt.Println(p.Dump()) + // Output: + // value + // invalid key + // some value + // Expanded key/value pairs + // key = value + // key2 = value + // + // Raw key/value pairs + // key = value + // key2 = ${key} +}
diff --git a/lex.go b/lex.go index 80506bb..562b85b 100644 --- a/lex.go +++ b/lex.go
@@ -48,8 +48,10 @@ itemValue // a value ) +// defines a constant for EOF const eof = -1 +// permitted whitespace characters space, FF and TAB const whitespace = " \f\t" // stateFn represents the state of the scanner as a function that returns the next state. @@ -322,8 +324,11 @@ case isEOF(r): return fmt.Errorf("premature EOF") + // silently drop the escape character and append the rune as is default: - return fmt.Errorf("invalid escape sequence %s", string(r)) + l.appendRune(r) + return nil + // return fmt.Errorf("invalid escape sequence %s", string(r)) } } @@ -351,6 +356,8 @@ // decodeEscapedCharacter returns the unescaped rune. We expect to be after the escape character. func decodeEscapedCharacter(r rune) rune { switch r { + case 'f': + return '\f' case 'n': return '\n' case 'r': @@ -397,7 +404,7 @@ // isEscapedCharacter reports whether we are at one of the characters that need escaping. // The escape character has already been consumed. func isEscapedCharacter(r rune) bool { - return strings.ContainsRune(" :=nrt", r) + return strings.ContainsRune(" :=fnrt", r) } // isWhitespace reports whether the rune is a whitespace character.
diff --git a/load.go b/load.go new file mode 100644 index 0000000..ecb9439 --- /dev/null +++ b/load.go
@@ -0,0 +1,100 @@ +// Copyright 2013 Frank Schroeder. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package goproperties + +import ( + "fmt" + "io/ioutil" + "os" +) + +// Load reads an ISO-8859-1 encoded buffer into a Properties struct. +func Load(buf []byte) (*Properties, error) { + return loadBuf(buf, enc_iso_8859_1) +} + +// LoadString reads an UTF-8 string into a Properties struct. +func LoadString(input string) (*Properties, error) { + return loadBuf([]byte(input), enc_utf8) +} + +// LoadFile reads a file into a Properties struct. +func LoadFile(filename string) (*Properties, error) { + return loadFiles([]string{filename}, false) +} + +// LoadFiles reads multiple file in the given order into +// a Properties struct. If 'ignoreMissing' is 'true' then +// non-existent files will not be reported as error. +func LoadFiles(filenames []string, ignoreMissing bool) (*Properties, error) { + return loadFiles(filenames, ignoreMissing) +} + +// MustLoadFile reads a file into a Properties struct and panics on error. +func MustLoadFile(filename string) *Properties { + return MustLoadFiles([]string{filename}, false) +} + +// MustLoadFiles reads multiple file in the given order into +// a Properties struct and panics on error. +// If 'ignoreMissing' is 'true' then non-existent files will not be reported as error. +func MustLoadFiles(filenames []string, ignoreMissing bool) *Properties { + p, err := loadFiles(filenames, ignoreMissing) + if err != nil { + panic(err) + } + return p +} + +type encoding uint + +const ( + enc_utf8 encoding = 1 << iota + enc_iso_8859_1 +) + +// Loads either an ISO-8859-1 or an UTF-8 encoded string into a Properties struct. +func loadBuf(buf []byte, enc encoding) (*Properties, error) { + return parse(convert(buf, enc)) +} + +func loadFiles(filenames []string, ignoreMissing bool) (*Properties, error) { + buff := make([]byte, 0, 4096) + + for _, filename := range filenames { + buf, err := ioutil.ReadFile(filename) + if err != nil { + if ignoreMissing && os.IsNotExist(err) { + // TODO(frank): should we log that we are skipping the file? + continue + } + return nil, err + } + + // concatenate the buffers and add a new line in case + // the previous file didn't end with a new line + buff = append(append(buff, buf...), '\n') + } + + return loadBuf(buff, enc_iso_8859_1) +} + +// Interprets a byte buffer either as ISO-8859-1 or UTF-8 encoded string. +// For ISO-8859-1 we can convert each byte straight into a rune since the +// first 256 unicode code points cover ISO-8859-1. +func convert(buf []byte, enc encoding) string { + switch enc { + case enc_utf8: + return string(buf) + case enc_iso_8859_1: + runes := make([]rune, len(buf)) + for i, b := range buf { + runes[i] = rune(b) + } + return string(runes) + default: + panic(fmt.Sprintf("unsupported encoding %v", enc)) + } +}
diff --git a/load_test.go b/load_test.go new file mode 100644 index 0000000..42fe1d7 --- /dev/null +++ b/load_test.go
@@ -0,0 +1,121 @@ +// Copyright 2013 Frank Schroeder. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package goproperties + +import ( + "fmt" + "io/ioutil" + "os" + + . "launchpad.net/gocheck" +) + +type LoadSuite struct { + tempFiles []string +} + +var ( + _ = Suite(&LoadSuite{}) +) + +// ---------------------------------------------------------------------------- + +func (s *LoadSuite) TestLoadFailsWithNotExistingFile(c *C) { + _, err := LoadFile("doesnotexist.properties") + c.Assert(err, NotNil) + c.Assert(err, ErrorMatches, "open.*no such file or directory") +} + +// ---------------------------------------------------------------------------- + +func (s *LoadSuite) TestLoadFilesFailsOnNotExistingFile(c *C) { + _, err := LoadFiles([]string{"doesnotexist.properties"}, false) + c.Assert(err, NotNil) + c.Assert(err, ErrorMatches, "open.*no such file or directory") +} + +// ---------------------------------------------------------------------------- + +func (s *LoadSuite) TestLoadFilesDoesNotFailOnNotExistingFileAndIgnoreMissing(c *C) { + p, err := LoadFiles([]string{"doesnotexist.properties"}, true) + c.Assert(err, IsNil) + c.Assert(p.Len(), Equals, 0) +} + +// ---------------------------------------------------------------------------- + +func (s *LoadSuite) TestLoad(c *C) { + filename := s.makeFile(c, "key=value") + p := MustLoadFile(filename) + + c.Assert(p.Len(), Equals, 1) + assertKeyValues(c, "", p, "key", "value") +} + +// ---------------------------------------------------------------------------- + +func (s *LoadSuite) TestLoadFiles(c *C) { + filename := s.makeFile(c, "key=value") + filename2 := s.makeFile(c, "key2=value2") + p := MustLoadFiles([]string{filename, filename2}, false) + assertKeyValues(c, "", p, "key", "value", "key2", "value2") +} + +// ---------------------------------------------------------------------------- + +func (s *LoadSuite) TestLoadFilesAndIgnoreMissing(c *C) { + filename := s.makeFile(c, "key=value") + filename2 := s.makeFile(c, "key2=value2") + p := MustLoadFiles([]string{filename, filename + "foo", filename2, filename2 + "foo"}, true) + assertKeyValues(c, "", p, "key", "value", "key2", "value2") +} + +// ---------------------------------------------------------------------------- + +func (s *LoadSuite) SetUpSuite(c *C) { + s.tempFiles = make([]string, 0) +} + +// ---------------------------------------------------------------------------- + +func (s *LoadSuite) TearDownSuite(c *C) { + for _, path := range s.tempFiles { + err := os.Remove(path) + if err != nil { + fmt.Printf("os.Remove: %v", err) + } + } +} + +// ---------------------------------------------------------------------------- + +func (s *LoadSuite) makeFile(c *C, data string) string { + f, err := ioutil.TempFile("", "properties") + if err != nil { + fmt.Printf("ioutil.TempFile: %v", err) + c.FailNow() + } + + // remember the temp file so that we can remove it later + s.tempFiles = append(s.tempFiles, f.Name()) + + n, err := fmt.Fprint(f, data) + if err != nil { + fmt.Printf("fmt.Fprintln: %v", err) + c.FailNow() + } + if n != len(data) { + fmt.Printf("Data size mismatch. expected=%d wrote=%d\n", len(data), n) + c.FailNow() + } + + err = f.Close() + if err != nil { + fmt.Printf("f.Close: %v", err) + c.FailNow() + } + + return f.Name() +}
diff --git a/parser.go b/parser.go index 849449b..8cb73d8 100644 --- a/parser.go +++ b/parser.go
@@ -13,14 +13,12 @@ lex *lexer } -func newParser() *parser { - return &parser{} -} - -func (p *parser) Parse(input string) (props Properties, err error) { +func parse(input string) (properties *Properties, err error) { + p := &parser{lex: lex(input)} defer p.recover(&err) + p.lex = lex(input) - props = make(map[string]string) + properties = NewProperties() for { token := p.expectOneOf(itemKey, itemEOF) @@ -31,13 +29,13 @@ token = p.expectOneOf(itemValue, itemEOF) if token.typ == itemEOF { - props[key] = "" + properties.m[key] = "" break } - props[key] = token.val + properties.m[key] = token.val } - return props, nil + return properties, nil } func (p *parser) errorf(format string, args ...interface{}) {
diff --git a/properties.go b/properties.go index 0f965a9..87bf5cf 100644 --- a/properties.go +++ b/properties.go
@@ -4,4 +4,166 @@ package goproperties -type Properties map[string]string +import ( + "fmt" + "io" + "log" + "strings" + "unicode/utf8" +) + +type Properties struct { + // Pre-/Postfix for property expansion. + Prefix string + Postfix string + + m map[string]string +} + +// NewProperties creates a new Properties struct with the default +// configuration for "${key}" expressions. +func NewProperties() *Properties { + return &Properties{ + Prefix: "${", + Postfix: "}", + m: make(map[string]string), + } +} + +// Get returns the expanded value for the given key if exists. Otherwise, ok is false. +func (p *Properties) Get(key string) (value string, ok bool) { + v, ok := p.m[key] + if !ok { + return "", false + } + + expanded, err := p.expand(v) + + // if there is an error then this is a format exception which we just log + // and return the input unchanged. + if err != nil { + log.Printf("%s in %q", err, key+" = "+v) + return v, true + } + + return expanded, true +} + +// GetDefault returns the expanded value for the given key if exists or the default value otherwise. +func (p *Properties) GetDefault(key, defaultValue string) (value string) { + if v, ok := p.Get(key); ok { + return v + } + return defaultValue +} + +// Len returns the number of keys. +func (p *Properties) Len() int { + return len(p.m) +} + +// Dump returns a string of all unexpanded 'key = value' pairs. +func (p *Properties) Dump() string { + var s string + for key, value := range p.m { + s = fmt.Sprintf("%s%s = %s\n", s, key, value) + } + return s +} + +// String returns a string of all expanded 'key = value' pairs. +func (p *Properties) String() string { + var s string + for key, _ := range p.m { + value, _ := p.Get(key) + s = fmt.Sprintf("%s%s = %s\n", s, key, value) + } + return s +} + +// Write writes all unexpanded 'key = value' pairs as ISO-8859-1 to the given writer. +func (p *Properties) Write(w io.Writer) (int, error) { + total := 0 + for key, value := range p.m { + s := fmt.Sprintf("%s = %s\n", encode(key, " :"), encode(value, "")) + n, err := w.Write([]byte(s)) + if err != nil { + return total, err + } + total += n + } + return total, nil +} + +// expand recursively expands expressions of '(prefix)key(postfix)' to their corresponding values. +// The function keeps track of the keys that were already expanded and stops if it +// detects a circular reference. +func (p *Properties) expand(input string) (string, error) { + // no pre/postfix -> nothing to expand + if p.Prefix == "" && p.Postfix == "" { + return input, nil + } + + return p.doExpand(input, make(map[string]bool)) +} + +func (p *Properties) doExpand(s string, keys map[string]bool) (string, error) { + a := strings.Index(s, p.Prefix) + if a == -1 { + return s, nil + } + + b := strings.Index(s[a:], p.Postfix) + if b == -1 { + return "", fmt.Errorf("Malformed expression") + } + + key := s[a+len(p.Prefix) : b-len(p.Postfix)+1] + + if _, ok := keys[key]; ok { + return "", fmt.Errorf("Circular reference") + } + + val, ok := p.m[key] + if !ok { + val = "" + } + + // remember that we've seen the key + keys[key] = true + + return p.doExpand(s[:a]+val+s[b+1:], keys) +} + +// encode encodes a UTF-8 string to ISO-8859-1 and escapes some characters. +func encode(s string, escape string) string { + var r rune + var w int + var v string + for pos := 0; pos < len(s); { + switch r, w = utf8.DecodeRuneInString(s[pos:]); { + case r < 1<<8: // single byte rune -> encode special chars only + switch r { + case '\f': + v += "\\f" + case '\n': + v += "\\n" + case '\r': + v += "\\r" + case '\t': + v += "\\t" + default: + if strings.ContainsRune(escape, r) { + v += "\\" + } + v += string(r) + } + case r < 1<<16: // two byte rune -> unicode literal + v += fmt.Sprintf("\\u%04x", r) + default: // more than two bytes per rune -> can't encode + v += "?" + } + pos += w + } + return v +}
diff --git a/decoder_test.go b/properties_test.go similarity index 63% rename from decoder_test.go rename to properties_test.go index 701ef2e..91b1a7e 100644 --- a/decoder_test.go +++ b/properties_test.go
@@ -5,13 +5,14 @@ package goproperties import ( + "bytes" "flag" "fmt" "os" "strings" "testing" - . "github.scm.corp.ebay.com/ecg-marktplaats/cas-go/third_party/launchpad.net/gocheck" + . "launchpad.net/gocheck" ) func Test(t *testing.T) { TestingT(t) } @@ -42,9 +43,27 @@ {"\rkey=value\r", "key", "value"}, {"\r\nkey=value\r\n", "key", "value"}, - // escaped chars - {"k\\ e\\:y\\= = value", "k e:y=", "value"}, // escaped chars in key - {"key = v\\ a\\:lu\\=e\\n\\r\\t", "key", "v a:lu=e\n\r\t"}, // escaped chars in value + // escaped chars in key + {"k\\ ey = value", "k ey", "value"}, + {"k\\:ey = value", "k:ey", "value"}, + {"k\\=ey = value", "k=ey", "value"}, + {"k\\fey = value", "k\fey", "value"}, + {"k\\ney = value", "k\ney", "value"}, + {"k\\rey = value", "k\rey", "value"}, + {"k\\tey = value", "k\tey", "value"}, + + // escaped chars in value + {"key = v\\ alue", "key", "v alue"}, + {"key = v\\:alue", "key", "v:alue"}, + {"key = v\\=alue", "key", "v=alue"}, + {"key = v\\falue", "key", "v\falue"}, + {"key = v\\nalue", "key", "v\nalue"}, + {"key = v\\ralue", "key", "v\ralue"}, + {"key = v\\talue", "key", "v\talue"}, + + // silently dropped escape character + {"k\\zey = value", "kzey", "value"}, + {"key = v\\zalue", "key", "vzalue"}, // unicode literals {"key\\u2318 = value", "key⌘", "value"}, @@ -60,6 +79,18 @@ // comments {"# this is a comment\n! and so is this\nkey1=value1\nkey#2=value#2\n\nkey!3=value!3\n# and another one\n! and the final one", "key1", "value1", "key#2", "value#2", "key!3", "value!3"}, + + // expansion tests + {"key=value\nkey2=${key}", "key", "value", "key2", "value"}, + {"key=value\nkey2=${key}\nkey3=${key2}", "key", "value", "key2", "value", "key3", "value"}, + + // circular references + {"key=${key}", "key", "${key}"}, + {"key1=${key2}\nkey2=${key1}", "key1", "${key2}", "key2", "${key1}"}, + + // malformed expressions + {"key=${ke", "key", "${ke"}, + {"key=valu${ke", "key", "valu${ke"}, } // define error test cases in the form of @@ -72,6 +103,15 @@ {"key\\u123", "invalid unicode literal"}, } +// define write encoding test cases in the form of +// {"input", "expected output after write"} +var writeTests = [][]string{ + {"key = value", "key = value\n"}, + {"key = value \\\n continued", "key = value continued\n"}, + {"key⌘ = value", "key\\u2318 = value\n"}, + {"ke\\ \\:y = value", "ke\\ \\:y = value\n"}, +} + // Benchmarks the decoder by creating a property file with 1000 key/value pairs. func BenchmarkDecoder(b *testing.B) { input := "" @@ -80,7 +120,7 @@ } b.ResetTimer() for i := 0; i < b.N; i++ { - Decode([]byte(input)) + Load([]byte(input)) } } @@ -106,6 +146,21 @@ } } +// Test write encoding. +func (l *TestSuite) TestWrite(c *C) { + for _, test := range writeTests { + input, output := test[0], test[1] + p, err := parse(input) + + buf := new(bytes.Buffer) + n, err := p.Write(buf) + c.Assert(err, IsNil) + s := string(buf.Bytes()) + c.Assert(n, Equals, len(output), Commentf("input=%q expected=%q obtained=%q", input, output, s)) + c.Assert(s, Equals, output, Commentf("input=%q expected=%q obtained=%q", input, output, s)) + } +} + // tests all combinations of delimiters, leading and/or trailing whitespace and newlines. func testAllCombinations(c *C, key, value string) { whitespace := []string{"", " ", "\f", "\t"} @@ -133,28 +188,34 @@ func testKeyValue(c *C, input string, keyvalues ...string) { printf("%q\n", input) - p, err := Decode([]byte(input)) + p, err := Load([]byte(input)) c.Assert(err, IsNil) - c.Assert(p, NotNil) - c.Assert(len(p), Equals, len(keyvalues)/2, Commentf("Odd number of key/value pairs.")) - - for i := 0; i < len(keyvalues)/2; i += 2 { - key, value := keyvalues[i], keyvalues[i+1] - v, ok := p[key] - c.Assert(ok, Equals, true, Commentf("No key %q for input %q", key, input)) - c.Assert(v, Equals, value, Commentf("Value %q does not match input %q", value, input)) - } + assertKeyValues(c, input, p, keyvalues...) } // tests whether some input produces a given error message. func testError(c *C, input, msg string) { printf("%q\n", input) - _, err := Decode([]byte(input)) + _, err := Load([]byte(input)) c.Assert(err, NotNil) c.Assert(strings.Contains(err.Error(), msg), Equals, true, Commentf("Expected %q got %q", msg, err.Error())) } +// tests whether key/value pairs exist for a given input. +// keyvalues is expected to be an even number of strings of "key", "value", ... +func assertKeyValues(c *C, input string, p *Properties, keyvalues ...string) { + c.Assert(p, NotNil) + c.Assert(2*p.Len(), Equals, len(keyvalues), Commentf("Odd number of key/value pairs.")) + + for i := 0; i < len(keyvalues); i += 2 { + key, value := keyvalues[i], keyvalues[i+1] + v, ok := p.Get(key) + c.Assert(ok, Equals, true, Commentf("No key %q found (input=%q)", key, input)) + c.Assert(v, Equals, value, Commentf("Value %q does not match %q (input=%q)", v, value, input)) + } +} + // prints to stderr if the -verbose flag was given. func printf(format string, args ...interface{}) { if *verbose {