1// Copyright 2014 Unknwon 2// 3// Licensed under the Apache License, Version 2.0 (the "License"): you may 4// not use this file except in compliance with the License. You may obtain 5// a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12// License for the specific language governing permissions and limitations 13// under the License. 14 15package ini_test 16 17import ( 18 "bytes" 19 "fmt" 20 "runtime" 21 "strings" 22 "testing" 23 "time" 24 25 . "github.com/smartystreets/goconvey/convey" 26 "gopkg.in/ini.v1" 27) 28 29func TestKey_AddShadow(t *testing.T) { 30 Convey("Add shadow to a key", t, func() { 31 f, err := ini.ShadowLoad([]byte(` 32[notes] 33-: note1`)) 34 So(err, ShouldBeNil) 35 So(f, ShouldNotBeNil) 36 37 k, err := f.Section("").NewKey("NAME", "ini") 38 So(err, ShouldBeNil) 39 So(k, ShouldNotBeNil) 40 41 So(k.AddShadow("ini.v1"), ShouldBeNil) 42 So(k.ValueWithShadows(), ShouldResemble, []string{"ini", "ini.v1"}) 43 44 Convey("Add shadow to boolean key", func() { 45 k, err := f.Section("").NewBooleanKey("published") 46 So(err, ShouldBeNil) 47 So(k, ShouldNotBeNil) 48 So(k.AddShadow("beta"), ShouldNotBeNil) 49 }) 50 51 Convey("Add shadow to auto-increment key", func() { 52 So(f.Section("notes").Key("#1").AddShadow("beta"), ShouldNotBeNil) 53 }) 54 }) 55 56 Convey("Shadow is not allowed", t, func() { 57 f := ini.Empty() 58 So(f, ShouldNotBeNil) 59 60 k, err := f.Section("").NewKey("NAME", "ini") 61 So(err, ShouldBeNil) 62 So(k, ShouldNotBeNil) 63 64 So(k.AddShadow("ini.v1"), ShouldNotBeNil) 65 }) 66} 67 68// Helpers for slice tests. 69func float64sEqual(values []float64, expected ...float64) { 70 So(values, ShouldHaveLength, len(expected)) 71 for i, v := range expected { 72 So(values[i], ShouldEqual, v) 73 } 74} 75 76func intsEqual(values []int, expected ...int) { 77 So(values, ShouldHaveLength, len(expected)) 78 for i, v := range expected { 79 So(values[i], ShouldEqual, v) 80 } 81} 82 83func int64sEqual(values []int64, expected ...int64) { 84 So(values, ShouldHaveLength, len(expected)) 85 for i, v := range expected { 86 So(values[i], ShouldEqual, v) 87 } 88} 89 90func uintsEqual(values []uint, expected ...uint) { 91 So(values, ShouldHaveLength, len(expected)) 92 for i, v := range expected { 93 So(values[i], ShouldEqual, v) 94 } 95} 96 97func uint64sEqual(values []uint64, expected ...uint64) { 98 So(values, ShouldHaveLength, len(expected)) 99 for i, v := range expected { 100 So(values[i], ShouldEqual, v) 101 } 102} 103 104func boolsEqual(values []bool, expected ...bool) { 105 So(values, ShouldHaveLength, len(expected)) 106 for i, v := range expected { 107 So(values[i], ShouldEqual, v) 108 } 109} 110 111func timesEqual(values []time.Time, expected ...time.Time) { 112 So(values, ShouldHaveLength, len(expected)) 113 for i, v := range expected { 114 So(values[i].String(), ShouldEqual, v.String()) 115 } 116} 117 118func TestKey_Helpers(t *testing.T) { 119 Convey("Getting and setting values", t, func() { 120 f, err := ini.Load(fullConf) 121 So(err, ShouldBeNil) 122 So(f, ShouldNotBeNil) 123 124 Convey("Get string representation", func() { 125 sec := f.Section("") 126 So(sec, ShouldNotBeNil) 127 So(sec.Key("NAME").Value(), ShouldEqual, "ini") 128 So(sec.Key("NAME").String(), ShouldEqual, "ini") 129 So(sec.Key("NAME").Validate(func(in string) string { 130 return in 131 }), ShouldEqual, "ini") 132 So(sec.Key("NAME").Comment, ShouldEqual, "; Package name") 133 So(sec.Key("IMPORT_PATH").String(), ShouldEqual, "gopkg.in/ini.v1") 134 135 Convey("With ValueMapper", func() { 136 f.ValueMapper = func(in string) string { 137 if in == "gopkg.in/%(NAME)s.%(VERSION)s" { 138 return "github.com/go-ini/ini" 139 } 140 return in 141 } 142 So(sec.Key("IMPORT_PATH").String(), ShouldEqual, "github.com/go-ini/ini") 143 }) 144 }) 145 146 Convey("Get values in non-default section", func() { 147 sec := f.Section("author") 148 So(sec, ShouldNotBeNil) 149 So(sec.Key("NAME").String(), ShouldEqual, "Unknwon") 150 So(sec.Key("GITHUB").String(), ShouldEqual, "https://github.com/Unknwon") 151 152 sec = f.Section("package") 153 So(sec, ShouldNotBeNil) 154 So(sec.Key("CLONE_URL").String(), ShouldEqual, "https://gopkg.in/ini.v1") 155 }) 156 157 Convey("Get auto-increment key names", func() { 158 keys := f.Section("features").Keys() 159 for i, k := range keys { 160 So(k.Name(), ShouldEqual, fmt.Sprintf("#%d", i+1)) 161 } 162 }) 163 164 Convey("Get parent-keys that are available to the child section", func() { 165 parentKeys := f.Section("package.sub").ParentKeys() 166 for _, k := range parentKeys { 167 So(k.Name(), ShouldEqual, "CLONE_URL") 168 } 169 }) 170 171 Convey("Get overwrite value", func() { 172 So(f.Section("author").Key("E-MAIL").String(), ShouldEqual, "u@gogs.io") 173 }) 174 175 Convey("Get sections", func() { 176 sections := f.Sections() 177 for i, name := range []string{ini.DefaultSection, "author", "package", "package.sub", "features", "types", "array", "note", "comments", "string escapes", "advance"} { 178 So(sections[i].Name(), ShouldEqual, name) 179 } 180 }) 181 182 Convey("Get parent section value", func() { 183 So(f.Section("package.sub").Key("CLONE_URL").String(), ShouldEqual, "https://gopkg.in/ini.v1") 184 So(f.Section("package.fake.sub").Key("CLONE_URL").String(), ShouldEqual, "https://gopkg.in/ini.v1") 185 }) 186 187 Convey("Get multiple line value", func() { 188 if runtime.GOOS == "windows" { 189 t.Skip("Skipping testing on Windows") 190 } 191 192 So(f.Section("author").Key("BIO").String(), ShouldEqual, "Gopher.\nCoding addict.\nGood man.\n") 193 }) 194 195 Convey("Get values with type", func() { 196 sec := f.Section("types") 197 v1, err := sec.Key("BOOL").Bool() 198 So(err, ShouldBeNil) 199 So(v1, ShouldBeTrue) 200 201 v1, err = sec.Key("BOOL_FALSE").Bool() 202 So(err, ShouldBeNil) 203 So(v1, ShouldBeFalse) 204 205 v2, err := sec.Key("FLOAT64").Float64() 206 So(err, ShouldBeNil) 207 So(v2, ShouldEqual, 1.25) 208 209 v3, err := sec.Key("INT").Int() 210 So(err, ShouldBeNil) 211 So(v3, ShouldEqual, 10) 212 213 v4, err := sec.Key("INT").Int64() 214 So(err, ShouldBeNil) 215 So(v4, ShouldEqual, 10) 216 217 v5, err := sec.Key("UINT").Uint() 218 So(err, ShouldBeNil) 219 So(v5, ShouldEqual, 3) 220 221 v6, err := sec.Key("UINT").Uint64() 222 So(err, ShouldBeNil) 223 So(v6, ShouldEqual, 3) 224 225 t, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z") 226 So(err, ShouldBeNil) 227 v7, err := sec.Key("TIME").Time() 228 So(err, ShouldBeNil) 229 So(v7.String(), ShouldEqual, t.String()) 230 231 v8, err := sec.Key("HEX_NUMBER").Int() 232 So(err, ShouldBeNil) 233 So(v8, ShouldEqual, 0x3000) 234 235 Convey("Must get values with type", func() { 236 So(sec.Key("STRING").MustString("404"), ShouldEqual, "str") 237 So(sec.Key("BOOL").MustBool(), ShouldBeTrue) 238 So(sec.Key("FLOAT64").MustFloat64(), ShouldEqual, 1.25) 239 So(sec.Key("INT").MustInt(), ShouldEqual, 10) 240 So(sec.Key("INT").MustInt64(), ShouldEqual, 10) 241 So(sec.Key("UINT").MustUint(), ShouldEqual, 3) 242 So(sec.Key("UINT").MustUint64(), ShouldEqual, 3) 243 So(sec.Key("TIME").MustTime().String(), ShouldEqual, t.String()) 244 So(sec.Key("HEX_NUMBER").MustInt(), ShouldEqual, 0x3000) 245 246 dur, err := time.ParseDuration("2h45m") 247 So(err, ShouldBeNil) 248 So(sec.Key("DURATION").MustDuration().Seconds(), ShouldEqual, dur.Seconds()) 249 250 Convey("Must get values with default value", func() { 251 So(sec.Key("STRING_404").MustString("404"), ShouldEqual, "404") 252 So(sec.Key("BOOL_404").MustBool(true), ShouldBeTrue) 253 So(sec.Key("FLOAT64_404").MustFloat64(2.5), ShouldEqual, 2.5) 254 So(sec.Key("INT_404").MustInt(15), ShouldEqual, 15) 255 So(sec.Key("INT64_404").MustInt64(15), ShouldEqual, 15) 256 So(sec.Key("UINT_404").MustUint(6), ShouldEqual, 6) 257 So(sec.Key("UINT64_404").MustUint64(6), ShouldEqual, 6) 258 So(sec.Key("HEX_NUMBER_404").MustInt(0x3001), ShouldEqual, 0x3001) 259 260 t, err := time.Parse(time.RFC3339, "2014-01-01T20:17:05Z") 261 So(err, ShouldBeNil) 262 So(sec.Key("TIME_404").MustTime(t).String(), ShouldEqual, t.String()) 263 264 So(sec.Key("DURATION_404").MustDuration(dur).Seconds(), ShouldEqual, dur.Seconds()) 265 266 Convey("Must should set default as key value", func() { 267 So(sec.Key("STRING_404").String(), ShouldEqual, "404") 268 So(sec.Key("BOOL_404").String(), ShouldEqual, "true") 269 So(sec.Key("FLOAT64_404").String(), ShouldEqual, "2.5") 270 So(sec.Key("INT_404").String(), ShouldEqual, "15") 271 So(sec.Key("INT64_404").String(), ShouldEqual, "15") 272 So(sec.Key("UINT_404").String(), ShouldEqual, "6") 273 So(sec.Key("UINT64_404").String(), ShouldEqual, "6") 274 So(sec.Key("TIME_404").String(), ShouldEqual, "2014-01-01T20:17:05Z") 275 So(sec.Key("DURATION_404").String(), ShouldEqual, "2h45m0s") 276 So(sec.Key("HEX_NUMBER_404").String(), ShouldEqual, "12289") 277 }) 278 }) 279 }) 280 }) 281 282 Convey("Get value with candidates", func() { 283 sec := f.Section("types") 284 So(sec.Key("STRING").In("", []string{"str", "arr", "types"}), ShouldEqual, "str") 285 So(sec.Key("FLOAT64").InFloat64(0, []float64{1.25, 2.5, 3.75}), ShouldEqual, 1.25) 286 So(sec.Key("INT").InInt(0, []int{10, 20, 30}), ShouldEqual, 10) 287 So(sec.Key("INT").InInt64(0, []int64{10, 20, 30}), ShouldEqual, 10) 288 So(sec.Key("UINT").InUint(0, []uint{3, 6, 9}), ShouldEqual, 3) 289 So(sec.Key("UINT").InUint64(0, []uint64{3, 6, 9}), ShouldEqual, 3) 290 291 zt, err := time.Parse(time.RFC3339, "0001-01-01T01:00:00Z") 292 So(err, ShouldBeNil) 293 t, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z") 294 So(err, ShouldBeNil) 295 So(sec.Key("TIME").InTime(zt, []time.Time{t, time.Now(), time.Now().Add(1 * time.Second)}).String(), ShouldEqual, t.String()) 296 297 Convey("Get value with candidates and default value", func() { 298 So(sec.Key("STRING_404").In("str", []string{"str", "arr", "types"}), ShouldEqual, "str") 299 So(sec.Key("FLOAT64_404").InFloat64(1.25, []float64{1.25, 2.5, 3.75}), ShouldEqual, 1.25) 300 So(sec.Key("INT_404").InInt(10, []int{10, 20, 30}), ShouldEqual, 10) 301 So(sec.Key("INT64_404").InInt64(10, []int64{10, 20, 30}), ShouldEqual, 10) 302 So(sec.Key("UINT_404").InUint(3, []uint{3, 6, 9}), ShouldEqual, 3) 303 So(sec.Key("UINT_404").InUint64(3, []uint64{3, 6, 9}), ShouldEqual, 3) 304 So(sec.Key("TIME_404").InTime(t, []time.Time{time.Now(), time.Now(), time.Now().Add(1 * time.Second)}).String(), ShouldEqual, t.String()) 305 }) 306 }) 307 308 Convey("Get values in range", func() { 309 sec := f.Section("types") 310 So(sec.Key("FLOAT64").RangeFloat64(0, 1, 2), ShouldEqual, 1.25) 311 So(sec.Key("INT").RangeInt(0, 10, 20), ShouldEqual, 10) 312 So(sec.Key("INT").RangeInt64(0, 10, 20), ShouldEqual, 10) 313 314 minT, err := time.Parse(time.RFC3339, "0001-01-01T01:00:00Z") 315 So(err, ShouldBeNil) 316 midT, err := time.Parse(time.RFC3339, "2013-01-01T01:00:00Z") 317 So(err, ShouldBeNil) 318 maxT, err := time.Parse(time.RFC3339, "9999-01-01T01:00:00Z") 319 So(err, ShouldBeNil) 320 t, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z") 321 So(err, ShouldBeNil) 322 So(sec.Key("TIME").RangeTime(t, minT, maxT).String(), ShouldEqual, t.String()) 323 324 Convey("Get value in range with default value", func() { 325 So(sec.Key("FLOAT64").RangeFloat64(5, 0, 1), ShouldEqual, 5) 326 So(sec.Key("INT").RangeInt(7, 0, 5), ShouldEqual, 7) 327 So(sec.Key("INT").RangeInt64(7, 0, 5), ShouldEqual, 7) 328 So(sec.Key("TIME").RangeTime(t, minT, midT).String(), ShouldEqual, t.String()) 329 }) 330 }) 331 332 Convey("Get values into slice", func() { 333 sec := f.Section("array") 334 So(strings.Join(sec.Key("STRINGS").Strings(","), ","), ShouldEqual, "en,zh,de") 335 So(len(sec.Key("STRINGS_404").Strings(",")), ShouldEqual, 0) 336 337 vals1 := sec.Key("FLOAT64S").Float64s(",") 338 float64sEqual(vals1, 1.1, 2.2, 3.3) 339 340 vals2 := sec.Key("INTS").Ints(",") 341 intsEqual(vals2, 1, 2, 3) 342 343 vals3 := sec.Key("INTS").Int64s(",") 344 int64sEqual(vals3, 1, 2, 3) 345 346 vals4 := sec.Key("UINTS").Uints(",") 347 uintsEqual(vals4, 1, 2, 3) 348 349 vals5 := sec.Key("UINTS").Uint64s(",") 350 uint64sEqual(vals5, 1, 2, 3) 351 352 vals6 := sec.Key("BOOLS").Bools(",") 353 boolsEqual(vals6, true, false, false) 354 355 t, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z") 356 So(err, ShouldBeNil) 357 vals7 := sec.Key("TIMES").Times(",") 358 timesEqual(vals7, t, t, t) 359 }) 360 361 Convey("Test string slice escapes", func() { 362 sec := f.Section("string escapes") 363 So(sec.Key("key1").Strings(","), ShouldResemble, []string{"value1", "value2", "value3"}) 364 So(sec.Key("key2").Strings(","), ShouldResemble, []string{"value1, value2"}) 365 So(sec.Key("key3").Strings(","), ShouldResemble, []string{`val\ue1`, "value2"}) 366 So(sec.Key("key4").Strings(","), ShouldResemble, []string{`value1\`, `value\\2`}) 367 So(sec.Key("key5").Strings(",,"), ShouldResemble, []string{"value1,, value2"}) 368 So(sec.Key("key6").Strings(" "), ShouldResemble, []string{"aaa", "bbb and space", "ccc"}) 369 }) 370 371 Convey("Get valid values into slice", func() { 372 sec := f.Section("array") 373 vals1 := sec.Key("FLOAT64S").ValidFloat64s(",") 374 float64sEqual(vals1, 1.1, 2.2, 3.3) 375 376 vals2 := sec.Key("INTS").ValidInts(",") 377 intsEqual(vals2, 1, 2, 3) 378 379 vals3 := sec.Key("INTS").ValidInt64s(",") 380 int64sEqual(vals3, 1, 2, 3) 381 382 vals4 := sec.Key("UINTS").ValidUints(",") 383 uintsEqual(vals4, 1, 2, 3) 384 385 vals5 := sec.Key("UINTS").ValidUint64s(",") 386 uint64sEqual(vals5, 1, 2, 3) 387 388 vals6 := sec.Key("BOOLS").ValidBools(",") 389 boolsEqual(vals6, true, false, false) 390 391 t, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z") 392 So(err, ShouldBeNil) 393 vals7 := sec.Key("TIMES").ValidTimes(",") 394 timesEqual(vals7, t, t, t) 395 }) 396 397 Convey("Get values one type into slice of another type", func() { 398 sec := f.Section("array") 399 vals1 := sec.Key("STRINGS").ValidFloat64s(",") 400 So(vals1, ShouldBeEmpty) 401 402 vals2 := sec.Key("STRINGS").ValidInts(",") 403 So(vals2, ShouldBeEmpty) 404 405 vals3 := sec.Key("STRINGS").ValidInt64s(",") 406 So(vals3, ShouldBeEmpty) 407 408 vals4 := sec.Key("STRINGS").ValidUints(",") 409 So(vals4, ShouldBeEmpty) 410 411 vals5 := sec.Key("STRINGS").ValidUint64s(",") 412 So(vals5, ShouldBeEmpty) 413 414 vals6 := sec.Key("STRINGS").ValidBools(",") 415 So(vals6, ShouldBeEmpty) 416 417 vals7 := sec.Key("STRINGS").ValidTimes(",") 418 So(vals7, ShouldBeEmpty) 419 }) 420 421 Convey("Get valid values into slice without errors", func() { 422 sec := f.Section("array") 423 vals1, err := sec.Key("FLOAT64S").StrictFloat64s(",") 424 So(err, ShouldBeNil) 425 float64sEqual(vals1, 1.1, 2.2, 3.3) 426 427 vals2, err := sec.Key("INTS").StrictInts(",") 428 So(err, ShouldBeNil) 429 intsEqual(vals2, 1, 2, 3) 430 431 vals3, err := sec.Key("INTS").StrictInt64s(",") 432 So(err, ShouldBeNil) 433 int64sEqual(vals3, 1, 2, 3) 434 435 vals4, err := sec.Key("UINTS").StrictUints(",") 436 So(err, ShouldBeNil) 437 uintsEqual(vals4, 1, 2, 3) 438 439 vals5, err := sec.Key("UINTS").StrictUint64s(",") 440 So(err, ShouldBeNil) 441 uint64sEqual(vals5, 1, 2, 3) 442 443 vals6, err := sec.Key("BOOLS").StrictBools(",") 444 So(err, ShouldBeNil) 445 boolsEqual(vals6, true, false, false) 446 447 t, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z") 448 So(err, ShouldBeNil) 449 vals7, err := sec.Key("TIMES").StrictTimes(",") 450 So(err, ShouldBeNil) 451 timesEqual(vals7, t, t, t) 452 }) 453 454 Convey("Get invalid values into slice", func() { 455 sec := f.Section("array") 456 vals1, err := sec.Key("STRINGS").StrictFloat64s(",") 457 So(vals1, ShouldBeEmpty) 458 So(err, ShouldNotBeNil) 459 460 vals2, err := sec.Key("STRINGS").StrictInts(",") 461 So(vals2, ShouldBeEmpty) 462 So(err, ShouldNotBeNil) 463 464 vals3, err := sec.Key("STRINGS").StrictInt64s(",") 465 So(vals3, ShouldBeEmpty) 466 So(err, ShouldNotBeNil) 467 468 vals4, err := sec.Key("STRINGS").StrictUints(",") 469 So(vals4, ShouldBeEmpty) 470 So(err, ShouldNotBeNil) 471 472 vals5, err := sec.Key("STRINGS").StrictUint64s(",") 473 So(vals5, ShouldBeEmpty) 474 So(err, ShouldNotBeNil) 475 476 vals6, err := sec.Key("STRINGS").StrictBools(",") 477 So(vals6, ShouldBeEmpty) 478 So(err, ShouldNotBeNil) 479 480 vals7, err := sec.Key("STRINGS").StrictTimes(",") 481 So(vals7, ShouldBeEmpty) 482 So(err, ShouldNotBeNil) 483 }) 484 }) 485} 486 487func TestKey_StringsWithShadows(t *testing.T) { 488 Convey("Get strings of shadows of a key", t, func() { 489 f, err := ini.ShadowLoad([]byte("")) 490 So(err, ShouldBeNil) 491 So(f, ShouldNotBeNil) 492 493 k, err := f.Section("").NewKey("NUMS", "1,2") 494 So(err, ShouldBeNil) 495 So(k, ShouldNotBeNil) 496 k, err = f.Section("").NewKey("NUMS", "4,5,6") 497 So(err, ShouldBeNil) 498 So(k, ShouldNotBeNil) 499 500 So(k.StringsWithShadows(","), ShouldResemble, []string{"1", "2", "4", "5", "6"}) 501 }) 502} 503 504func TestKey_SetValue(t *testing.T) { 505 Convey("Set value of key", t, func() { 506 f := ini.Empty() 507 So(f, ShouldNotBeNil) 508 509 k, err := f.Section("").NewKey("NAME", "ini") 510 So(err, ShouldBeNil) 511 So(k, ShouldNotBeNil) 512 So(k.Value(), ShouldEqual, "ini") 513 514 k.SetValue("ini.v1") 515 So(k.Value(), ShouldEqual, "ini.v1") 516 }) 517} 518 519func TestKey_NestedValues(t *testing.T) { 520 Convey("Read and write nested values", t, func() { 521 f, err := ini.LoadSources(ini.LoadOptions{ 522 AllowNestedValues: true, 523 }, []byte(` 524aws_access_key_id = foo 525aws_secret_access_key = bar 526region = us-west-2 527s3 = 528 max_concurrent_requests=10 529 max_queue_size=1000`)) 530 So(err, ShouldBeNil) 531 So(f, ShouldNotBeNil) 532 533 So(f.Section("").Key("s3").NestedValues(), ShouldResemble, []string{"max_concurrent_requests=10", "max_queue_size=1000"}) 534 535 var buf bytes.Buffer 536 _, err = f.WriteTo(&buf) 537 So(err, ShouldBeNil) 538 So(buf.String(), ShouldEqual, `aws_access_key_id = foo 539aws_secret_access_key = bar 540region = us-west-2 541s3 = 542 max_concurrent_requests=10 543 max_queue_size=1000 544 545`) 546 }) 547} 548 549func TestRecursiveValues(t *testing.T) { 550 Convey("Recursive values should not reflect on same key", t, func() { 551 f, err := ini.Load([]byte(` 552NAME = ini 553expires = yes 554[package] 555NAME = %(NAME)s 556expires = %(expires)s`)) 557 So(err, ShouldBeNil) 558 So(f, ShouldNotBeNil) 559 560 So(f.Section("package").Key("NAME").String(), ShouldEqual, "ini") 561 So(f.Section("package").Key("expires").String(), ShouldEqual, "yes") 562 }) 563 564 Convey("Recursive value with no target found", t, func() { 565 f, err := ini.Load([]byte(` 566[foo] 567bar = %(missing)s 568`)) 569 So(err, ShouldBeNil) 570 So(f, ShouldNotBeNil) 571 572 So(f.Section("foo").Key("bar").String(), ShouldEqual, "%(missing)s") 573 }) 574} 575