New Upstream Release - golang-github-go-ini-ini

Ready changes

Summary

Merged new upstream version: 1.66.4 (was: 1.55.0).

Resulting package

Built on 2022-03-07T11:17 (took 1m50s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-releases golang-github-go-ini-ini-dev

Lintian Result

Diff

diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..4a2d918
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,12 @@
+# http://editorconfig.org
+
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*_test.go]
+trim_trailing_whitespace = false
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
deleted file mode 100644
index ab57ef7..0000000
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ /dev/null
@@ -1,23 +0,0 @@
----
-name: Bug report
-about: Create a report to help us improve
-title: ''
-labels: ''
-assignees: ''
-
----
-
-**Describe the bug**
-A clear and concise description of what the bug is.
-
-**To Reproduce**
-A code snippet to reproduce the problem described above.
-
-**Expected behavior**
-A clear and concise description of what you expected to happen.
-
-**Screenshots**
-If applicable, add screenshots to help explain your problem.
-
-**Additional context**
-Add any other context about the problem here, or any suggestion to fix the problem.
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 0000000..9048e7d
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,51 @@
+name: Bug report
+description: File a bug report to help us improve
+labels: ["bug"]
+body:
+  - type: markdown
+    attributes:
+      value: |
+        Thanks for taking the time to fill out this bug report!
+
+        - Before you file an issue read the [Contributing guide](https://github.com/go-ini/ini/blob/main/.github/contributing.md).
+        - Check to make sure someone hasn't already opened a similar [issue](https://github.com/go-ini/ini/issues).
+  - type: input
+    attributes:
+      label: Version
+      description: Please specify the exact Go module version you're reporting for.
+    validations:
+      required: true
+  - type: textarea
+    attributes:
+      label: Describe the bug
+      description: A clear and concise description of what the bug is.
+    validations:
+      required: true
+  - type: textarea
+    attributes:
+      label: To reproduce
+      description: A code snippet to reproduce the problem described above.
+    validations:
+      required: true
+  - type: textarea
+    attributes:
+      label: Expected behavior
+      description: A clear and concise description of what you expected to happen.
+    validations:
+      required: true
+  - type: textarea
+    attributes:
+      label: Additional context
+      description: |
+        Links? References? Suggestions? Anything that will give us more context about the issue you are encountering!
+
+        Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
+    validations:
+      required: false
+  - type: checkboxes
+    attributes:
+      label: Code of Conduct
+      description: By submitting this issue, you agree to follow our [Code of Conduct](https://go.dev/conduct)
+      options:
+        - label: I agree to follow this project's Code of Conduct
+          required: true
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..3ba13e0
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1 @@
+blank_issues_enabled: false
diff --git a/.github/ISSUE_TEMPLATE/documentation.yml b/.github/ISSUE_TEMPLATE/documentation.yml
new file mode 100644
index 0000000..1400a6e
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/documentation.yml
@@ -0,0 +1,30 @@
+name: Improve documentation
+description: Suggest an idea or a patch for documentation
+labels: ["documentation"]
+body:
+  - type: markdown
+    attributes:
+      value: |
+        Thanks for taking the time to fill out this form!
+
+        - Before you file an issue read the [Contributing guide](https://github.com/go-ini/ini/blob/main/.github/contributing.md).
+        - Check to make sure someone hasn't already opened a similar [issue](https://github.com/go-ini/ini/issues).
+  - type: textarea
+    attributes:
+      label: What needs to be improved? Please describe
+      description: A clear and concise description of what is wrong or missing.
+    validations:
+      required: true
+  - type: textarea
+    attributes:
+      label: Why do you think it is important?
+      description: A clear and concise explanation of the rationale.
+    validations:
+      required: true
+  - type: checkboxes
+    attributes:
+      label: Code of Conduct
+      description: By submitting this issue, you agree to follow our [Code of Conduct](https://go.dev/conduct)
+      options:
+        - label: I agree to follow this project's Code of Conduct
+          required: true
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
deleted file mode 100644
index bbcbbe7..0000000
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ /dev/null
@@ -1,20 +0,0 @@
----
-name: Feature request
-about: Suggest an idea for this project
-title: ''
-labels: ''
-assignees: ''
-
----
-
-**Is your feature request related to a problem? Please describe.**
-A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
-
-**Describe the solution you'd like**
-A clear and concise description of what you want to happen.
-
-**Describe alternatives you've considered**
-A clear and concise description of any alternative solutions or features you've considered.
-
-**Additional context**
-Add any other context or screenshots about the feature request here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
new file mode 100644
index 0000000..33896ac
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -0,0 +1,45 @@
+name: Feature request
+description: Suggest an idea for this project
+labels: ["feature"]
+body:
+  - type: markdown
+    attributes:
+      value: |
+        Thanks for taking the time to fill out this form!
+
+        - Before you file an issue read the [Contributing guide](https://github.com/go-ini/ini/blob/main/.github/contributing.md).
+        - Check to make sure someone hasn't already opened a similar [issue](https://github.com/go-ini/ini/issues).
+  - type: textarea
+    attributes:
+      label: Describe the feature
+      description: A clear and concise description of what the problem is, e.g. I'm always frustrated when [...]
+    validations:
+      required: true
+  - type: textarea
+    attributes:
+      label: Describe the solution you'd like
+      description: A clear and concise description of what you want to happen.
+    validations:
+      required: true
+  - type: textarea
+    attributes:
+      label: Describe alternatives you've considered
+      description: A clear and concise description of any alternative solutions or features you've considered.
+    validations:
+      required: true
+  - type: textarea
+    attributes:
+      label: Additional context
+      description: |
+        Links? References? Suggestions? Anything that will give us more context about the feature you are requesting!
+
+        Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
+    validations:
+      required: false
+  - type: checkboxes
+    attributes:
+      label: Code of Conduct
+      description: By submitting this issue, you agree to follow our [Code of Conduct](https://go.dev/conduct)
+      options:
+        - label: I agree to follow this project's Code of Conduct
+          required: true
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index b4565ae..94bb3d3 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,3 +1,11 @@
-### What problem should be fixed?
+### Describe the pull request
 
-### Have you added test cases to catch the problem?
+A clear and concise description of what the pull request is about, i.e. what problem should be fixed?
+
+Link to the issue: <!-- paste the issue link here, or put "n/a" if not applicable -->
+
+### Checklist
+
+- [ ] I agree to follow the [Code of Conduct](https://go.dev/conduct) by submitting this pull request.
+- [ ] I have read and acknowledge the [Contributing guide](https://github.com/go-ini/ini/blob/main/.github/contributing.md).
+- [ ] I have added test cases to cover the new code.
diff --git a/.github/contributing.md b/.github/contributing.md
new file mode 100644
index 0000000..79a3ee9
--- /dev/null
+++ b/.github/contributing.md
@@ -0,0 +1,55 @@
+# Welcome to go-ini contributing guide
+
+Thank you for investing your time in contributing to our projects!
+
+Read our [Code of Conduct](https://go.dev/conduct) to keep our community approachable and respectable.
+
+In this guide you will get an overview of the contribution workflow from opening an issue, creating a PR, reviewing, and merging the PR.
+
+Use the table of contents icon <img src="https://github.com/github/docs/raw/50561895328b8f369694973252127b7d93899d83/assets/images/table-of-contents.png" width="25" height="25" /> on the top left corner of this document to get to a specific section of this guide quickly.
+
+## New contributor guide
+
+To get an overview of the project, read the [README](/README.md). Here are some resources to help you get started with open source contributions:
+
+- [Finding ways to contribute to open source on GitHub](https://docs.github.com/en/get-started/exploring-projects-on-github/finding-ways-to-contribute-to-open-source-on-github)
+- [Set up Git](https://docs.github.com/en/get-started/quickstart/set-up-git)
+- [GitHub flow](https://docs.github.com/en/get-started/quickstart/github-flow)
+- [Collaborating with pull requests](https://docs.github.com/en/github/collaborating-with-pull-requests)
+
+In addition to the general guides with open source contributions, you would also need to:
+
+- Have basic knowledge about INI configuration format and programming in [Go](https://go.dev/).
+- Have a working local development setup with a reasonable good IDE or editor like [Visual Studio Code](https://code.visualstudio.com/docs/languages/go), [GoLand](https://www.jetbrains.com/go/) or [Vim](https://github.com/fatih/vim-go).
+
+## Issues
+
+### Create a new issue
+
+- [Check to make sure](https://docs.github.com/en/github/searching-for-information-on-github/searching-on-github/searching-issues-and-pull-requests#search-by-the-title-body-or-comments) someone hasn't already opened a similar [issue](https://github.com/go-ini/ini/issues).
+- If a similar issue doesn't exist, open a new issue using a relevant [issue form](https://github.com/go-ini/ini/issues/new/choose).
+
+### Pick up an issue to solve
+
+- Scan through our [existing issues](https://github.com/go-ini/ini/issues) to find one that interests you.
+- The [good first issue](https://github.com/go-ini/ini/labels/good%20first%20issue) is a good place to start exploring issues that are well-groomed for newcomers.
+- Do not hesitate to ask for more details or clarifying questions on the issue!
+- Communicate on the issue you are intended to pick up _before_ starting working on it.
+- Every issue that gets picked up will have an expected timeline for the implementation, the issue may be reassigned after the expected timeline. Please be responsible and proactive on the communication 🙇‍♂️
+
+## Pull requests
+
+When you're finished with the changes, create a pull request, or a series of pull requests if necessary.
+
+Contributing to another codebase is not as simple as code changes, it is also about contributing influence to the design. Therefore, we kindly ask you that:
+
+- Please acknowledge that no pull request is guaranteed to be merged.
+- Please always do a self-review before requesting reviews from others.
+- Please expect code review to be strict and may have multiple rounds.
+- Please make self-contained incremental changes, pull requests with huge diff may be rejected for review.
+- Please use English in code comments and docstring.
+- Please do not force push unless absolutely necessary. Force pushes make review much harder in multiple rounds, and we use [Squash and merge](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/about-pull-request-merges#squash-and-merge-your-pull-request-commits) so you don't need to worry about messy commits and just focus on the changes.
+
+## Your PR is merged!
+
+Congratulations 🎉🎉 Thanks again for taking the effort to have this journey with us 🌟
diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
new file mode 100644
index 0000000..ea1947f
--- /dev/null
+++ b/.github/workflows/go.yml
@@ -0,0 +1,60 @@
+name: Go
+on:
+  push:
+    branches: [ main ]
+    paths:
+      - '**.go'
+      - 'go.mod'
+      - '.golangci.yml'
+      - '.github/workflows/go.yml'
+  pull_request:
+    paths:
+      - '**.go'
+      - 'go.mod'
+      - '.golangci.yml'
+      - '.github/workflows/go.yml'
+env:
+  GOPROXY: "https://proxy.golang.org"
+
+jobs:
+  lint:
+    name: Lint
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v2
+      - name: Init Go Modules
+        run: |
+          go mod init github.com/go-ini/ini
+          go mod tidy
+      - name: Run golangci-lint
+        uses: golangci/golangci-lint-action@v2
+        with:
+          version: latest
+          args: --timeout=30m
+          skip-pkg-cache: true # Wrokaround of the "tar" problem: https://github.com/golangci/golangci-lint-action/issues/244
+
+  test:
+    name: Test
+    strategy:
+      matrix:
+        go-version: [ 1.15.x, 1.16.x, 1.17.x ]
+        platform: [ ubuntu-latest, macos-latest, windows-latest ]
+    runs-on: ${{ matrix.platform }}
+    steps:
+      - name: Install Go
+        uses: actions/setup-go@v2
+        with:
+          go-version: ${{ matrix.go-version }}
+      - name: Checkout code
+        uses: actions/checkout@v2
+      - name: Run tests with coverage
+        run: |
+          go mod init github.com/go-ini/ini
+          go mod tidy
+          go test -v -race -coverprofile=coverage -covermode=atomic ./...
+      - name: Upload coverage report to Codecov
+        uses: codecov/codecov-action@v1.5.0
+        with:
+          file: ./coverage
+          flags: unittests
diff --git a/.github/workflows/lsif.yml b/.github/workflows/lsif.yml
index dd4d948..49cefe8 100644
--- a/.github/workflows/lsif.yml
+++ b/.github/workflows/lsif.yml
@@ -1,17 +1,28 @@
 name: LSIF
-on: [push]
+on:
+  push:
+    paths:
+      - '**.go'
+      - 'go.mod'
+      - '.github/workflows/lsif.yml'
+env:
+  GOPROXY: "https://proxy.golang.org"
+
 jobs:
-  build:
+  lsif-go:
+    if: github.repository == 'go-ini/ini'
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v1
+      - uses: actions/checkout@v2
       - name: Generate LSIF data
         uses: sourcegraph/lsif-go-action@master
+      - name: Upload LSIF data to sourcegraph.com
+        continue-on-error: true
+        uses: docker://sourcegraph/src-cli:latest
         with:
-          verbose: 'true'
-      - name: Upload LSIF data
-        uses: sourcegraph/lsif-upload-action@master
+          args: lsif upload -github-token=${{ secrets.GITHUB_TOKEN }}
+      - name: Upload LSIF data to sourcegraph.unknwon.cn
         continue-on-error: true
+        uses: docker://sourcegraph/src-cli:latest
         with:
-          endpoint: https://sourcegraph.com
-          github_token: ${{ secrets.GITHUB_TOKEN }}
+          args: -endpoint=https://sourcegraph.unknwon.cn lsif upload -github-token=${{ secrets.GITHUB_TOKEN }}
diff --git a/.gitignore b/.gitignore
index 1241112..588388b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,4 @@ ini.sublime-workspace
 testdata/conf_reflect.ini
 .idea
 /.vscode
+.DS_Store
diff --git a/.golangci.yml b/.golangci.yml
new file mode 100644
index 0000000..b7256ba
--- /dev/null
+++ b/.golangci.yml
@@ -0,0 +1,21 @@
+linters-settings:
+  nakedret:
+    max-func-lines: 0 # Disallow any unnamed return statement
+
+linters:
+  enable:
+    - deadcode
+    - errcheck
+    - gosimple
+    - govet
+    - ineffassign
+    - staticcheck
+    - structcheck
+    - typecheck
+    - unused
+    - varcheck
+    - nakedret
+    - gofmt
+    - rowserrcheck
+    - unconvert
+    - goimports
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 4db2e66..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,21 +0,0 @@
-language: go
-os: linux
-dist: xenial
-go:
-  - 1.6.x
-  - 1.7.x
-  - 1.8.x
-  - 1.9.x
-  - 1.10.x
-  - 1.11.x
-  - 1.12.x
-  - 1.13.x
-  - 1.14.x
-install: skip
-script:
-  - go get golang.org/x/tools/cmd/cover
-  - go get github.com/smartystreets/goconvey
-  - mkdir -p $HOME/gopath/src/gopkg.in
-  - ln -s $HOME/gopath/src/github.com/go-ini/ini $HOME/gopath/src/gopkg.in/ini.v1
-  - cd $HOME/gopath/src/gopkg.in/ini.v1
-  - go test -v -cover -race
diff --git a/README.md b/README.md
index 783eb06..1e42944 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,9 @@
 # INI
 
-[![Build Status](https://img.shields.io/travis/go-ini/ini/master.svg?style=for-the-badge&logo=travis)](https://travis-ci.org/go-ini/ini) [![Sourcegraph](https://img.shields.io/badge/view%20on-Sourcegraph-brightgreen.svg?style=for-the-badge&logo=sourcegraph)](https://sourcegraph.com/github.com/go-ini/ini)
+[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/go-ini/ini/Go?logo=github&style=for-the-badge)](https://github.com/go-ini/ini/actions?query=workflow%3AGo)
+[![codecov](https://img.shields.io/codecov/c/github/go-ini/ini/master?logo=codecov&style=for-the-badge)](https://codecov.io/gh/go-ini/ini)
+[![GoDoc](https://img.shields.io/badge/GoDoc-Reference-blue?style=for-the-badge&logo=go)](https://pkg.go.dev/github.com/go-ini/ini?tab=doc)
+[![Sourcegraph](https://img.shields.io/badge/view%20on-Sourcegraph-brightgreen.svg?style=for-the-badge&logo=sourcegraph)](https://sourcegraph.com/github.com/go-ini/ini)
 
 ![](https://avatars0.githubusercontent.com/u/10216035?v=3&s=200)
 
@@ -21,7 +24,7 @@ Package ini provides INI file read and write functionality in Go.
 
 ## Installation
 
-The minimum requirement of Go is **1.6**.
+The minimum requirement of Go is **1.12**.
 
 ```sh
 $ go get gopkg.in/ini.v1
@@ -33,6 +36,7 @@ Please add `-u` flag to update in the future.
 
 - [Getting Started](https://ini.unknwon.io/docs/intro/getting_started)
 - [API Documentation](https://gowalker.org/gopkg.in/ini.v1)
+- 中国大陆镜像:https://ini.unknwon.cn
 
 ## License
 
diff --git a/bench_test.go b/bench_test.go
index a1244a7..9fd72f0 100644
--- a/bench_test.go
+++ b/bench_test.go
@@ -12,16 +12,14 @@
 // License for the specific language governing permissions and limitations
 // under the License.
 
-package ini_test
+package ini
 
 import (
 	"testing"
-
-	"gopkg.in/ini.v1"
 )
 
-func newTestFile(block bool) *ini.File {
-	c, _ := ini.Load([]byte(confData))
+func newTestFile(block bool) *File {
+	c, _ := Load([]byte(confData))
 	c.BlockMode = block
 	return c
 }
diff --git a/codecov.yml b/codecov.yml
new file mode 100644
index 0000000..31f646e
--- /dev/null
+++ b/codecov.yml
@@ -0,0 +1,9 @@
+coverage:
+  range: "60...95"
+  status:
+    project:
+      default:
+        threshold: 1%
+
+comment:
+  layout: 'diff'
diff --git a/data_source.go b/data_source.go
index bbedf36..c3a541f 100644
--- a/data_source.go
+++ b/data_source.go
@@ -66,10 +66,10 @@ func parseDataSource(source interface{}) (dataSource, error) {
 		return sourceFile{s}, nil
 	case []byte:
 		return &sourceData{s}, nil
-	case io.Reader:
-		return &sourceReadCloser{ioutil.NopCloser(s)}, nil
 	case io.ReadCloser:
 		return &sourceReadCloser{s}, nil
+	case io.Reader:
+		return &sourceReadCloser{ioutil.NopCloser(s)}, nil
 	default:
 		return nil, fmt.Errorf("error parsing data source: unknown type %q", s)
 	}
diff --git a/debian/changelog b/debian/changelog
index bd6968f..b11ba94 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+golang-github-go-ini-ini (1.66.4-1) UNRELEASED; urgency=low
+
+  * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Mon, 07 Mar 2022 11:15:38 -0000
+
 golang-github-go-ini-ini (1.55.0-1) unstable; urgency=medium
 
   * Team Upload.
diff --git a/file.go b/file.go
index f95606f..9d91c31 100644
--- a/file.go
+++ b/file.go
@@ -55,6 +55,9 @@ func newFile(dataSources []dataSource, opts LoadOptions) *File {
 	if len(opts.KeyValueDelimiterOnWrite) == 0 {
 		opts.KeyValueDelimiterOnWrite = "="
 	}
+	if len(opts.ChildSectionDelimiter) == 0 {
+		opts.ChildSectionDelimiter = "."
+	}
 
 	return &File{
 		BlockMode:   true,
@@ -82,7 +85,7 @@ func (f *File) NewSection(name string) (*Section, error) {
 		return nil, errors.New("empty section name")
 	}
 
-	if f.options.Insensitive && name != DefaultSection {
+	if (f.options.Insensitive || f.options.InsensitiveSections) && name != DefaultSection {
 		name = strings.ToLower(name)
 	}
 
@@ -139,12 +142,18 @@ func (f *File) GetSection(name string) (*Section, error) {
 	return secs[0], err
 }
 
+// HasSection returns true if the file contains a section with given name.
+func (f *File) HasSection(name string) bool {
+	section, _ := f.GetSection(name)
+	return section != nil
+}
+
 // SectionsByName returns all sections with given name.
 func (f *File) SectionsByName(name string) ([]*Section, error) {
 	if len(name) == 0 {
 		name = DefaultSection
 	}
-	if f.options.Insensitive {
+	if f.options.Insensitive || f.options.InsensitiveSections {
 		name = strings.ToLower(name)
 	}
 
@@ -165,8 +174,9 @@ func (f *File) SectionsByName(name string) ([]*Section, error) {
 func (f *File) Section(name string) *Section {
 	sec, err := f.GetSection(name)
 	if err != nil {
-		// Note: It's OK here because the only possible error is empty section name,
-		// but if it's empty, this piece of code won't be executed.
+		if name == "" {
+			name = DefaultSection
+		}
 		sec, _ = f.NewSection(name)
 		return sec
 	}
@@ -236,7 +246,7 @@ func (f *File) DeleteSectionWithIndex(name string, index int) error {
 	if len(name) == 0 {
 		name = DefaultSection
 	}
-	if f.options.Insensitive {
+	if f.options.Insensitive || f.options.InsensitiveSections {
 		name = strings.ToLower(name)
 	}
 
@@ -299,6 +309,9 @@ func (f *File) Reload() (err error) {
 			}
 			return err
 		}
+		if f.options.ShortCircuit {
+			return nil
+		}
 	}
 	return nil
 }
@@ -347,7 +360,7 @@ func (f *File) writeToBuffer(indent string) (*bytes.Buffer, error) {
 			}
 		}
 
-		if i > 0 || DefaultHeader {
+		if i > 0 || DefaultHeader || (i == 0 && strings.ToUpper(sec.name) != DefaultSection) {
 			if _, err := buf.WriteString("[" + sname + "]" + LineBreak); err != nil {
 				return nil, err
 			}
@@ -429,16 +442,16 @@ func (f *File) writeToBuffer(indent string) (*bytes.Buffer, error) {
 				kname = `"""` + kname + `"""`
 			}
 
-			for _, val := range key.ValueWithShadows() {
+			writeKeyValue := func(val string) (bool, error) {
 				if _, err := buf.WriteString(kname); err != nil {
-					return nil, err
+					return false, err
 				}
 
 				if key.isBooleanType {
 					if kname != sec.keyList[len(sec.keyList)-1] {
 						buf.WriteString(LineBreak)
 					}
-					continue KeyList
+					return true, nil
 				}
 
 				// Write out alignment spaces before "=" sign
@@ -451,9 +464,28 @@ func (f *File) writeToBuffer(indent string) (*bytes.Buffer, error) {
 					val = `"""` + val + `"""`
 				} else if !f.options.IgnoreInlineComment && strings.ContainsAny(val, "#;") {
 					val = "`" + val + "`"
+				} else if len(strings.TrimSpace(val)) != len(val) {
+					val = `"` + val + `"`
 				}
 				if _, err := buf.WriteString(equalSign + val + LineBreak); err != nil {
+					return false, err
+				}
+				return false, nil
+			}
+
+			shadows := key.ValueWithShadows()
+			if len(shadows) == 0 {
+				if _, err := writeKeyValue(""); err != nil {
+					return nil, err
+				}
+			}
+
+			for _, val := range shadows {
+				exitLoop, err := writeKeyValue(val)
+				if err != nil {
 					return nil, err
+				} else if exitLoop {
+					continue KeyList
 				}
 			}
 
@@ -494,7 +526,7 @@ func (f *File) WriteTo(w io.Writer) (int64, error) {
 // SaveToIndent writes content to file system with given value indention.
 func (f *File) SaveToIndent(filename, indent string) error {
 	// Note: Because we are truncating with os.Create,
-	// 	so it's safer to save to a temporary file location and rename afte done.
+	// 	so it's safer to save to a temporary file location and rename after done.
 	buf, err := f.writeToBuffer(indent)
 	if err != nil {
 		return err
diff --git a/file_test.go b/file_test.go
index 9cd4c93..6ab6739 100644
--- a/file_test.go
+++ b/file_test.go
@@ -12,61 +12,59 @@
 // License for the specific language governing permissions and limitations
 // under the License.
 
-package ini_test
+package ini
 
 import (
 	"bytes"
 	"io/ioutil"
+	"runtime"
+	"sort"
 	"testing"
 
-	. "github.com/smartystreets/goconvey/convey"
-	"gopkg.in/ini.v1"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func TestEmpty(t *testing.T) {
-	Convey("Create an empty object", t, func() {
-		f := ini.Empty()
-		So(f, ShouldNotBeNil)
+	f := Empty()
+	require.NotNil(t, f)
 
-		// Should only have the default section
-		So(len(f.Sections()), ShouldEqual, 1)
+	// Should only have the default section
+	assert.Len(t, f.Sections(), 1)
 
-		// Default section should not contain any key
-		So(len(f.Section("").Keys()), ShouldBeZeroValue)
-	})
+	// Default section should not contain any key
+	assert.Len(t, f.Section("").Keys(), 0)
 }
 
 func TestFile_NewSection(t *testing.T) {
-	Convey("Create a new section", t, func() {
-		f := ini.Empty()
-		So(f, ShouldNotBeNil)
+	f := Empty()
+	require.NotNil(t, f)
 
-		sec, err := f.NewSection("author")
-		So(err, ShouldBeNil)
-		So(sec, ShouldNotBeNil)
-		So(sec.Name(), ShouldEqual, "author")
+	sec, err := f.NewSection("author")
+	require.NoError(t, err)
+	require.NotNil(t, sec)
+	assert.Equal(t, "author", sec.Name())
 
-		So(f.SectionStrings(), ShouldResemble, []string{ini.DefaultSection, "author"})
+	assert.Equal(t, []string{DefaultSection, "author"}, f.SectionStrings())
 
-		Convey("With duplicated name", func() {
-			sec, err := f.NewSection("author")
-			So(err, ShouldBeNil)
-			So(sec, ShouldNotBeNil)
+	t.Run("with duplicated name", func(t *testing.T) {
+		sec, err := f.NewSection("author")
+		require.NoError(t, err)
+		require.NotNil(t, sec)
 
-			// Does nothing if section already exists
-			So(f.SectionStrings(), ShouldResemble, []string{ini.DefaultSection, "author"})
-		})
+		// Does nothing if section already exists
+		assert.Equal(t, []string{DefaultSection, "author"}, f.SectionStrings())
+	})
 
-		Convey("With empty string", func() {
-			_, err := f.NewSection("")
-			So(err, ShouldNotBeNil)
-		})
+	t.Run("with empty string", func(t *testing.T) {
+		_, err := f.NewSection("")
+		require.Error(t, err)
 	})
 }
 
 func TestFile_NonUniqueSection(t *testing.T) {
-	Convey("Read and write non-unique sections", t, func() {
-		f, err := ini.LoadSources(ini.LoadOptions{
+	t.Run("read and write non-unique sections", func(t *testing.T) {
+		f, err := LoadSources(LoadOptions{
 			AllowNonUniqueSections: true,
 		}, []byte(`[Interface]
 Address = 192.168.2.1
@@ -80,21 +78,21 @@ AllowedIPs = 192.168.2.2/32
 [Peer]
 PublicKey = <client2's publickey>
 AllowedIPs = 192.168.2.3/32`))
-		So(err, ShouldBeNil)
-		So(f, ShouldNotBeNil)
+		require.NoError(t, err)
+		require.NotNil(t, f)
 
 		sec, err := f.NewSection("Peer")
-		So(err, ShouldBeNil)
-		So(f, ShouldNotBeNil)
+		require.NoError(t, err)
+		require.NotNil(t, f)
 
-		sec.NewKey("PublicKey", "<client3's publickey>")
-		sec.NewKey("AllowedIPs", "192.168.2.4/32")
+		_, _ = sec.NewKey("PublicKey", "<client3's publickey>")
+		_, _ = sec.NewKey("AllowedIPs", "192.168.2.4/32")
 
 		var buf bytes.Buffer
 		_, err = f.WriteTo(&buf)
-		So(err, ShouldBeNil)
+		require.NoError(t, err)
 		str := buf.String()
-		So(str, ShouldEqual, `[Interface]
+		assert.Equal(t, `[Interface]
 Address    = 192.168.2.1
 PrivateKey = <server's privatekey>
 ListenPort = 51820
@@ -111,11 +109,11 @@ AllowedIPs = 192.168.2.3/32
 PublicKey  = <client3's publickey>
 AllowedIPs = 192.168.2.4/32
 
-`)
+`, str)
 	})
 
-	Convey("Delete non-unique section", t, func() {
-		f, err := ini.LoadSources(ini.LoadOptions{
+	t.Run("delete non-unique section", func(t *testing.T) {
+		f, err := LoadSources(LoadOptions{
 			AllowNonUniqueSections: true,
 		}, []byte(`[Interface]
 Address    = 192.168.2.1
@@ -135,17 +133,17 @@ PublicKey  = <client3's publickey>
 AllowedIPs = 192.168.2.4/32
 
 `))
-		So(err, ShouldBeNil)
-		So(f, ShouldNotBeNil)
+		require.NoError(t, err)
+		require.NotNil(t, f)
 
 		err = f.DeleteSectionWithIndex("Peer", 1)
-		So(err, ShouldBeNil)
+		require.NoError(t, err)
 
 		var buf bytes.Buffer
 		_, err = f.WriteTo(&buf)
-		So(err, ShouldBeNil)
+		require.NoError(t, err)
 		str := buf.String()
-		So(str, ShouldEqual, `[Interface]
+		assert.Equal(t, `[Interface]
 Address    = 192.168.2.1
 PrivateKey = <server's privatekey>
 ListenPort = 51820
@@ -158,240 +156,299 @@ AllowedIPs = 192.168.2.2/32
 PublicKey  = <client3's publickey>
 AllowedIPs = 192.168.2.4/32
 
-`)
+`, str)
 	})
 
-	Convey("Delete all sections", t, func() {
-		f := ini.Empty(ini.LoadOptions{
+	t.Run("delete all sections", func(t *testing.T) {
+		f := Empty(LoadOptions{
 			AllowNonUniqueSections: true,
 		})
-		So(f, ShouldNotBeNil)
+		require.NotNil(t, f)
 
-		f.NewSections("Interface", "Peer", "Peer")
-		So(f.SectionStrings(), ShouldResemble, []string{ini.DefaultSection, "Interface", "Peer", "Peer"})
+		_ = f.NewSections("Interface", "Peer", "Peer")
+		assert.Equal(t, []string{DefaultSection, "Interface", "Peer", "Peer"}, f.SectionStrings())
 		f.DeleteSection("Peer")
-		So(f.SectionStrings(), ShouldResemble, []string{ini.DefaultSection, "Interface"})
+		assert.Equal(t, []string{DefaultSection, "Interface"}, f.SectionStrings())
 	})
 }
 
 func TestFile_NewRawSection(t *testing.T) {
-	Convey("Create a new raw section", t, func() {
-		f := ini.Empty()
-		So(f, ShouldNotBeNil)
+	f := Empty()
+	require.NotNil(t, f)
 
-		sec, err := f.NewRawSection("comments", `1111111111111111111000000000000000001110000
+	sec, err := f.NewRawSection("comments", `1111111111111111111000000000000000001110000
 111111111111111111100000000000111000000000`)
-		So(err, ShouldBeNil)
-		So(sec, ShouldNotBeNil)
-		So(sec.Name(), ShouldEqual, "comments")
-
-		So(f.SectionStrings(), ShouldResemble, []string{ini.DefaultSection, "comments"})
-		So(f.Section("comments").Body(), ShouldEqual, `1111111111111111111000000000000000001110000
-111111111111111111100000000000111000000000`)
-
-		Convey("With duplicated name", func() {
-			sec, err := f.NewRawSection("comments", `1111111111111111111000000000000000001110000`)
-			So(err, ShouldBeNil)
-			So(sec, ShouldNotBeNil)
-			So(f.SectionStrings(), ShouldResemble, []string{ini.DefaultSection, "comments"})
-
-			// Overwrite previous existed section
-			So(f.Section("comments").Body(), ShouldEqual, `1111111111111111111000000000000000001110000`)
-		})
+	require.NoError(t, err)
+	require.NotNil(t, sec)
+	assert.Equal(t, "comments", sec.Name())
+
+	assert.Equal(t, []string{DefaultSection, "comments"}, f.SectionStrings())
+	assert.Equal(t, `1111111111111111111000000000000000001110000
+111111111111111111100000000000111000000000`, f.Section("comments").Body())
+
+	t.Run("with duplicated name", func(t *testing.T) {
+		sec, err := f.NewRawSection("comments", `1111111111111111111000000000000000001110000`)
+		require.NoError(t, err)
+		require.NotNil(t, sec)
+		assert.Equal(t, []string{DefaultSection, "comments"}, f.SectionStrings())
+
+		// Overwrite previous existed section
+		assert.Equal(t, `1111111111111111111000000000000000001110000`, f.Section("comments").Body())
+	})
 
-		Convey("With empty string", func() {
-			_, err := f.NewRawSection("", "")
-			So(err, ShouldNotBeNil)
-		})
+	t.Run("with empty string", func(t *testing.T) {
+		_, err := f.NewRawSection("", "")
+		require.Error(t, err)
 	})
 }
 
 func TestFile_NewSections(t *testing.T) {
-	Convey("Create new sections", t, func() {
-		f := ini.Empty()
-		So(f, ShouldNotBeNil)
+	f := Empty()
+	require.NotNil(t, f)
 
-		So(f.NewSections("package", "author"), ShouldBeNil)
-		So(f.SectionStrings(), ShouldResemble, []string{ini.DefaultSection, "package", "author"})
+	assert.NoError(t, f.NewSections("package", "author"))
+	assert.Equal(t, []string{DefaultSection, "package", "author"}, f.SectionStrings())
 
-		Convey("With duplicated name", func() {
-			So(f.NewSections("author", "features"), ShouldBeNil)
+	t.Run("with duplicated name", func(t *testing.T) {
+		assert.NoError(t, f.NewSections("author", "features"))
 
-			// Ignore section already exists
-			So(f.SectionStrings(), ShouldResemble, []string{ini.DefaultSection, "package", "author", "features"})
-		})
+		// Ignore section already exists
+		assert.Equal(t, []string{DefaultSection, "package", "author", "features"}, f.SectionStrings())
+	})
 
-		Convey("With empty string", func() {
-			So(f.NewSections("", ""), ShouldNotBeNil)
-		})
+	t.Run("with empty string", func(t *testing.T) {
+		assert.Error(t, f.NewSections("", ""))
 	})
 }
 
 func TestFile_GetSection(t *testing.T) {
-	Convey("Get a section", t, func() {
-		f, err := ini.Load(fullConf)
-		So(err, ShouldBeNil)
-		So(f, ShouldNotBeNil)
-
-		sec, err := f.GetSection("author")
-		So(err, ShouldBeNil)
-		So(sec, ShouldNotBeNil)
-		So(sec.Name(), ShouldEqual, "author")
-
-		Convey("Section not exists", func() {
-			_, err := f.GetSection("404")
-			So(err, ShouldNotBeNil)
-		})
+	f, err := Load(fullConf)
+	require.NoError(t, err)
+	require.NotNil(t, f)
+
+	sec, err := f.GetSection("author")
+	require.NoError(t, err)
+	require.NotNil(t, sec)
+	assert.Equal(t, "author", sec.Name())
+
+	t.Run("section not exists", func(t *testing.T) {
+		_, err := f.GetSection("404")
+		require.Error(t, err)
+	})
+}
+
+func TestFile_HasSection(t *testing.T) {
+	f, err := Load(fullConf)
+	require.NoError(t, err)
+	require.NotNil(t, f)
+
+	sec := f.HasSection("author")
+	assert.True(t, sec)
+
+	t.Run("section not exists", func(t *testing.T) {
+		nonexistent := f.HasSection("404")
+		assert.False(t, nonexistent)
 	})
 }
 
 func TestFile_Section(t *testing.T) {
-	Convey("Get a section", t, func() {
-		f, err := ini.Load(fullConf)
-		So(err, ShouldBeNil)
-		So(f, ShouldNotBeNil)
+	t.Run("get a section", func(t *testing.T) {
+		f, err := Load(fullConf)
+		require.NoError(t, err)
+		require.NotNil(t, f)
 
 		sec := f.Section("author")
-		So(sec, ShouldNotBeNil)
-		So(sec.Name(), ShouldEqual, "author")
+		require.NotNil(t, sec)
+		assert.Equal(t, "author", sec.Name())
 
-		Convey("Section not exists", func() {
+		t.Run("section not exists", func(t *testing.T) {
 			sec := f.Section("404")
-			So(sec, ShouldNotBeNil)
-			So(sec.Name(), ShouldEqual, "404")
+			require.NotNil(t, sec)
+			assert.Equal(t, "404", sec.Name())
 		})
 	})
 
-	Convey("Get default section in lower case with insensitive load", t, func() {
-		f, err := ini.InsensitiveLoad([]byte(`
+	t.Run("get default section in lower case with insensitive load", func(t *testing.T) {
+		f, err := InsensitiveLoad([]byte(`
 [default]
 NAME = ini
 VERSION = v1`))
-		So(err, ShouldBeNil)
-		So(f, ShouldNotBeNil)
+		require.NoError(t, err)
+		require.NotNil(t, f)
 
-		So(f.Section("").Key("name").String(), ShouldEqual, "ini")
-		So(f.Section("").Key("version").String(), ShouldEqual, "v1")
+		assert.Equal(t, "ini", f.Section("").Key("name").String())
+		assert.Equal(t, "v1", f.Section("").Key("version").String())
 	})
-}
 
-func TestFile_Sections(t *testing.T) {
-	Convey("Get all sections", t, func() {
-		f, err := ini.Load(fullConf)
-		So(err, ShouldBeNil)
-		So(f, ShouldNotBeNil)
-
-		secs := f.Sections()
-		names := []string{ini.DefaultSection, "author", "package", "package.sub", "features", "types", "array", "note", "comments", "string escapes", "advance"}
-		So(len(secs), ShouldEqual, len(names))
-		for i, name := range names {
-			So(secs[i].Name(), ShouldEqual, name)
+	t.Run("get sections after deletion", func(t *testing.T) {
+		f, err := Load([]byte(`
+[RANDOM]
+`))
+		require.NoError(t, err)
+		require.NotNil(t, f)
+
+		sectionNames := f.SectionStrings()
+		sort.Strings(sectionNames)
+		assert.Equal(t, []string{DefaultSection, "RANDOM"}, sectionNames)
+
+		for _, currentSection := range sectionNames {
+			f.DeleteSection(currentSection)
+		}
+
+		for sectionParam, expectedSectionName := range map[string]string{
+			"":       DefaultSection,
+			"RANDOM": "RANDOM",
+		} {
+			sec := f.Section(sectionParam)
+			require.NotNil(t, sec)
+			assert.Equal(t, expectedSectionName, sec.Name())
 		}
 	})
+
+}
+
+func TestFile_Sections(t *testing.T) {
+	f, err := Load(fullConf)
+	require.NoError(t, err)
+	require.NotNil(t, f)
+
+	secs := f.Sections()
+	names := []string{DefaultSection, "author", "package", "package.sub", "features", "types", "array", "note", "comments", "string escapes", "advance"}
+	assert.Len(t, secs, len(names))
+	for i, name := range names {
+		assert.Equal(t, name, secs[i].Name())
+	}
 }
 
 func TestFile_ChildSections(t *testing.T) {
-	Convey("Get child sections by parent name", t, func() {
-		f, err := ini.Load([]byte(`
+	f, err := Load([]byte(`
 [node]
 [node.biz1]
 [node.biz2]
 [node.biz3]
 [node.bizN]
 `))
-		So(err, ShouldBeNil)
-		So(f, ShouldNotBeNil)
-
-		children := f.ChildSections("node")
-		names := []string{"node.biz1", "node.biz2", "node.biz3", "node.bizN"}
-		So(len(children), ShouldEqual, len(names))
-		for i, name := range names {
-			So(children[i].Name(), ShouldEqual, name)
-		}
-	})
+	require.NoError(t, err)
+	require.NotNil(t, f)
+
+	children := f.ChildSections("node")
+	names := []string{"node.biz1", "node.biz2", "node.biz3", "node.bizN"}
+	assert.Len(t, children, len(names))
+	for i, name := range names {
+		assert.Equal(t, name, children[i].Name())
+	}
 }
 
 func TestFile_SectionStrings(t *testing.T) {
-	Convey("Get all section names", t, func() {
-		f, err := ini.Load(fullConf)
-		So(err, ShouldBeNil)
-		So(f, ShouldNotBeNil)
+	f, err := Load(fullConf)
+	require.NoError(t, err)
+	require.NotNil(t, f)
 
-		So(f.SectionStrings(), ShouldResemble, []string{ini.DefaultSection, "author", "package", "package.sub", "features", "types", "array", "note", "comments", "string escapes", "advance"})
-	})
+	assert.Equal(t, []string{DefaultSection, "author", "package", "package.sub", "features", "types", "array", "note", "comments", "string escapes", "advance"}, f.SectionStrings())
 }
 
 func TestFile_DeleteSection(t *testing.T) {
-	Convey("Delete a section", t, func() {
-		f := ini.Empty()
-		So(f, ShouldNotBeNil)
+	t.Run("delete a section", func(t *testing.T) {
+		f := Empty()
+		require.NotNil(t, f)
 
-		f.NewSections("author", "package", "features")
+		_ = f.NewSections("author", "package", "features")
 		f.DeleteSection("features")
 		f.DeleteSection("")
-		So(f.SectionStrings(), ShouldResemble, []string{"author", "package"})
+		assert.Equal(t, []string{"author", "package"}, f.SectionStrings())
+	})
+
+	t.Run("delete default section", func(t *testing.T) {
+		f := Empty()
+		require.NotNil(t, f)
+
+		f.Section("").Key("foo").SetValue("bar")
+		f.Section("section1").Key("key1").SetValue("value1")
+		f.DeleteSection("")
+		assert.Equal(t, []string{"section1"}, f.SectionStrings())
+
+		var buf bytes.Buffer
+		_, err := f.WriteTo(&buf)
+		require.NoError(t, err)
+
+		assert.Equal(t, `[section1]
+key1 = value1
+
+`, buf.String())
+	})
+
+	t.Run("delete a section with InsensitiveSections", func(t *testing.T) {
+		f := Empty(LoadOptions{InsensitiveSections: true})
+		require.NotNil(t, f)
+
+		_ = f.NewSections("author", "package", "features")
+		f.DeleteSection("FEATURES")
+		f.DeleteSection("")
+		assert.Equal(t, []string{"author", "package"}, f.SectionStrings())
 	})
 }
 
 func TestFile_Append(t *testing.T) {
-	Convey("Append a data source", t, func() {
-		f := ini.Empty()
-		So(f, ShouldNotBeNil)
+	f := Empty()
+	require.NotNil(t, f)
 
-		So(f.Append(minimalConf, []byte(`
+	assert.NoError(t, f.Append(minimalConf, []byte(`
 [author]
-NAME = Unknwon`)), ShouldBeNil)
+NAME = Unknwon`)))
 
-		Convey("With bad input", func() {
-			So(f.Append(123), ShouldNotBeNil)
-			So(f.Append(minimalConf, 123), ShouldNotBeNil)
-		})
+	t.Run("with bad input", func(t *testing.T) {
+		assert.Error(t, f.Append(123))
+		assert.Error(t, f.Append(minimalConf, 123))
 	})
 }
 
 func TestFile_WriteTo(t *testing.T) {
-	Convey("Write content to somewhere", t, func() {
-		f, err := ini.Load(fullConf)
-		So(err, ShouldBeNil)
-		So(f, ShouldNotBeNil)
+	if runtime.GOOS == "windows" {
+		t.Skip("Skipping testing on Windows")
+	}
+
+	t.Run("write content to somewhere", func(t *testing.T) {
+		f, err := Load(fullConf)
+		require.NoError(t, err)
+		require.NotNil(t, f)
 
 		f.Section("author").Comment = `Information about package author
 # Bio can be written in multiple lines.`
 		f.Section("author").Key("NAME").Comment = "This is author name"
-		f.Section("note").NewBooleanKey("boolean_key")
-		f.Section("note").NewKey("more", "notes")
+		_, _ = f.Section("note").NewBooleanKey("boolean_key")
+		_, _ = f.Section("note").NewKey("more", "notes")
 
 		var buf bytes.Buffer
 		_, err = f.WriteTo(&buf)
-		So(err, ShouldBeNil)
+		require.NoError(t, err)
 
 		golden := "testdata/TestFile_WriteTo.golden"
 		if *update {
-			ioutil.WriteFile(golden, buf.Bytes(), 0644)
+			require.NoError(t, ioutil.WriteFile(golden, buf.Bytes(), 0644))
 		}
 
 		expected, err := ioutil.ReadFile(golden)
-		So(err, ShouldBeNil)
-		So(buf.String(), ShouldEqual, string(expected))
+		require.NoError(t, err)
+		assert.Equal(t, string(expected), buf.String())
 	})
 
-	Convey("Support multiline comments", t, func() {
-		f, err := ini.Load([]byte(`
+	t.Run("support multiline comments", func(t *testing.T) {
+		f, err := Load([]byte(`
 # 
 # general.domain
 # 
 # Domain name of XX system.
 domain      = mydomain.com
 `))
-		So(err, ShouldBeNil)
+		require.NoError(t, err)
 
 		f.Section("").Key("test").Comment = "Multiline\nComment"
 
 		var buf bytes.Buffer
 		_, err = f.WriteTo(&buf)
-		So(err, ShouldBeNil)
+		require.NoError(t, err)
 
-		So(buf.String(), ShouldEqual, `# 
+		assert.Equal(t, `# 
 # general.domain
 # 
 # Domain name of XX system.
@@ -400,27 +457,43 @@ domain = mydomain.com
 ; Comment
 test   = 
 
-`)
+`, buf.String())
 
 	})
+
+	t.Run("keep leading and trailing spaces in value", func(t *testing.T) {
+		f, _ := Load([]byte(`[foo]
+bar1 = '  val ue1 '
+bar2 = """  val ue2 """
+bar3 = "  val ue3 "
+`))
+		require.NotNil(t, f)
+
+		var buf bytes.Buffer
+		_, err := f.WriteTo(&buf)
+		require.NoError(t, err)
+		assert.Equal(t, `[foo]
+bar1 = "  val ue1 "
+bar2 = "  val ue2 "
+bar3 = "  val ue3 "
+
+`, buf.String())
+	})
 }
 
 func TestFile_SaveTo(t *testing.T) {
-	Convey("Write content to somewhere", t, func() {
-		f, err := ini.Load(fullConf)
-		So(err, ShouldBeNil)
-		So(f, ShouldNotBeNil)
+	f, err := Load(fullConf)
+	require.NoError(t, err)
+	require.NotNil(t, f)
 
-		So(f.SaveTo("testdata/conf_out.ini"), ShouldBeNil)
-		So(f.SaveToIndent("testdata/conf_out.ini", "\t"), ShouldBeNil)
-	})
+	assert.NoError(t, f.SaveTo("testdata/conf_out.ini"))
+	assert.NoError(t, f.SaveToIndent("testdata/conf_out.ini", "\t"))
 }
 
 func TestFile_WriteToWithOutputDelimiter(t *testing.T) {
-	Convey("Write content to somewhere using a custom output delimiter", t, func() {
-		f, err := ini.LoadSources(ini.LoadOptions{
-			KeyValueDelimiterOnWrite: "->",
-		}, []byte(`[Others]
+	f, err := LoadSources(LoadOptions{
+		KeyValueDelimiterOnWrite: "->",
+	}, []byte(`[Others]
 Cities = HangZhou|Boston
 Visits = 1993-10-07T20:17:05Z, 1993-10-07T20:17:05Z
 Years = 1993,1994
@@ -430,11 +503,11 @@ Populations = 12345678,98765432
 Coordinates = 192.168,10.11
 Flags       = true,false
 Note = Hello world!`))
-		So(err, ShouldBeNil)
-		So(f, ShouldNotBeNil)
+	require.NoError(t, err)
+	require.NotNil(t, f)
 
-		var actual bytes.Buffer
-		var expected = []byte(`[Others]
+	var actual bytes.Buffer
+	var expected = []byte(`[Others]
 Cities      -> HangZhou|Boston
 Visits      -> 1993-10-07T20:17:05Z, 1993-10-07T20:17:05Z
 Years       -> 1993,1994
@@ -446,28 +519,25 @@ Flags       -> true,false
 Note        -> Hello world!
 
 `)
-		_, err = f.WriteTo(&actual)
-		So(err, ShouldBeNil)
+	_, err = f.WriteTo(&actual)
+	require.NoError(t, err)
 
-		So(bytes.Equal(expected, actual.Bytes()), ShouldBeTrue)
-	})
+	assert.Equal(t, expected, actual.Bytes())
 }
 
 // Inspired by https://github.com/go-ini/ini/issues/207
 func TestReloadAfterShadowLoad(t *testing.T) {
-	Convey("Reload file after ShadowLoad", t, func() {
-		f, err := ini.ShadowLoad([]byte(`
+	f, err := ShadowLoad([]byte(`
 [slice]
 v = 1
 v = 2
 v = 3
 `))
-		So(err, ShouldBeNil)
-		So(f, ShouldNotBeNil)
+	require.NoError(t, err)
+	require.NotNil(t, f)
 
-		So(f.Section("slice").Key("v").ValueWithShadows(), ShouldResemble, []string{"1", "2", "3"})
+	assert.Equal(t, []string{"1", "2", "3"}, f.Section("slice").Key("v").ValueWithShadows())
 
-		So(f.Reload(), ShouldBeNil)
-		So(f.Section("slice").Key("v").ValueWithShadows(), ShouldResemble, []string{"1", "2", "3"})
-	})
+	require.NoError(t, f.Reload())
+	assert.Equal(t, []string{"1", "2", "3"}, f.Section("slice").Key("v").ValueWithShadows())
 }
diff --git a/helper_test.go b/helper_test.go
index 1866479..439ad3b 100644
--- a/helper_test.go
+++ b/helper_test.go
@@ -17,13 +17,11 @@ package ini
 import (
 	"testing"
 
-	. "github.com/smartystreets/goconvey/convey"
+	"github.com/stretchr/testify/assert"
 )
 
-func Test_isSlice(t *testing.T) {
-	Convey("Check if a string is in the slice", t, func() {
-		ss := []string{"a", "b", "c"}
-		So(inSlice("a", ss), ShouldBeTrue)
-		So(inSlice("d", ss), ShouldBeFalse)
-	})
+func TestIsInSlice(t *testing.T) {
+	ss := []string{"a", "b", "c"}
+	assert.True(t, inSlice("a", ss))
+	assert.False(t, inSlice("d", ss))
 }
diff --git a/ini.go b/ini.go
index 9f28cb3..ac2a93a 100644
--- a/ini.go
+++ b/ini.go
@@ -1,5 +1,3 @@
-// +build go1.6
-
 // Copyright 2014 Unknwon
 //
 // Licensed under the Apache License, Version 2.0 (the "License"): you may
@@ -18,8 +16,10 @@
 package ini
 
 import (
+	"os"
 	"regexp"
 	"runtime"
+	"strings"
 )
 
 const (
@@ -55,8 +55,10 @@ var (
 	DefaultFormatRight = ""
 )
 
+var inTest = len(os.Args) > 0 && strings.HasSuffix(strings.TrimSuffix(os.Args[0], ".exe"), ".test")
+
 func init() {
-	if runtime.GOOS == "windows" {
+	if runtime.GOOS == "windows" && !inTest {
 		LineBreak = "\r\n"
 	}
 }
@@ -67,12 +69,18 @@ type LoadOptions struct {
 	Loose bool
 	// Insensitive indicates whether the parser forces all section and key names to lowercase.
 	Insensitive bool
+	// InsensitiveSections indicates whether the parser forces all section to lowercase.
+	InsensitiveSections bool
+	// InsensitiveKeys indicates whether the parser forces all key names to lowercase.
+	InsensitiveKeys bool
 	// IgnoreContinuation indicates whether to ignore continuation lines while parsing.
 	IgnoreContinuation bool
 	// IgnoreInlineComment indicates whether to ignore comments at the end of value and treat it as part of value.
 	IgnoreInlineComment bool
 	// SkipUnrecognizableLines indicates whether to skip unrecognizable lines that do not conform to key/value pairs.
 	SkipUnrecognizableLines bool
+	// ShortCircuit indicates whether to ignore other configuration sources after loaded the first available configuration source.
+	ShortCircuit bool
 	// AllowBooleanKeys indicates whether to allow boolean type keys or treat as value is missing.
 	// This type of keys are mostly used in my.cnf.
 	AllowBooleanKeys bool
@@ -103,8 +111,10 @@ type LoadOptions struct {
 	UnparseableSections []string
 	// KeyValueDelimiters is the sequence of delimiters that are used to separate key and value. By default, it is "=:".
 	KeyValueDelimiters string
-	// KeyValueDelimiters is the delimiter that are used to separate key and value output. By default, it is "=".
+	// KeyValueDelimiterOnWrite is the delimiter that are used to separate key and value output. By default, it is "=".
 	KeyValueDelimiterOnWrite string
+	// ChildSectionDelimiter is the delimiter that is used to separate child sections. By default, it is ".".
+	ChildSectionDelimiter string
 	// PreserveSurroundedQuote indicates whether to preserve surrounded quote (single and double quotes).
 	PreserveSurroundedQuote bool
 	// DebugFunc is called to collect debug information (currently only useful to debug parsing Python-style multiline values).
@@ -113,6 +123,8 @@ type LoadOptions struct {
 	ReaderBufferSize int
 	// AllowNonUniqueSections indicates whether to allow sections with the same name multiple times.
 	AllowNonUniqueSections bool
+	// AllowDuplicateShadowValues indicates whether values for shadowed keys should be deduplicated.
+	AllowDuplicateShadowValues bool
 }
 
 // DebugFunc is the type of function called to log parse events.
diff --git a/ini_python_multiline_test.go b/ini_python_multiline_test.go
deleted file mode 100644
index 52a1ed0..0000000
--- a/ini_python_multiline_test.go
+++ /dev/null
@@ -1,65 +0,0 @@
-package ini_test
-
-import (
-	"path/filepath"
-	"testing"
-
-	. "github.com/smartystreets/goconvey/convey"
-	"gopkg.in/ini.v1"
-)
-
-type testData struct {
-	Value1 string `ini:"value1"`
-	Value2 string `ini:"value2"`
-	Value3 string `ini:"value3"`
-}
-
-func TestMultiline(t *testing.T) {
-	Convey("Parse Python-style multiline values", t, func() {
-		path := filepath.Join("testdata", "multiline.ini")
-		f, err := ini.LoadSources(ini.LoadOptions{
-			AllowPythonMultilineValues: true,
-			ReaderBufferSize:           64 * 1024,
-			/*
-				Debug: func(m string) {
-					fmt.Println(m)
-				},
-			*/
-		}, path)
-		So(err, ShouldBeNil)
-		So(f, ShouldNotBeNil)
-		So(len(f.Sections()), ShouldEqual, 1)
-
-		defaultSection := f.Section("")
-		So(f.Section(""), ShouldNotBeNil)
-
-		var testData testData
-		err = defaultSection.MapTo(&testData)
-		So(err, ShouldBeNil)
-		So(testData.Value1, ShouldEqual, "some text here\nsome more text here\n\nthere is an empty line above and below\n")
-		So(testData.Value2, ShouldEqual, "there is an empty line above\nthat is not indented so it should not be part\nof the value")
-		So(testData.Value3, ShouldEqual, `.
-
-Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Eu consequat ac felis donec et odio pellentesque diam volutpat. Mauris commodo quis imperdiet massa tincidunt nunc. Interdum velit euismod in pellentesque. Nisl condimentum id venenatis a condimentum vitae sapien pellentesque. Nascetur ridiculus mus mauris vitae. Posuere urna nec tincidunt praesent semper feugiat. Lorem donec massa sapien faucibus et molestie ac feugiat sed. Ipsum dolor sit amet consectetur adipiscing elit. Enim sed faucibus turpis in eu mi. A diam sollicitudin tempor id. Quam nulla porttitor massa id neque aliquam vestibulum morbi blandit.
-
-Lectus sit amet est placerat in egestas. At risus viverra adipiscing at in tellus integer. Tristique senectus et netus et malesuada fames ac. In hac habitasse platea dictumst. Purus in mollis nunc sed. Pellentesque sit amet porttitor eget dolor morbi. Elit at imperdiet dui accumsan sit amet nulla. Cursus in hac habitasse platea dictumst. Bibendum arcu vitae elementum curabitur. Faucibus ornare suspendisse sed nisi lacus. In vitae turpis massa sed. Libero nunc consequat interdum varius sit amet. Molestie a iaculis at erat pellentesque.
-
-Dui faucibus in ornare quam viverra orci sagittis eu. Purus in mollis nunc sed id semper. Sed arcu non odio euismod lacinia at. Quis commodo odio aenean sed adipiscing diam donec. Quisque id diam vel quam elementum pulvinar. Lorem ipsum dolor sit amet. Purus ut faucibus pulvinar elementum integer enim neque volutpat ac. Fermentum posuere urna nec tincidunt praesent semper feugiat nibh sed. Gravida rutrum quisque non tellus orci. Ipsum dolor sit amet consectetur adipiscing elit pellentesque habitant. Et sollicitudin ac orci phasellus egestas tellus rutrum tellus pellentesque. Eget gravida cum sociis natoque penatibus et magnis. Elementum eu facilisis sed odio morbi quis commodo. Mollis nunc sed id semper risus in hendrerit gravida rutrum. Lorem dolor sed viverra ipsum.
-
-Pellentesque adipiscing commodo elit at imperdiet dui accumsan sit amet. Justo eget magna fermentum iaculis eu non diam. Condimentum mattis pellentesque id nibh tortor id aliquet lectus. Tellus molestie nunc non blandit massa enim. Mauris ultrices eros in cursus turpis. Purus viverra accumsan in nisl nisi scelerisque. Quis lectus nulla at volutpat. Purus ut faucibus pulvinar elementum integer enim. In pellentesque massa placerat duis ultricies lacus sed turpis. Elit sed vulputate mi sit amet mauris commodo. Tellus elementum sagittis vitae et. Duis tristique sollicitudin nibh sit amet commodo nulla facilisi nullam. Lectus vestibulum mattis ullamcorper velit sed ullamcorper morbi tincidunt ornare. Libero id faucibus nisl tincidunt eget nullam. Mattis aliquam faucibus purus in massa tempor. Fames ac turpis egestas sed tempus urna. Gravida in fermentum et sollicitudin ac orci phasellus egestas.
-
-Blandit turpis cursus in hac habitasse. Sed id semper risus in. Amet porttitor eget dolor morbi non arcu. Rhoncus est pellentesque elit ullamcorper dignissim cras tincidunt. Ut morbi tincidunt augue interdum velit. Lorem mollis aliquam ut porttitor leo a. Nunc eget lorem dolor sed viverra. Scelerisque mauris pellentesque pulvinar pellentesque. Elit at imperdiet dui accumsan sit amet. Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. Laoreet non curabitur gravida arcu ac tortor dignissim. Tortor pretium viverra suspendisse potenti nullam ac tortor vitae purus. Lacus sed viverra tellus in hac habitasse platea dictumst vestibulum. Viverra adipiscing at in tellus. Duis at tellus at urna condimentum. Eget gravida cum sociis natoque penatibus et magnis dis parturient. Pharetra massa massa ultricies mi quis hendrerit.
-
-Mauris pellentesque pulvinar pellentesque habitant morbi tristique. Maecenas volutpat blandit aliquam etiam. Sed turpis tincidunt id aliquet. Eget duis at tellus at urna condimentum. Pellentesque habitant morbi tristique senectus et. Amet aliquam id diam maecenas. Volutpat est velit egestas dui id. Vulputate eu scelerisque felis imperdiet proin fermentum leo vel orci. Massa sed elementum tempus egestas sed sed risus pretium. Quam quisque id diam vel quam elementum pulvinar etiam non. Sapien faucibus et molestie ac. Ipsum dolor sit amet consectetur adipiscing. Viverra orci sagittis eu volutpat. Leo urna molestie at elementum. Commodo viverra maecenas accumsan lacus. Non sodales neque sodales ut etiam sit amet. Habitant morbi tristique senectus et netus et malesuada fames. Habitant morbi tristique senectus et netus et malesuada. Blandit aliquam etiam erat velit scelerisque in. Varius duis at consectetur lorem donec massa sapien faucibus et.
-
-Augue mauris augue neque gravida in. Odio ut sem nulla pharetra diam sit amet nisl suscipit. Nulla aliquet enim tortor at auctor urna nunc id. Morbi tristique senectus et netus et malesuada fames ac. Quam id leo in vitae turpis massa sed elementum tempus. Ipsum faucibus vitae aliquet nec ullamcorper sit amet risus nullam. Maecenas volutpat blandit aliquam etiam erat velit scelerisque in. Sagittis nisl rhoncus mattis rhoncus urna neque viverra justo. Massa tempor nec feugiat nisl pretium. Vulputate sapien nec sagittis aliquam malesuada bibendum arcu vitae elementum. Enim lobortis scelerisque fermentum dui faucibus in ornare. Faucibus ornare suspendisse sed nisi lacus. Morbi tristique senectus et netus et malesuada fames. Malesuada pellentesque elit eget gravida cum sociis natoque penatibus et. Dictum non consectetur a erat nam at. Leo urna molestie at elementum eu facilisis sed odio morbi. Quam id leo in vitae turpis massa. Neque egestas congue quisque egestas diam in arcu. Varius morbi enim nunc faucibus a pellentesque sit. Aliquet enim tortor at auctor urna.
-
-Elit scelerisque mauris pellentesque pulvinar pellentesque habitant morbi tristique. Luctus accumsan tortor posuere ac. Eu ultrices vitae auctor eu augue ut lectus arcu bibendum. Pretium nibh ipsum consequat nisl vel pretium lectus. Aliquam etiam erat velit scelerisque in dictum. Sem et tortor consequat id porta nibh venenatis cras sed. A scelerisque purus semper eget duis at tellus at urna. At auctor urna nunc id. Ornare quam viverra orci sagittis eu volutpat odio. Nisl purus in mollis nunc sed id semper. Ornare suspendisse sed nisi lacus sed. Consectetur lorem donec massa sapien faucibus et. Ipsum dolor sit amet consectetur adipiscing elit ut. Porta nibh venenatis cras sed. Dignissim diam quis enim lobortis scelerisque. Quam nulla porttitor massa id. Tellus molestie nunc non blandit massa.
-
-Malesuada fames ac turpis egestas. Suscipit tellus mauris a diam maecenas. Turpis in eu mi bibendum neque egestas. Venenatis tellus in metus vulputate eu scelerisque felis imperdiet. Quis imperdiet massa tincidunt nunc pulvinar sapien et. Urna duis convallis convallis tellus id. Velit egestas dui id ornare arcu odio. Consectetur purus ut faucibus pulvinar elementum integer enim neque. Aenean sed adipiscing diam donec adipiscing tristique. Tortor aliquam nulla facilisi cras fermentum odio eu. Diam in arcu cursus euismod quis viverra nibh cras.
-
-Id ornare arcu odio ut sem. Arcu dictum varius duis at consectetur lorem donec massa sapien. Proin libero nunc consequat interdum varius sit. Ut eu sem integer vitae justo. Vitae elementum curabitur vitae nunc. Diam quam nulla porttitor massa. Lectus mauris ultrices eros in cursus turpis massa tincidunt dui. Natoque penatibus et magnis dis parturient montes. Pellentesque habitant morbi tristique senectus et netus et malesuada fames. Libero nunc consequat interdum varius sit. Rhoncus dolor purus non enim praesent. Pellentesque sit amet porttitor eget. Nibh tortor id aliquet lectus proin nibh. Fermentum iaculis eu non diam phasellus vestibulum lorem sed.
-
-Eu feugiat pretium nibh ipsum consequat nisl vel pretium lectus. Habitant morbi tristique senectus et netus et malesuada fames ac. Urna condimentum mattis pellentesque id. Lorem sed risus ultricies tristique nulla aliquet enim tortor at. Ipsum dolor sit amet consectetur adipiscing elit. Convallis a cras semper auctor neque vitae tempus quam. A diam sollicitudin tempor id eu nisl nunc mi ipsum. Maecenas sed enim ut sem viverra aliquet eget. Massa enim nec dui nunc mattis enim. Nam aliquam sem et tortor consequat. Adipiscing commodo elit at imperdiet dui accumsan sit amet nulla. Nullam eget felis eget nunc lobortis. Mauris a diam maecenas sed enim ut sem viverra. Ornare massa eget egestas purus. In hac habitasse platea dictumst. Ut tortor pretium viverra suspendisse potenti nullam ac tortor. Nisl nunc mi ipsum faucibus. At varius vel pharetra vel. Mauris ultrices eros in cursus turpis massa tincidunt.`)
-	})
-}
diff --git a/ini_test.go b/ini_test.go
index e95aa01..ade2a2c 100644
--- a/ini_test.go
+++ b/ini_test.go
@@ -12,16 +12,18 @@
 // License for the specific language governing permissions and limitations
 // under the License.
 
-package ini_test
+package ini
 
 import (
 	"bytes"
 	"flag"
 	"io/ioutil"
+	"path/filepath"
+	"runtime"
 	"testing"
 
-	. "github.com/smartystreets/goconvey/convey"
-	"gopkg.in/ini.v1"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 const (
@@ -51,57 +53,57 @@ const (
 var update = flag.Bool("update", false, "Update .golden files")
 
 func TestLoad(t *testing.T) {
-	Convey("Load from good data sources", t, func() {
-		f, err := ini.Load(
+	t.Run("load from good data sources", func(t *testing.T) {
+		f, err := Load(
 			"testdata/minimal.ini",
 			[]byte("NAME = ini\nIMPORT_PATH = gopkg.in/%(NAME)s.%(VERSION)s"),
 			bytes.NewReader([]byte(`VERSION = v1`)),
 			ioutil.NopCloser(bytes.NewReader([]byte("[author]\nNAME = Unknwon"))),
 		)
-		So(err, ShouldBeNil)
-		So(f, ShouldNotBeNil)
+		require.NoError(t, err)
+		require.NotNil(t, f)
 
 		// Validate values make sure all sources are loaded correctly
 		sec := f.Section("")
-		So(sec.Key("NAME").String(), ShouldEqual, "ini")
-		So(sec.Key("VERSION").String(), ShouldEqual, "v1")
-		So(sec.Key("IMPORT_PATH").String(), ShouldEqual, "gopkg.in/ini.v1")
+		assert.Equal(t, "ini", sec.Key("NAME").String())
+		assert.Equal(t, "v1", sec.Key("VERSION").String())
+		assert.Equal(t, "gopkg.in/ini.v1", sec.Key("IMPORT_PATH").String())
 
 		sec = f.Section("author")
-		So(sec.Key("NAME").String(), ShouldEqual, "Unknwon")
-		So(sec.Key("E-MAIL").String(), ShouldEqual, "u@gogs.io")
+		assert.Equal(t, "Unknwon", sec.Key("NAME").String())
+		assert.Equal(t, "u@gogs.io", sec.Key("E-MAIL").String())
 	})
 
-	Convey("Load from bad data sources", t, func() {
-		Convey("Invalid input", func() {
-			_, err := ini.Load(notFoundConf)
-			So(err, ShouldNotBeNil)
+	t.Run("load from bad data sources", func(t *testing.T) {
+		t.Run("invalid input", func(t *testing.T) {
+			_, err := Load(notFoundConf)
+			require.Error(t, err)
 		})
 
-		Convey("Unsupported type", func() {
-			_, err := ini.Load(123)
-			So(err, ShouldNotBeNil)
+		t.Run("unsupported type", func(t *testing.T) {
+			_, err := Load(123)
+			require.Error(t, err)
 		})
 	})
 
-	Convey("Can't properly parse INI files containing `#` or `;` in value", t, func() {
-		f, err := ini.Load([]byte(`
+	t.Run("cannot properly parse INI files containing `#` or `;` in value", func(t *testing.T) {
+		f, err := Load([]byte(`
 	[author]
 	NAME = U#n#k#n#w#o#n
 	GITHUB = U;n;k;n;w;o;n
 	`))
-		So(err, ShouldBeNil)
-		So(f, ShouldNotBeNil)
+		require.NoError(t, err)
+		require.NotNil(t, f)
 
 		sec := f.Section("author")
 		nameValue := sec.Key("NAME").String()
 		githubValue := sec.Key("GITHUB").String()
-		So(nameValue, ShouldEqual, "U")
-		So(githubValue, ShouldEqual, "U")
+		assert.Equal(t, "U", nameValue)
+		assert.Equal(t, "U", githubValue)
 	})
 
-	Convey("Can't parse small python-compatible INI files", t, func() {
-		f, err := ini.Load([]byte(`
+	t.Run("cannot parse small python-compatible INI files", func(t *testing.T) {
+		f, err := Load([]byte(`
 [long]
 long_rsa_private_key = -----BEGIN RSA PRIVATE KEY-----
    foo
@@ -110,13 +112,13 @@ long_rsa_private_key = -----BEGIN RSA PRIVATE KEY-----
    barfoo
    -----END RSA PRIVATE KEY-----
 `))
-		So(err, ShouldNotBeNil)
-		So(f, ShouldBeNil)
-		So(err.Error(), ShouldEqual, "key-value delimiter not found: foo\n")
+		require.Error(t, err)
+		assert.Nil(t, f)
+		assert.Equal(t, "key-value delimiter not found: foo\n", err.Error())
 	})
 
-	Convey("Can't parse big python-compatible INI files", t, func() {
-		f, err := ini.Load([]byte(`
+	t.Run("cannot parse big python-compatible INI files", func(t *testing.T) {
+		f, err := Load([]byte(`
 [long]
 long_rsa_private_key = -----BEGIN RSA PRIVATE KEY-----
    1foo
@@ -217,165 +219,222 @@ long_rsa_private_key = -----BEGIN RSA PRIVATE KEY-----
    96barfoo
    -----END RSA PRIVATE KEY-----
 `))
-		So(err, ShouldNotBeNil)
-		So(f, ShouldBeNil)
-		So(err.Error(), ShouldEqual, "key-value delimiter not found: 1foo\n")
+		require.Error(t, err)
+		assert.Nil(t, f)
+		assert.Equal(t, "key-value delimiter not found: 1foo\n", err.Error())
 	})
 }
 
 func TestLooseLoad(t *testing.T) {
-	Convey("Load from data sources with option `Loose` true", t, func() {
-		f, err := ini.LoadSources(ini.LoadOptions{Loose: true}, notFoundConf, minimalConf)
-		So(err, ShouldBeNil)
-		So(f, ShouldNotBeNil)
-
-		Convey("Inverse case", func() {
-			_, err = ini.Load(notFoundConf)
-			So(err, ShouldNotBeNil)
-		})
+	f, err := LoadSources(LoadOptions{Loose: true}, notFoundConf, minimalConf)
+	require.NoError(t, err)
+	require.NotNil(t, f)
+
+	t.Run("inverse case", func(t *testing.T) {
+		_, err = Load(notFoundConf)
+		require.Error(t, err)
 	})
 }
 
 func TestInsensitiveLoad(t *testing.T) {
-	Convey("Insensitive to section and key names", t, func() {
-		f, err := ini.InsensitiveLoad(minimalConf)
-		So(err, ShouldBeNil)
-		So(f, ShouldNotBeNil)
+	t.Run("insensitive to section and key names", func(t *testing.T) {
+		f, err := InsensitiveLoad(minimalConf)
+		require.NoError(t, err)
+		require.NotNil(t, f)
 
-		So(f.Section("Author").Key("e-mail").String(), ShouldEqual, "u@gogs.io")
+		assert.Equal(t, "u@gogs.io", f.Section("Author").Key("e-mail").String())
 
-		Convey("Write out", func() {
+		t.Run("write out", func(t *testing.T) {
 			var buf bytes.Buffer
 			_, err := f.WriteTo(&buf)
-			So(err, ShouldBeNil)
-			So(buf.String(), ShouldEqual, `[author]
+			require.NoError(t, err)
+			assert.Equal(t, `[author]
 e-mail = u@gogs.io
 
-`)
+`,
+				buf.String(),
+			)
 		})
 
-		Convey("Inverse case", func() {
-			f, err := ini.Load(minimalConf)
-			So(err, ShouldBeNil)
-			So(f, ShouldNotBeNil)
+		t.Run("inverse case", func(t *testing.T) {
+			f, err := Load(minimalConf)
+			require.NoError(t, err)
+			require.NotNil(t, f)
 
-			So(f.Section("Author").Key("e-mail").String(), ShouldBeEmpty)
+			assert.Empty(t, f.Section("Author").Key("e-mail").String())
 		})
 	})
 
 	// Ref: https://github.com/go-ini/ini/issues/198
-	Convey("Insensitive load with default section", t, func() {
-		f, err := ini.InsensitiveLoad([]byte(`
+	t.Run("insensitive load with default section", func(t *testing.T) {
+		f, err := InsensitiveLoad([]byte(`
 user = unknwon
 [profile]
 email = unknwon@local
 `))
-		So(err, ShouldBeNil)
-		So(f, ShouldNotBeNil)
+		require.NoError(t, err)
+		require.NotNil(t, f)
 
-		So(f.Section(ini.DefaultSection).Key("user").String(), ShouldEqual, "unknwon")
+		assert.Equal(t, "unknwon", f.Section(DefaultSection).Key("user").String())
 	})
 }
 
 func TestLoadSources(t *testing.T) {
-	Convey("Load from data sources with options", t, func() {
-		Convey("with true `AllowPythonMultilineValues`", func() {
-			Convey("Ignore nonexistent files", func() {
-				f, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: true, Loose: true}, notFoundConf, minimalConf)
-				So(err, ShouldBeNil)
-				So(f, ShouldNotBeNil)
-
-				Convey("Inverse case", func() {
-					_, err = ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: true}, notFoundConf)
-					So(err, ShouldNotBeNil)
-				})
+	t.Run("with true `AllowPythonMultilineValues`", func(t *testing.T) {
+		t.Run("ignore nonexistent files", func(t *testing.T) {
+			f, err := LoadSources(LoadOptions{AllowPythonMultilineValues: true, Loose: true}, notFoundConf, minimalConf)
+			require.NoError(t, err)
+			require.NotNil(t, f)
+
+			t.Run("inverse case", func(t *testing.T) {
+				_, err = LoadSources(LoadOptions{AllowPythonMultilineValues: true}, notFoundConf)
+				require.Error(t, err)
 			})
+		})
 
-			Convey("Insensitive to section and key names", func() {
-				f, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: true, Insensitive: true}, minimalConf)
-				So(err, ShouldBeNil)
-				So(f, ShouldNotBeNil)
+		t.Run("insensitive to section and key names", func(t *testing.T) {
+			f, err := LoadSources(LoadOptions{AllowPythonMultilineValues: true, Insensitive: true}, minimalConf)
+			require.NoError(t, err)
+			require.NotNil(t, f)
 
-				So(f.Section("Author").Key("e-mail").String(), ShouldEqual, "u@gogs.io")
+			assert.Equal(t, "u@gogs.io", f.Section("Author").Key("e-mail").String())
 
-				Convey("Write out", func() {
-					var buf bytes.Buffer
-					_, err := f.WriteTo(&buf)
-					So(err, ShouldBeNil)
-					So(buf.String(), ShouldEqual, `[author]
+			t.Run("write out", func(t *testing.T) {
+				var buf bytes.Buffer
+				_, err := f.WriteTo(&buf)
+				require.NoError(t, err)
+				assert.Equal(t, `[author]
 e-mail = u@gogs.io
 
-`)
-				})
+`,
+					buf.String(),
+				)
+			})
 
-				Convey("Inverse case", func() {
-					f, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: true}, minimalConf)
-					So(err, ShouldBeNil)
-					So(f, ShouldNotBeNil)
+			t.Run("inverse case", func(t *testing.T) {
+				f, err := LoadSources(LoadOptions{AllowPythonMultilineValues: true}, minimalConf)
+				require.NoError(t, err)
+				require.NotNil(t, f)
 
-					So(f.Section("Author").Key("e-mail").String(), ShouldBeEmpty)
-				})
+				assert.Empty(t, f.Section("Author").Key("e-mail").String())
 			})
+		})
+
+		t.Run("insensitive to sections and sensitive to key names", func(t *testing.T) {
+			f, err := LoadSources(LoadOptions{InsensitiveSections: true}, minimalConf)
+			require.NoError(t, err)
+			require.NotNil(t, f)
+
+			assert.Equal(t, "u@gogs.io", f.Section("Author").Key("E-MAIL").String())
+
+			t.Run("write out", func(t *testing.T) {
+				var buf bytes.Buffer
+				_, err := f.WriteTo(&buf)
+				require.NoError(t, err)
+				assert.Equal(t, `[author]
+E-MAIL = u@gogs.io
+
+`,
+					buf.String(),
+				)
+			})
+
+			t.Run("inverse case", func(t *testing.T) {
+				f, err := LoadSources(LoadOptions{}, minimalConf)
+				require.NoError(t, err)
+				require.NotNil(t, f)
+
+				assert.Empty(t, f.Section("Author").Key("e-mail").String())
+			})
+		})
+
+		t.Run("sensitive to sections and insensitive to key names", func(t *testing.T) {
+			f, err := LoadSources(LoadOptions{InsensitiveKeys: true}, minimalConf)
+			require.NoError(t, err)
+			require.NotNil(t, f)
 
-			Convey("Ignore continuation lines", func() {
-				f, err := ini.LoadSources(ini.LoadOptions{
-					AllowPythonMultilineValues: true,
-					IgnoreContinuation:         true,
-				}, []byte(`
+			assert.Equal(t, "u@gogs.io", f.Section("author").Key("e-mail").String())
+
+			t.Run("write out", func(t *testing.T) {
+				var buf bytes.Buffer
+				_, err := f.WriteTo(&buf)
+				require.NoError(t, err)
+				assert.Equal(t, `[author]
+e-mail = u@gogs.io
+
+`,
+					buf.String(),
+				)
+			})
+
+			t.Run("inverse case", func(t *testing.T) {
+				f, err := LoadSources(LoadOptions{}, minimalConf)
+				require.NoError(t, err)
+				require.NotNil(t, f)
+
+				assert.Empty(t, f.Section("Author").Key("e-mail").String())
+			})
+		})
+
+		t.Run("ignore continuation lines", func(t *testing.T) {
+			f, err := LoadSources(LoadOptions{
+				AllowPythonMultilineValues: true,
+				IgnoreContinuation:         true,
+			}, []byte(`
 key1=a\b\
 key2=c\d\
 key3=value`))
-				So(err, ShouldBeNil)
-				So(f, ShouldNotBeNil)
+			require.NoError(t, err)
+			require.NotNil(t, f)
 
-				So(f.Section("").Key("key1").String(), ShouldEqual, `a\b\`)
-				So(f.Section("").Key("key2").String(), ShouldEqual, `c\d\`)
-				So(f.Section("").Key("key3").String(), ShouldEqual, "value")
+			assert.Equal(t, `a\b\`, f.Section("").Key("key1").String())
+			assert.Equal(t, `c\d\`, f.Section("").Key("key2").String())
+			assert.Equal(t, "value", f.Section("").Key("key3").String())
 
-				Convey("Inverse case", func() {
-					f, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: true}, []byte(`
+			t.Run("inverse case", func(t *testing.T) {
+				f, err := LoadSources(LoadOptions{AllowPythonMultilineValues: true}, []byte(`
 key1=a\b\
 key2=c\d\`))
-					So(err, ShouldBeNil)
-					So(f, ShouldNotBeNil)
+				require.NoError(t, err)
+				require.NotNil(t, f)
 
-					So(f.Section("").Key("key1").String(), ShouldEqual, `a\bkey2=c\d`)
-				})
+				assert.Equal(t, `a\bkey2=c\d`, f.Section("").Key("key1").String())
 			})
+		})
 
-			Convey("Ignore inline comments", func() {
-				f, err := ini.LoadSources(ini.LoadOptions{
-					AllowPythonMultilineValues: true,
-					IgnoreInlineComment:        true,
-				}, []byte(`
+		t.Run("ignore inline comments", func(t *testing.T) {
+			f, err := LoadSources(LoadOptions{
+				AllowPythonMultilineValues: true,
+				IgnoreInlineComment:        true,
+			}, []byte(`
 key1=value ;comment
 key2=value2 #comment2
 key3=val#ue #comment3`))
-				So(err, ShouldBeNil)
-				So(f, ShouldNotBeNil)
+			require.NoError(t, err)
+			require.NotNil(t, f)
 
-				So(f.Section("").Key("key1").String(), ShouldEqual, `value ;comment`)
-				So(f.Section("").Key("key2").String(), ShouldEqual, `value2 #comment2`)
-				So(f.Section("").Key("key3").String(), ShouldEqual, `val#ue #comment3`)
+			assert.Equal(t, `value ;comment`, f.Section("").Key("key1").String())
+			assert.Equal(t, `value2 #comment2`, f.Section("").Key("key2").String())
+			assert.Equal(t, `val#ue #comment3`, f.Section("").Key("key3").String())
 
-				Convey("Inverse case", func() {
-					f, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: true}, []byte(`
+			t.Run("inverse case", func(t *testing.T) {
+				f, err := LoadSources(LoadOptions{AllowPythonMultilineValues: true}, []byte(`
 key1=value ;comment
 key2=value2 #comment2`))
-					So(err, ShouldBeNil)
-					So(f, ShouldNotBeNil)
-
-					So(f.Section("").Key("key1").String(), ShouldEqual, `value`)
-					So(f.Section("").Key("key1").Comment, ShouldEqual, `;comment`)
-					So(f.Section("").Key("key2").String(), ShouldEqual, `value2`)
-					So(f.Section("").Key("key2").Comment, ShouldEqual, `#comment2`)
-				})
+				require.NoError(t, err)
+				require.NotNil(t, f)
+
+				assert.Equal(t, `value`, f.Section("").Key("key1").String())
+				assert.Equal(t, `;comment`, f.Section("").Key("key1").Comment)
+				assert.Equal(t, `value2`, f.Section("").Key("key2").String())
+				assert.Equal(t, `#comment2`, f.Section("").Key("key2").Comment)
 			})
+		})
 
-			Convey("Skip unrecognizable lines", func() {
-				f, err := ini.LoadSources(ini.LoadOptions{
-					SkipUnrecognizableLines: true,
-				}, []byte(`
+		t.Run("skip unrecognizable lines", func(t *testing.T) {
+			f, err := LoadSources(LoadOptions{
+				SkipUnrecognizableLines: true,
+			}, []byte(`
 GenerationDepth: 13
 
 BiomeRarityScale: 100
@@ -387,128 +446,136 @@ BiomeRarityScale: 100
 BiomeGroup(NormalBiomes, 3, 99, RoofedForestEnchanted, ForestSakura, FloatingJungle
 BiomeGroup(IceBiomes, 4, 85, Ice Plains)
 `))
-				So(err, ShouldBeNil)
-				So(f, ShouldNotBeNil)
+			require.NoError(t, err)
+			require.NotNil(t, f)
 
-				So(f.Section("").Key("GenerationDepth").String(), ShouldEqual, "13")
-				So(f.Section("").Key("BiomeRarityScale").String(), ShouldEqual, "100")
-				So(f.Section("").HasKey("BiomeGroup"), ShouldBeFalse)
-			})
+			assert.Equal(t, "13", f.Section("").Key("GenerationDepth").String())
+			assert.Equal(t, "100", f.Section("").Key("BiomeRarityScale").String())
+			assert.False(t, f.Section("").HasKey("BiomeGroup"))
+		})
 
-			Convey("Allow boolean type keys", func() {
-				f, err := ini.LoadSources(ini.LoadOptions{
-					AllowPythonMultilineValues: true,
-					AllowBooleanKeys:           true,
-				}, []byte(`
+		t.Run("allow boolean type keys", func(t *testing.T) {
+			f, err := LoadSources(LoadOptions{
+				AllowPythonMultilineValues: true,
+				AllowBooleanKeys:           true,
+			}, []byte(`
 key1=hello
 #key2
 key3`))
-				So(err, ShouldBeNil)
-				So(f, ShouldNotBeNil)
+			require.NoError(t, err)
+			require.NotNil(t, f)
 
-				So(f.Section("").KeyStrings(), ShouldResemble, []string{"key1", "key3"})
-				So(f.Section("").Key("key3").MustBool(false), ShouldBeTrue)
+			assert.Equal(t, []string{"key1", "key3"}, f.Section("").KeyStrings())
+			assert.True(t, f.Section("").Key("key3").MustBool(false))
 
-				Convey("Write out", func() {
-					var buf bytes.Buffer
-					_, err := f.WriteTo(&buf)
-					So(err, ShouldBeNil)
-					So(buf.String(), ShouldEqual, `key1 = hello
+			t.Run("write out", func(t *testing.T) {
+				var buf bytes.Buffer
+				_, err := f.WriteTo(&buf)
+				require.NoError(t, err)
+				assert.Equal(t, `key1 = hello
 # key2
 key3
-`)
-				})
+`,
+					buf.String(),
+				)
+			})
 
-				Convey("Inverse case", func() {
-					_, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: true}, []byte(`
+			t.Run("inverse case", func(t *testing.T) {
+				_, err := LoadSources(LoadOptions{AllowPythonMultilineValues: true}, []byte(`
 key1=hello
 #key2
 key3`))
-					So(err, ShouldNotBeNil)
-				})
+				require.Error(t, err)
 			})
+		})
 
-			Convey("Allow shadow keys", func() {
-				f, err := ini.LoadSources(ini.LoadOptions{AllowShadows: true, AllowPythonMultilineValues: true}, []byte(`
+		t.Run("allow shadow keys", func(t *testing.T) {
+			f, err := LoadSources(LoadOptions{AllowShadows: true, AllowPythonMultilineValues: true}, []byte(`
 [remote "origin"]
 url = https://github.com/Antergone/test1.git
 url = https://github.com/Antergone/test2.git
 fetch = +refs/heads/*:refs/remotes/origin/*`))
-				So(err, ShouldBeNil)
-				So(f, ShouldNotBeNil)
+			require.NoError(t, err)
+			require.NotNil(t, f)
 
-				So(f.Section(`remote "origin"`).Key("url").String(), ShouldEqual, "https://github.com/Antergone/test1.git")
-				So(f.Section(`remote "origin"`).Key("url").ValueWithShadows(), ShouldResemble, []string{
+			assert.Equal(t, "https://github.com/Antergone/test1.git", f.Section(`remote "origin"`).Key("url").String())
+			assert.Equal(
+				t,
+				[]string{
 					"https://github.com/Antergone/test1.git",
 					"https://github.com/Antergone/test2.git",
-				})
-				So(f.Section(`remote "origin"`).Key("fetch").String(), ShouldEqual, "+refs/heads/*:refs/remotes/origin/*")
-
-				Convey("Write out", func() {
-					var buf bytes.Buffer
-					_, err := f.WriteTo(&buf)
-					So(err, ShouldBeNil)
-					So(buf.String(), ShouldEqual, `[remote "origin"]
+				},
+				f.Section(`remote "origin"`).Key("url").ValueWithShadows(),
+			)
+			assert.Equal(t, "+refs/heads/*:refs/remotes/origin/*", f.Section(`remote "origin"`).Key("fetch").String())
+
+			t.Run("write out", func(t *testing.T) {
+				var buf bytes.Buffer
+				_, err := f.WriteTo(&buf)
+				require.NoError(t, err)
+				assert.Equal(t, `[remote "origin"]
 url   = https://github.com/Antergone/test1.git
 url   = https://github.com/Antergone/test2.git
 fetch = +refs/heads/*:refs/remotes/origin/*
 
-`)
-				})
+`,
+					buf.String(),
+				)
+			})
 
-				Convey("Inverse case", func() {
-					f, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: true}, []byte(`
+			t.Run("inverse case", func(t *testing.T) {
+				f, err := LoadSources(LoadOptions{AllowPythonMultilineValues: true}, []byte(`
 [remote "origin"]
 url = https://github.com/Antergone/test1.git
 url = https://github.com/Antergone/test2.git`))
-					So(err, ShouldBeNil)
-					So(f, ShouldNotBeNil)
+				require.NoError(t, err)
+				require.NotNil(t, f)
 
-					So(f.Section(`remote "origin"`).Key("url").String(), ShouldEqual, "https://github.com/Antergone/test2.git")
-				})
+				assert.Equal(t, "https://github.com/Antergone/test2.git", f.Section(`remote "origin"`).Key("url").String())
 			})
+		})
 
-			Convey("Unescape double quotes inside value", func() {
-				f, err := ini.LoadSources(ini.LoadOptions{
-					AllowPythonMultilineValues: true,
-					UnescapeValueDoubleQuotes:  true,
-				}, []byte(`
+		t.Run("unescape double quotes inside value", func(t *testing.T) {
+			f, err := LoadSources(LoadOptions{
+				AllowPythonMultilineValues: true,
+				UnescapeValueDoubleQuotes:  true,
+			}, []byte(`
 create_repo="创建了仓库 <a href=\"%s\">%s</a>"`))
-				So(err, ShouldBeNil)
-				So(f, ShouldNotBeNil)
+			require.NoError(t, err)
+			require.NotNil(t, f)
 
-				So(f.Section("").Key("create_repo").String(), ShouldEqual, `创建了仓库 <a href="%s">%s</a>`)
+			assert.Equal(t, `创建了仓库 <a href="%s">%s</a>`, f.Section("").Key("create_repo").String())
 
-				Convey("Inverse case", func() {
-					f, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: true}, []byte(`
+			t.Run("inverse case", func(t *testing.T) {
+				f, err := LoadSources(LoadOptions{AllowPythonMultilineValues: true}, []byte(`
 create_repo="创建了仓库 <a href=\"%s\">%s</a>"`))
-					So(err, ShouldBeNil)
-					So(f, ShouldNotBeNil)
+				require.NoError(t, err)
+				require.NotNil(t, f)
 
-					So(f.Section("").Key("create_repo").String(), ShouldEqual, `"创建了仓库 <a href=\"%s\">%s</a>"`)
-				})
+				assert.Equal(t, `"创建了仓库 <a href=\"%s\">%s</a>"`, f.Section("").Key("create_repo").String())
 			})
+		})
 
-			Convey("Unescape comment symbols inside value", func() {
-				f, err := ini.LoadSources(ini.LoadOptions{
-					AllowPythonMultilineValues:  true,
-					IgnoreInlineComment:         true,
-					UnescapeValueCommentSymbols: true,
-				}, []byte(`
+		t.Run("unescape comment symbols inside value", func(t *testing.T) {
+			f, err := LoadSources(LoadOptions{
+				AllowPythonMultilineValues:  true,
+				IgnoreInlineComment:         true,
+				UnescapeValueCommentSymbols: true,
+			}, []byte(`
 key = test value <span style="color: %s\; background: %s">more text</span>
 `))
-				So(err, ShouldBeNil)
-				So(f, ShouldNotBeNil)
+			require.NoError(t, err)
+			require.NotNil(t, f)
 
-				So(f.Section("").Key("key").String(), ShouldEqual, `test value <span style="color: %s; background: %s">more text</span>`)
-			})
+			assert.Equal(t, `test value <span style="color: %s; background: %s">more text</span>`, f.Section("").Key("key").String())
+		})
 
-			Convey("Can parse small python-compatible INI files", func() {
-				f, err := ini.LoadSources(ini.LoadOptions{
-					AllowPythonMultilineValues: true,
-					Insensitive:                true,
-					UnparseableSections:        []string{"core_lesson", "comments"},
-				}, []byte(`
+		t.Run("can parse small python-compatible INI files", func(t *testing.T) {
+			f, err := LoadSources(LoadOptions{
+				AllowPythonMultilineValues: true,
+				Insensitive:                true,
+				UnparseableSections:        []string{"core_lesson", "comments"},
+			}, []byte(`
 [long]
 long_rsa_private_key = -----BEGIN RSA PRIVATE KEY-----
   foo
@@ -521,19 +588,19 @@ multiline_list =
   second
   third
 `))
-				So(err, ShouldBeNil)
-				So(f, ShouldNotBeNil)
+			require.NoError(t, err)
+			require.NotNil(t, f)
 
-				So(f.Section("long").Key("long_rsa_private_key").String(), ShouldEqual, "-----BEGIN RSA PRIVATE KEY-----\nfoo\nbar\nfoobar\nbarfoo\n-----END RSA PRIVATE KEY-----")
-				So(f.Section("long").Key("multiline_list").String(), ShouldEqual, "\nfirst\nsecond\nthird")
-			})
+			assert.Equal(t, "-----BEGIN RSA PRIVATE KEY-----\n  foo\n  bar\n  foobar\n  barfoo\n  -----END RSA PRIVATE KEY-----", f.Section("long").Key("long_rsa_private_key").String())
+			assert.Equal(t, "\n  first\n  second\n  third", f.Section("long").Key("multiline_list").String())
+		})
 
-			Convey("Can parse big python-compatible INI files", func() {
-				f, err := ini.LoadSources(ini.LoadOptions{
-					AllowPythonMultilineValues: true,
-					Insensitive:                true,
-					UnparseableSections:        []string{"core_lesson", "comments"},
-				}, []byte(`
+		t.Run("can parse big python-compatible INI files", func(t *testing.T) {
+			f, err := LoadSources(LoadOptions{
+				AllowPythonMultilineValues: true,
+				Insensitive:                true,
+				UnparseableSections:        []string{"core_lesson", "comments"},
+			}, []byte(`
 [long]
 long_rsa_private_key = -----BEGIN RSA PRIVATE KEY-----
    1foo
@@ -634,115 +701,117 @@ long_rsa_private_key = -----BEGIN RSA PRIVATE KEY-----
    96barfoo
    -----END RSA PRIVATE KEY-----
 `))
-				So(err, ShouldBeNil)
-				So(f, ShouldNotBeNil)
-
-				So(f.Section("long").Key("long_rsa_private_key").String(), ShouldEqual, `-----BEGIN RSA PRIVATE KEY-----
-1foo
-2bar
-3foobar
-4barfoo
-5foo
-6bar
-7foobar
-8barfoo
-9foo
-10bar
-11foobar
-12barfoo
-13foo
-14bar
-15foobar
-16barfoo
-17foo
-18bar
-19foobar
-20barfoo
-21foo
-22bar
-23foobar
-24barfoo
-25foo
-26bar
-27foobar
-28barfoo
-29foo
-30bar
-31foobar
-32barfoo
-33foo
-34bar
-35foobar
-36barfoo
-37foo
-38bar
-39foobar
-40barfoo
-41foo
-42bar
-43foobar
-44barfoo
-45foo
-46bar
-47foobar
-48barfoo
-49foo
-50bar
-51foobar
-52barfoo
-53foo
-54bar
-55foobar
-56barfoo
-57foo
-58bar
-59foobar
-60barfoo
-61foo
-62bar
-63foobar
-64barfoo
-65foo
-66bar
-67foobar
-68barfoo
-69foo
-70bar
-71foobar
-72barfoo
-73foo
-74bar
-75foobar
-76barfoo
-77foo
-78bar
-79foobar
-80barfoo
-81foo
-82bar
-83foobar
-84barfoo
-85foo
-86bar
-87foobar
-88barfoo
-89foo
-90bar
-91foobar
-92barfoo
-93foo
-94bar
-95foobar
-96barfoo
------END RSA PRIVATE KEY-----`)
-			})
+			require.NoError(t, err)
+			require.NotNil(t, f)
 
-			Convey("Allow unparsable sections", func() {
-				f, err := ini.LoadSources(ini.LoadOptions{
-					AllowPythonMultilineValues: true,
-					Insensitive:                true,
-					UnparseableSections:        []string{"core_lesson", "comments"},
-				}, []byte(`
+			assert.Equal(t, `-----BEGIN RSA PRIVATE KEY-----
+   1foo
+   2bar
+   3foobar
+   4barfoo
+   5foo
+   6bar
+   7foobar
+   8barfoo
+   9foo
+   10bar
+   11foobar
+   12barfoo
+   13foo
+   14bar
+   15foobar
+   16barfoo
+   17foo
+   18bar
+   19foobar
+   20barfoo
+   21foo
+   22bar
+   23foobar
+   24barfoo
+   25foo
+   26bar
+   27foobar
+   28barfoo
+   29foo
+   30bar
+   31foobar
+   32barfoo
+   33foo
+   34bar
+   35foobar
+   36barfoo
+   37foo
+   38bar
+   39foobar
+   40barfoo
+   41foo
+   42bar
+   43foobar
+   44barfoo
+   45foo
+   46bar
+   47foobar
+   48barfoo
+   49foo
+   50bar
+   51foobar
+   52barfoo
+   53foo
+   54bar
+   55foobar
+   56barfoo
+   57foo
+   58bar
+   59foobar
+   60barfoo
+   61foo
+   62bar
+   63foobar
+   64barfoo
+   65foo
+   66bar
+   67foobar
+   68barfoo
+   69foo
+   70bar
+   71foobar
+   72barfoo
+   73foo
+   74bar
+   75foobar
+   76barfoo
+   77foo
+   78bar
+   79foobar
+   80barfoo
+   81foo
+   82bar
+   83foobar
+   84barfoo
+   85foo
+   86bar
+   87foobar
+   88barfoo
+   89foo
+   90bar
+   91foobar
+   92barfoo
+   93foo
+   94bar
+   95foobar
+   96barfoo
+   -----END RSA PRIVATE KEY-----`,
+				f.Section("long").Key("long_rsa_private_key").String(),
+			)
+		})
+
+		t.Run("allow unparsable sections", func(t *testing.T) {
+			f, err := LoadSources(LoadOptions{
+				AllowPythonMultilineValues: true,
+				Insensitive:                true,
+				UnparseableSections:        []string{"core_lesson", "comments"},
+			}, []byte(`
 Lesson_Location = 87
 Lesson_Status = C
 Score = 3
@@ -754,20 +823,22 @@ my lesson state data – 1111111111111111111000000000000000001110000
 
 [COMMENTS]
 <1><L.Slide#2> This slide has the fuel listed in the wrong units <e.1>`))
-				So(err, ShouldBeNil)
-				So(f, ShouldNotBeNil)
-
-				So(f.Section("").Key("score").String(), ShouldEqual, "3")
-				So(f.Section("").Body(), ShouldBeEmpty)
-				So(f.Section("core_lesson").Body(), ShouldEqual, `my lesson state data – 1111111111111111111000000000000000001110000
-111111111111111111100000000000111000000000 – end my lesson state data`)
-				So(f.Section("comments").Body(), ShouldEqual, `<1><L.Slide#2> This slide has the fuel listed in the wrong units <e.1>`)
-
-				Convey("Write out", func() {
-					var buf bytes.Buffer
-					_, err := f.WriteTo(&buf)
-					So(err, ShouldBeNil)
-					So(buf.String(), ShouldEqual, `lesson_location = 87
+			require.NoError(t, err)
+			require.NotNil(t, f)
+
+			assert.Equal(t, "3", f.Section("").Key("score").String())
+			assert.Empty(t, f.Section("").Body())
+			assert.Equal(t, `my lesson state data – 1111111111111111111000000000000000001110000
+111111111111111111100000000000111000000000 – end my lesson state data`,
+				f.Section("core_lesson").Body(),
+			)
+			assert.Equal(t, `<1><L.Slide#2> This slide has the fuel listed in the wrong units <e.1>`, f.Section("comments").Body())
+
+			t.Run("write out", func(t *testing.T) {
+				var buf bytes.Buffer
+				_, err := f.WriteTo(&buf)
+				require.NoError(t, err)
+				assert.Equal(t, `lesson_location = 87
 lesson_status   = C
 score           = 3
 time            = 00:02:30
@@ -778,270 +849,282 @@ my lesson state data – 1111111111111111111000000000000000001110000
 
 [comments]
 <1><L.Slide#2> This slide has the fuel listed in the wrong units <e.1>
-`)
-				})
+`,
+					buf.String(),
+				)
+			})
 
-				Convey("Inverse case", func() {
-					_, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: true}, []byte(`
+			t.Run("inverse case", func(t *testing.T) {
+				_, err := LoadSources(LoadOptions{AllowPythonMultilineValues: true}, []byte(`
 [CORE_LESSON]
 my lesson state data – 1111111111111111111000000000000000001110000
 111111111111111111100000000000111000000000 – end my lesson state data`))
-					So(err, ShouldNotBeNil)
-				})
+				require.Error(t, err)
 			})
+		})
 
-			Convey("And false `SpaceBeforeInlineComment`", func() {
-				Convey("Can't parse INI files containing `#` or `;` in value", func() {
-					f, err := ini.LoadSources(
-						ini.LoadOptions{AllowPythonMultilineValues: false, SpaceBeforeInlineComment: false},
-						[]byte(`
+		t.Run("and false `SpaceBeforeInlineComment`", func(t *testing.T) {
+			t.Run("cannot parse INI files containing `#` or `;` in value", func(t *testing.T) {
+				f, err := LoadSources(
+					LoadOptions{AllowPythonMultilineValues: false, SpaceBeforeInlineComment: false},
+					[]byte(`
 [author]
 NAME = U#n#k#n#w#o#n
 GITHUB = U;n;k;n;w;o;n
 `))
-					So(err, ShouldBeNil)
-					So(f, ShouldNotBeNil)
-					sec := f.Section("author")
-					nameValue := sec.Key("NAME").String()
-					githubValue := sec.Key("GITHUB").String()
-					So(nameValue, ShouldEqual, "U")
-					So(githubValue, ShouldEqual, "U")
-				})
+				require.NoError(t, err)
+				require.NotNil(t, f)
+				sec := f.Section("author")
+				nameValue := sec.Key("NAME").String()
+				githubValue := sec.Key("GITHUB").String()
+				assert.Equal(t, "U", nameValue)
+				assert.Equal(t, "U", githubValue)
 			})
+		})
 
-			Convey("And true `SpaceBeforeInlineComment`", func() {
-				Convey("Can parse INI files containing `#` or `;` in value", func() {
-					f, err := ini.LoadSources(
-						ini.LoadOptions{AllowPythonMultilineValues: false, SpaceBeforeInlineComment: true},
-						[]byte(`
+		t.Run("and true `SpaceBeforeInlineComment`", func(t *testing.T) {
+			t.Run("can parse INI files containing `#` or `;` in value", func(t *testing.T) {
+				f, err := LoadSources(
+					LoadOptions{AllowPythonMultilineValues: false, SpaceBeforeInlineComment: true},
+					[]byte(`
 [author]
 NAME = U#n#k#n#w#o#n
 GITHUB = U;n;k;n;w;o;n
 `))
-					So(err, ShouldBeNil)
-					So(f, ShouldNotBeNil)
-					sec := f.Section("author")
-					nameValue := sec.Key("NAME").String()
-					githubValue := sec.Key("GITHUB").String()
-					So(nameValue, ShouldEqual, "U#n#k#n#w#o#n")
-					So(githubValue, ShouldEqual, "U;n;k;n;w;o;n")
-				})
+				require.NoError(t, err)
+				require.NotNil(t, f)
+				sec := f.Section("author")
+				nameValue := sec.Key("NAME").String()
+				githubValue := sec.Key("GITHUB").String()
+				assert.Equal(t, "U#n#k#n#w#o#n", nameValue)
+				assert.Equal(t, "U;n;k;n;w;o;n", githubValue)
 			})
 		})
+	})
 
-		Convey("with false `AllowPythonMultilineValues`", func() {
-			Convey("Ignore nonexistent files", func() {
-				f, err := ini.LoadSources(ini.LoadOptions{
+	t.Run("with false `AllowPythonMultilineValues`", func(t *testing.T) {
+		t.Run("ignore nonexistent files", func(t *testing.T) {
+			f, err := LoadSources(LoadOptions{
+				AllowPythonMultilineValues: false,
+				Loose:                      true,
+			}, notFoundConf, minimalConf)
+			require.NoError(t, err)
+			require.NotNil(t, f)
+
+			t.Run("inverse case", func(t *testing.T) {
+				_, err = LoadSources(LoadOptions{
 					AllowPythonMultilineValues: false,
-					Loose:                      true,
-				}, notFoundConf, minimalConf)
-				So(err, ShouldBeNil)
-				So(f, ShouldNotBeNil)
-
-				Convey("Inverse case", func() {
-					_, err = ini.LoadSources(ini.LoadOptions{
-						AllowPythonMultilineValues: false,
-					}, notFoundConf)
-					So(err, ShouldNotBeNil)
-				})
+				}, notFoundConf)
+				require.Error(t, err)
 			})
+		})
 
-			Convey("Insensitive to section and key names", func() {
-				f, err := ini.LoadSources(ini.LoadOptions{
-					AllowPythonMultilineValues: false,
-					Insensitive:                true,
-				}, minimalConf)
-				So(err, ShouldBeNil)
-				So(f, ShouldNotBeNil)
-
-				So(f.Section("Author").Key("e-mail").String(), ShouldEqual, "u@gogs.io")
-
-				Convey("Write out", func() {
-					var buf bytes.Buffer
-					_, err := f.WriteTo(&buf)
-					So(err, ShouldBeNil)
-					So(buf.String(), ShouldEqual, `[author]
+		t.Run("insensitive to section and key names", func(t *testing.T) {
+			f, err := LoadSources(LoadOptions{
+				AllowPythonMultilineValues: false,
+				Insensitive:                true,
+			}, minimalConf)
+			require.NoError(t, err)
+			require.NotNil(t, f)
+
+			assert.Equal(t, "u@gogs.io", f.Section("Author").Key("e-mail").String())
+
+			t.Run("write out", func(t *testing.T) {
+				var buf bytes.Buffer
+				_, err := f.WriteTo(&buf)
+				require.NoError(t, err)
+				assert.Equal(t, `[author]
 e-mail = u@gogs.io
 
-`)
-				})
+`,
+					buf.String(),
+				)
+			})
 
-				Convey("Inverse case", func() {
-					f, err := ini.LoadSources(ini.LoadOptions{
-						AllowPythonMultilineValues: false,
-					}, minimalConf)
-					So(err, ShouldBeNil)
-					So(f, ShouldNotBeNil)
+			t.Run("inverse case", func(t *testing.T) {
+				f, err := LoadSources(LoadOptions{
+					AllowPythonMultilineValues: false,
+				}, minimalConf)
+				require.NoError(t, err)
+				require.NotNil(t, f)
 
-					So(f.Section("Author").Key("e-mail").String(), ShouldBeEmpty)
-				})
+				assert.Empty(t, f.Section("Author").Key("e-mail").String())
 			})
+		})
 
-			Convey("Ignore continuation lines", func() {
-				f, err := ini.LoadSources(ini.LoadOptions{
-					AllowPythonMultilineValues: false,
-					IgnoreContinuation:         true,
-				}, []byte(`
+		t.Run("ignore continuation lines", func(t *testing.T) {
+			f, err := LoadSources(LoadOptions{
+				AllowPythonMultilineValues: false,
+				IgnoreContinuation:         true,
+			}, []byte(`
 key1=a\b\
 key2=c\d\
 key3=value`))
-				So(err, ShouldBeNil)
-				So(f, ShouldNotBeNil)
+			require.NoError(t, err)
+			require.NotNil(t, f)
 
-				So(f.Section("").Key("key1").String(), ShouldEqual, `a\b\`)
-				So(f.Section("").Key("key2").String(), ShouldEqual, `c\d\`)
-				So(f.Section("").Key("key3").String(), ShouldEqual, "value")
+			assert.Equal(t, `a\b\`, f.Section("").Key("key1").String())
+			assert.Equal(t, `c\d\`, f.Section("").Key("key2").String())
+			assert.Equal(t, "value", f.Section("").Key("key3").String())
 
-				Convey("Inverse case", func() {
-					f, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: false}, []byte(`
+			t.Run("inverse case", func(t *testing.T) {
+				f, err := LoadSources(LoadOptions{AllowPythonMultilineValues: false}, []byte(`
 key1=a\b\
 key2=c\d\`))
-					So(err, ShouldBeNil)
-					So(f, ShouldNotBeNil)
+				require.NoError(t, err)
+				require.NotNil(t, f)
 
-					So(f.Section("").Key("key1").String(), ShouldEqual, `a\bkey2=c\d`)
-				})
+				assert.Equal(t, `a\bkey2=c\d`, f.Section("").Key("key1").String())
 			})
+		})
 
-			Convey("Ignore inline comments", func() {
-				f, err := ini.LoadSources(ini.LoadOptions{
-					AllowPythonMultilineValues: false,
-					IgnoreInlineComment:        true,
-				}, []byte(`
+		t.Run("ignore inline comments", func(t *testing.T) {
+			f, err := LoadSources(LoadOptions{
+				AllowPythonMultilineValues: false,
+				IgnoreInlineComment:        true,
+			}, []byte(`
 key1=value ;comment
 key2=value2 #comment2
 key3=val#ue #comment3`))
-				So(err, ShouldBeNil)
-				So(f, ShouldNotBeNil)
+			require.NoError(t, err)
+			require.NotNil(t, f)
 
-				So(f.Section("").Key("key1").String(), ShouldEqual, `value ;comment`)
-				So(f.Section("").Key("key2").String(), ShouldEqual, `value2 #comment2`)
-				So(f.Section("").Key("key3").String(), ShouldEqual, `val#ue #comment3`)
+			assert.Equal(t, `value ;comment`, f.Section("").Key("key1").String())
+			assert.Equal(t, `value2 #comment2`, f.Section("").Key("key2").String())
+			assert.Equal(t, `val#ue #comment3`, f.Section("").Key("key3").String())
 
-				Convey("Inverse case", func() {
-					f, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: false}, []byte(`
+			t.Run("inverse case", func(t *testing.T) {
+				f, err := LoadSources(LoadOptions{AllowPythonMultilineValues: false}, []byte(`
 key1=value ;comment
 key2=value2 #comment2`))
-					So(err, ShouldBeNil)
-					So(f, ShouldNotBeNil)
-
-					So(f.Section("").Key("key1").String(), ShouldEqual, `value`)
-					So(f.Section("").Key("key1").Comment, ShouldEqual, `;comment`)
-					So(f.Section("").Key("key2").String(), ShouldEqual, `value2`)
-					So(f.Section("").Key("key2").Comment, ShouldEqual, `#comment2`)
-				})
+				require.NoError(t, err)
+				require.NotNil(t, f)
+
+				assert.Equal(t, `value`, f.Section("").Key("key1").String())
+				assert.Equal(t, `;comment`, f.Section("").Key("key1").Comment)
+				assert.Equal(t, `value2`, f.Section("").Key("key2").String())
+				assert.Equal(t, `#comment2`, f.Section("").Key("key2").Comment)
 			})
+		})
 
-			Convey("Allow boolean type keys", func() {
-				f, err := ini.LoadSources(ini.LoadOptions{
-					AllowPythonMultilineValues: false,
-					AllowBooleanKeys:           true,
-				}, []byte(`
+		t.Run("allow boolean type keys", func(t *testing.T) {
+			f, err := LoadSources(LoadOptions{
+				AllowPythonMultilineValues: false,
+				AllowBooleanKeys:           true,
+			}, []byte(`
 key1=hello
 #key2
 key3`))
-				So(err, ShouldBeNil)
-				So(f, ShouldNotBeNil)
+			require.NoError(t, err)
+			require.NotNil(t, f)
 
-				So(f.Section("").KeyStrings(), ShouldResemble, []string{"key1", "key3"})
-				So(f.Section("").Key("key3").MustBool(false), ShouldBeTrue)
+			assert.Equal(t, []string{"key1", "key3"}, f.Section("").KeyStrings())
+			assert.True(t, f.Section("").Key("key3").MustBool(false))
 
-				Convey("Write out", func() {
-					var buf bytes.Buffer
-					_, err := f.WriteTo(&buf)
-					So(err, ShouldBeNil)
-					So(buf.String(), ShouldEqual, `key1 = hello
+			t.Run("write out", func(t *testing.T) {
+				var buf bytes.Buffer
+				_, err := f.WriteTo(&buf)
+				require.NoError(t, err)
+				assert.Equal(t, `key1 = hello
 # key2
 key3
-`)
-				})
+`,
+					buf.String(),
+				)
+			})
 
-				Convey("Inverse case", func() {
-					_, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: false}, []byte(`
+			t.Run("inverse case", func(t *testing.T) {
+				_, err := LoadSources(LoadOptions{AllowPythonMultilineValues: false}, []byte(`
 key1=hello
 #key2
 key3`))
-					So(err, ShouldNotBeNil)
-				})
+				require.Error(t, err)
 			})
+		})
 
-			Convey("Allow shadow keys", func() {
-				f, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: false, AllowShadows: true}, []byte(`
+		t.Run("allow shadow keys", func(t *testing.T) {
+			f, err := LoadSources(LoadOptions{AllowPythonMultilineValues: false, AllowShadows: true}, []byte(`
 [remote "origin"]
 url = https://github.com/Antergone/test1.git
 url = https://github.com/Antergone/test2.git
 fetch = +refs/heads/*:refs/remotes/origin/*`))
-				So(err, ShouldBeNil)
-				So(f, ShouldNotBeNil)
+			require.NoError(t, err)
+			require.NotNil(t, f)
 
-				So(f.Section(`remote "origin"`).Key("url").String(), ShouldEqual, "https://github.com/Antergone/test1.git")
-				So(f.Section(`remote "origin"`).Key("url").ValueWithShadows(), ShouldResemble, []string{
+			assert.Equal(t, "https://github.com/Antergone/test1.git", f.Section(`remote "origin"`).Key("url").String())
+			assert.Equal(
+				t,
+				[]string{
 					"https://github.com/Antergone/test1.git",
 					"https://github.com/Antergone/test2.git",
-				})
-				So(f.Section(`remote "origin"`).Key("fetch").String(), ShouldEqual, "+refs/heads/*:refs/remotes/origin/*")
-
-				Convey("Write out", func() {
-					var buf bytes.Buffer
-					_, err := f.WriteTo(&buf)
-					So(err, ShouldBeNil)
-					So(buf.String(), ShouldEqual, `[remote "origin"]
+				},
+				f.Section(`remote "origin"`).Key("url").ValueWithShadows(),
+			)
+			assert.Equal(t, "+refs/heads/*:refs/remotes/origin/*", f.Section(`remote "origin"`).Key("fetch").String())
+
+			t.Run("write out", func(t *testing.T) {
+				var buf bytes.Buffer
+				_, err := f.WriteTo(&buf)
+				require.NoError(t, err)
+				assert.Equal(t, `[remote "origin"]
 url   = https://github.com/Antergone/test1.git
 url   = https://github.com/Antergone/test2.git
 fetch = +refs/heads/*:refs/remotes/origin/*
 
-`)
-				})
+`,
+					buf.String(),
+				)
+			})
 
-				Convey("Inverse case", func() {
-					f, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: false}, []byte(`
+			t.Run("inverse case", func(t *testing.T) {
+				f, err := LoadSources(LoadOptions{AllowPythonMultilineValues: false}, []byte(`
 [remote "origin"]
 url = https://github.com/Antergone/test1.git
 url = https://github.com/Antergone/test2.git`))
-					So(err, ShouldBeNil)
-					So(f, ShouldNotBeNil)
+				require.NoError(t, err)
+				require.NotNil(t, f)
 
-					So(f.Section(`remote "origin"`).Key("url").String(), ShouldEqual, "https://github.com/Antergone/test2.git")
-				})
+				assert.Equal(t, "https://github.com/Antergone/test2.git", f.Section(`remote "origin"`).Key("url").String())
 			})
+		})
 
-			Convey("Unescape double quotes inside value", func() {
-				f, err := ini.LoadSources(ini.LoadOptions{
-					AllowPythonMultilineValues: false,
-					UnescapeValueDoubleQuotes:  true,
-				}, []byte(`
+		t.Run("unescape double quotes inside value", func(t *testing.T) {
+			f, err := LoadSources(LoadOptions{
+				AllowPythonMultilineValues: false,
+				UnescapeValueDoubleQuotes:  true,
+			}, []byte(`
 create_repo="创建了仓库 <a href=\"%s\">%s</a>"`))
-				So(err, ShouldBeNil)
-				So(f, ShouldNotBeNil)
+			require.NoError(t, err)
+			require.NotNil(t, f)
 
-				So(f.Section("").Key("create_repo").String(), ShouldEqual, `创建了仓库 <a href="%s">%s</a>`)
+			assert.Equal(t, `创建了仓库 <a href="%s">%s</a>`, f.Section("").Key("create_repo").String())
 
-				Convey("Inverse case", func() {
-					f, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: false}, []byte(`
+			t.Run("inverse case", func(t *testing.T) {
+				f, err := LoadSources(LoadOptions{AllowPythonMultilineValues: false}, []byte(`
 create_repo="创建了仓库 <a href=\"%s\">%s</a>"`))
-					So(err, ShouldBeNil)
-					So(f, ShouldNotBeNil)
+				require.NoError(t, err)
+				require.NotNil(t, f)
 
-					So(f.Section("").Key("create_repo").String(), ShouldEqual, `"创建了仓库 <a href=\"%s\">%s</a>"`)
-				})
+				assert.Equal(t, `"创建了仓库 <a href=\"%s\">%s</a>"`, f.Section("").Key("create_repo").String())
 			})
+		})
 
-			Convey("Unescape comment symbols inside value", func() {
-				f, err := ini.LoadSources(ini.LoadOptions{
-					AllowPythonMultilineValues:  false,
-					IgnoreInlineComment:         true,
-					UnescapeValueCommentSymbols: true,
-				}, []byte(`
+		t.Run("unescape comment symbols inside value", func(t *testing.T) {
+			f, err := LoadSources(LoadOptions{
+				AllowPythonMultilineValues:  false,
+				IgnoreInlineComment:         true,
+				UnescapeValueCommentSymbols: true,
+			}, []byte(`
 key = test value <span style="color: %s\; background: %s">more text</span>
 `))
-				So(err, ShouldBeNil)
-				So(f, ShouldNotBeNil)
+			require.NoError(t, err)
+			require.NotNil(t, f)
 
-				So(f.Section("").Key("key").String(), ShouldEqual, `test value <span style="color: %s; background: %s">more text</span>`)
-			})
+			assert.Equal(t, `test value <span style="color: %s; background: %s">more text</span>`, f.Section("").Key("key").String())
+		})
 
-			Convey("Can't parse small python-compatible INI files", func() {
-				f, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: false}, []byte(`
+		t.Run("cannot parse small python-compatible INI files", func(t *testing.T) {
+			f, err := LoadSources(LoadOptions{AllowPythonMultilineValues: false}, []byte(`
 [long]
 long_rsa_private_key = -----BEGIN RSA PRIVATE KEY-----
   foo
@@ -1050,13 +1133,13 @@ long_rsa_private_key = -----BEGIN RSA PRIVATE KEY-----
   barfoo
   -----END RSA PRIVATE KEY-----
 `))
-				So(err, ShouldNotBeNil)
-				So(f, ShouldBeNil)
-				So(err.Error(), ShouldEqual, "key-value delimiter not found: foo\n")
-			})
+			require.Error(t, err)
+			assert.Nil(t, f)
+			assert.Equal(t, "key-value delimiter not found: foo\n", err.Error())
+		})
 
-			Convey("Can't parse big python-compatible INI files", func() {
-				f, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: false}, []byte(`
+		t.Run("cannot parse big python-compatible INI files", func(t *testing.T) {
+			f, err := LoadSources(LoadOptions{AllowPythonMultilineValues: false}, []byte(`
 [long]
 long_rsa_private_key = -----BEGIN RSA PRIVATE KEY-----
   1foo
@@ -1157,17 +1240,17 @@ long_rsa_private_key = -----BEGIN RSA PRIVATE KEY-----
   96barfoo
   -----END RSA PRIVATE KEY-----
 `))
-				So(err, ShouldNotBeNil)
-				So(f, ShouldBeNil)
-				So(err.Error(), ShouldEqual, "key-value delimiter not found: 1foo\n")
-			})
+			require.Error(t, err)
+			assert.Nil(t, f)
+			assert.Equal(t, "key-value delimiter not found: 1foo\n", err.Error())
+		})
 
-			Convey("Allow unparsable sections", func() {
-				f, err := ini.LoadSources(ini.LoadOptions{
-					AllowPythonMultilineValues: false,
-					Insensitive:                true,
-					UnparseableSections:        []string{"core_lesson", "comments"},
-				}, []byte(`
+		t.Run("allow unparsable sections", func(t *testing.T) {
+			f, err := LoadSources(LoadOptions{
+				AllowPythonMultilineValues: false,
+				Insensitive:                true,
+				UnparseableSections:        []string{"core_lesson", "comments"},
+			}, []byte(`
 Lesson_Location = 87
 Lesson_Status = C
 Score = 3
@@ -1179,20 +1262,22 @@ my lesson state data – 1111111111111111111000000000000000001110000
 
 [COMMENTS]
 <1><L.Slide#2> This slide has the fuel listed in the wrong units <e.1>`))
-				So(err, ShouldBeNil)
-				So(f, ShouldNotBeNil)
-
-				So(f.Section("").Key("score").String(), ShouldEqual, "3")
-				So(f.Section("").Body(), ShouldBeEmpty)
-				So(f.Section("core_lesson").Body(), ShouldEqual, `my lesson state data – 1111111111111111111000000000000000001110000
-111111111111111111100000000000111000000000 – end my lesson state data`)
-				So(f.Section("comments").Body(), ShouldEqual, `<1><L.Slide#2> This slide has the fuel listed in the wrong units <e.1>`)
-
-				Convey("Write out", func() {
-					var buf bytes.Buffer
-					_, err := f.WriteTo(&buf)
-					So(err, ShouldBeNil)
-					So(buf.String(), ShouldEqual, `lesson_location = 87
+			require.NoError(t, err)
+			require.NotNil(t, f)
+
+			assert.Equal(t, "3", f.Section("").Key("score").String())
+			assert.Empty(t, f.Section("").Body())
+			assert.Equal(t, `my lesson state data – 1111111111111111111000000000000000001110000
+111111111111111111100000000000111000000000 – end my lesson state data`,
+				f.Section("core_lesson").Body(),
+			)
+			assert.Equal(t, `<1><L.Slide#2> This slide has the fuel listed in the wrong units <e.1>`, f.Section("comments").Body())
+
+			t.Run("write out", func(t *testing.T) {
+				var buf bytes.Buffer
+				_, err := f.WriteTo(&buf)
+				require.NoError(t, err)
+				assert.Equal(t, `lesson_location = 87
 lesson_status   = C
 score           = 3
 time            = 00:02:30
@@ -1203,104 +1288,372 @@ my lesson state data – 1111111111111111111000000000000000001110000
 
 [comments]
 <1><L.Slide#2> This slide has the fuel listed in the wrong units <e.1>
-`)
-				})
+`,
+					buf.String(),
+				)
+			})
 
-				Convey("Inverse case", func() {
-					_, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: false}, []byte(`
+			t.Run("inverse case", func(t *testing.T) {
+				_, err := LoadSources(LoadOptions{AllowPythonMultilineValues: false}, []byte(`
 [CORE_LESSON]
 my lesson state data – 1111111111111111111000000000000000001110000
 111111111111111111100000000000111000000000 – end my lesson state data`))
-					So(err, ShouldNotBeNil)
-				})
+				require.Error(t, err)
 			})
+		})
 
-			Convey("And false `SpaceBeforeInlineComment`", func() {
-				Convey("Can't parse INI files containing `#` or `;` in value", func() {
-					f, err := ini.LoadSources(
-						ini.LoadOptions{AllowPythonMultilineValues: true, SpaceBeforeInlineComment: false},
-						[]byte(`
+		t.Run("and false `SpaceBeforeInlineComment`", func(t *testing.T) {
+			t.Run("cannot parse INI files containing `#` or `;` in value", func(t *testing.T) {
+				f, err := LoadSources(
+					LoadOptions{AllowPythonMultilineValues: true, SpaceBeforeInlineComment: false},
+					[]byte(`
 [author]
 NAME = U#n#k#n#w#o#n
 GITHUB = U;n;k;n;w;o;n
 `))
-					So(err, ShouldBeNil)
-					So(f, ShouldNotBeNil)
-					sec := f.Section("author")
-					nameValue := sec.Key("NAME").String()
-					githubValue := sec.Key("GITHUB").String()
-					So(nameValue, ShouldEqual, "U")
-					So(githubValue, ShouldEqual, "U")
-				})
+				require.NoError(t, err)
+				require.NotNil(t, f)
+				sec := f.Section("author")
+				nameValue := sec.Key("NAME").String()
+				githubValue := sec.Key("GITHUB").String()
+				assert.Equal(t, "U", nameValue)
+				assert.Equal(t, "U", githubValue)
 			})
+		})
 
-			Convey("And true `SpaceBeforeInlineComment`", func() {
-				Convey("Can parse INI files containing `#` or `;` in value", func() {
-					f, err := ini.LoadSources(
-						ini.LoadOptions{AllowPythonMultilineValues: true, SpaceBeforeInlineComment: true},
-						[]byte(`
+		t.Run("and true `SpaceBeforeInlineComment`", func(t *testing.T) {
+			t.Run("can parse INI files containing `#` or `;` in value", func(t *testing.T) {
+				f, err := LoadSources(
+					LoadOptions{AllowPythonMultilineValues: true, SpaceBeforeInlineComment: true},
+					[]byte(`
 [author]
 NAME = U#n#k#n#w#o#n
 GITHUB = U;n;k;n;w;o;n
 `))
-					So(err, ShouldBeNil)
-					So(f, ShouldNotBeNil)
-					sec := f.Section("author")
-					nameValue := sec.Key("NAME").String()
-					githubValue := sec.Key("GITHUB").String()
-					So(nameValue, ShouldEqual, "U#n#k#n#w#o#n")
-					So(githubValue, ShouldEqual, "U;n;k;n;w;o;n")
-				})
+				require.NoError(t, err)
+				require.NotNil(t, f)
+				sec := f.Section("author")
+				nameValue := sec.Key("NAME").String()
+				githubValue := sec.Key("GITHUB").String()
+				assert.Equal(t, "U#n#k#n#w#o#n", nameValue)
+				assert.Equal(t, "U;n;k;n;w;o;n", githubValue)
 			})
 		})
 	})
+
+	t.Run("with `ChildSectionDelimiter` ':'", func(t *testing.T) {
+		t.Run("get all keys of parent sections", func(t *testing.T) {
+			f := Empty(LoadOptions{ChildSectionDelimiter: ":"})
+			require.NotNil(t, f)
+
+			k, err := f.Section("package").NewKey("NAME", "ini")
+			require.NoError(t, err)
+			assert.NotNil(t, k)
+			k, err = f.Section("package").NewKey("VERSION", "v1")
+			require.NoError(t, err)
+			assert.NotNil(t, k)
+			k, err = f.Section("package").NewKey("IMPORT_PATH", "gopkg.in/ini.v1")
+			require.NoError(t, err)
+			assert.NotNil(t, k)
+
+			keys := f.Section("package:sub:sub2").ParentKeys()
+			names := []string{"NAME", "VERSION", "IMPORT_PATH"}
+			assert.Equal(t, len(names), len(keys))
+			for i, name := range names {
+				assert.Equal(t, name, keys[i].Name())
+			}
+		})
+
+		t.Run("getting and setting values", func(t *testing.T) {
+			f, err := LoadSources(LoadOptions{ChildSectionDelimiter: ":"}, fullConf)
+			require.NoError(t, err)
+			require.NotNil(t, f)
+
+			t.Run("get parent-keys that are available to the child section", func(t *testing.T) {
+				parentKeys := f.Section("package:sub").ParentKeys()
+				assert.NotNil(t, parentKeys)
+				for _, k := range parentKeys {
+					assert.Equal(t, "CLONE_URL", k.Name())
+				}
+			})
+
+			t.Run("get parent section value", func(t *testing.T) {
+				assert.Equal(t, "https://gopkg.in/ini.v1", f.Section("package:sub").Key("CLONE_URL").String())
+				assert.Equal(t, "https://gopkg.in/ini.v1", f.Section("package:fake:sub").Key("CLONE_URL").String())
+			})
+		})
+
+		t.Run("get child sections by parent name", func(t *testing.T) {
+			f, err := LoadSources(LoadOptions{ChildSectionDelimiter: ":"}, []byte(`
+[node]
+[node:biz1]
+[node:biz2]
+[node.biz3]
+[node.bizN]
+`))
+			require.NoError(t, err)
+			require.NotNil(t, f)
+
+			children := f.ChildSections("node")
+			names := []string{"node:biz1", "node:biz2"}
+			assert.Equal(t, len(names), len(children))
+			for i, name := range names {
+				assert.Equal(t, name, children[i].Name())
+			}
+		})
+	})
+
+	t.Run("ShortCircuit", func(t *testing.T) {
+		t.Run("load the first available configuration, ignore other configuration", func(t *testing.T) {
+			f, err := LoadSources(LoadOptions{ShortCircuit: true}, minimalConf, []byte(`key1 = value1`))
+			require.NotNil(t, f)
+			require.NoError(t, err)
+			var buf bytes.Buffer
+			_, err = f.WriteTo(&buf)
+			require.NoError(t, err)
+			assert.Equal(t, `[author]
+E-MAIL = u@gogs.io
+
+`,
+				buf.String(),
+			)
+		})
+
+		t.Run("return an error when fail to load", func(t *testing.T) {
+			f, err := LoadSources(LoadOptions{ShortCircuit: true}, notFoundConf, minimalConf)
+			assert.Nil(t, f)
+			require.Error(t, err)
+		})
+
+		t.Run("used with Loose to ignore errors that the file does not exist", func(t *testing.T) {
+			f, err := LoadSources(LoadOptions{ShortCircuit: true, Loose: true}, notFoundConf, minimalConf)
+			require.NotNil(t, f)
+			require.NoError(t, err)
+			var buf bytes.Buffer
+			_, err = f.WriteTo(&buf)
+			require.NoError(t, err)
+			assert.Equal(t, `[author]
+E-MAIL = u@gogs.io
+
+`,
+				buf.String(),
+			)
+		})
+
+		t.Run("ensure all sources are loaded without ShortCircuit", func(t *testing.T) {
+			f, err := LoadSources(LoadOptions{ShortCircuit: false}, minimalConf, []byte(`key1 = value1`))
+			require.NotNil(t, f)
+			require.NoError(t, err)
+			var buf bytes.Buffer
+			_, err = f.WriteTo(&buf)
+			require.NoError(t, err)
+			assert.Equal(t, `key1 = value1
+
+[author]
+E-MAIL = u@gogs.io
+
+`,
+				buf.String(),
+			)
+		})
+	})
 }
 
 func Test_KeyValueDelimiters(t *testing.T) {
-	Convey("Custom key-value delimiters", t, func() {
-		f, err := ini.LoadSources(ini.LoadOptions{
+	t.Run("custom key-value delimiters", func(t *testing.T) {
+		f, err := LoadSources(LoadOptions{
 			KeyValueDelimiters: "?!",
 		}, []byte(`
 [section]
 key1?value1
 key2!value2
 `))
-		So(err, ShouldBeNil)
-		So(f, ShouldNotBeNil)
+		require.NoError(t, err)
+		require.NotNil(t, f)
 
-		So(f.Section("section").Key("key1").String(), ShouldEqual, "value1")
-		So(f.Section("section").Key("key2").String(), ShouldEqual, "value2")
+		assert.Equal(t, "value1", f.Section("section").Key("key1").String())
+		assert.Equal(t, "value2", f.Section("section").Key("key2").String())
 	})
 }
 
 func Test_PreserveSurroundedQuote(t *testing.T) {
-	Convey("Preserve surrounded quote test", t, func() {
-		f, err := ini.LoadSources(ini.LoadOptions{
+	t.Run("preserve surrounded quote test", func(t *testing.T) {
+		f, err := LoadSources(LoadOptions{
 			PreserveSurroundedQuote: true,
 		}, []byte(`
 [section]
 key1 = "value1"
 key2 = value2
 `))
-		So(err, ShouldBeNil)
-		So(f, ShouldNotBeNil)
+		require.NoError(t, err)
+		require.NotNil(t, f)
 
-		So(f.Section("section").Key("key1").String(), ShouldEqual, "\"value1\"")
-		So(f.Section("section").Key("key2").String(), ShouldEqual, "value2")
+		assert.Equal(t, "\"value1\"", f.Section("section").Key("key1").String())
+		assert.Equal(t, "value2", f.Section("section").Key("key2").String())
 	})
 
-	Convey("Preserve surrounded quote test inverse test", t, func() {
-		f, err := ini.LoadSources(ini.LoadOptions{
+	t.Run("preserve surrounded quote test inverse test", func(t *testing.T) {
+		f, err := LoadSources(LoadOptions{
 			PreserveSurroundedQuote: false,
 		}, []byte(`
 [section]
 key1 = "value1"
 key2 = value2
 `))
-		So(err, ShouldBeNil)
-		So(f, ShouldNotBeNil)
+		require.NoError(t, err)
+		require.NotNil(t, f)
+
+		assert.Equal(t, "value1", f.Section("section").Key("key1").String())
+		assert.Equal(t, "value2", f.Section("section").Key("key2").String())
+	})
+}
+
+type testData struct {
+	Value1 string `ini:"value1"`
+	Value2 string `ini:"value2"`
+	Value3 string `ini:"value3"`
+}
+
+func TestPythonMultiline(t *testing.T) {
+	if runtime.GOOS == "windows" {
+		t.Skip("Skipping testing on Windows")
+	}
+
+	path := filepath.Join("testdata", "multiline.ini")
+	f, err := LoadSources(LoadOptions{
+		AllowPythonMultilineValues: true,
+		ReaderBufferSize:           64 * 1024,
+	}, path)
+	require.NoError(t, err)
+	require.NotNil(t, f)
+	assert.Len(t, f.Sections(), 1)
+
+	defaultSection := f.Section("")
+	assert.NotNil(t, f.Section(""))
+
+	var testData testData
+	err = defaultSection.MapTo(&testData)
+	require.NoError(t, err)
+	assert.Equal(t, "some text here\n\tsome more text here\n\t\n\tthere is an empty line above and below\n\t", testData.Value1)
+	assert.Equal(t, "there is an empty line above\n    that is not indented so it should not be part\n    of the value", testData.Value2)
+	assert.Equal(t, `.
+ 
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Eu consequat ac felis donec et odio pellentesque diam volutpat. Mauris commodo quis imperdiet massa tincidunt nunc. Interdum velit euismod in pellentesque. Nisl condimentum id venenatis a condimentum vitae sapien pellentesque. Nascetur ridiculus mus mauris vitae. Posuere urna nec tincidunt praesent semper feugiat. Lorem donec massa sapien faucibus et molestie ac feugiat sed. Ipsum dolor sit amet consectetur adipiscing elit. Enim sed faucibus turpis in eu mi. A diam sollicitudin tempor id. Quam nulla porttitor massa id neque aliquam vestibulum morbi blandit.
+ 
+ Lectus sit amet est placerat in egestas. At risus viverra adipiscing at in tellus integer. Tristique senectus et netus et malesuada fames ac. In hac habitasse platea dictumst. Purus in mollis nunc sed. Pellentesque sit amet porttitor eget dolor morbi. Elit at imperdiet dui accumsan sit amet nulla. Cursus in hac habitasse platea dictumst. Bibendum arcu vitae elementum curabitur. Faucibus ornare suspendisse sed nisi lacus. In vitae turpis massa sed. Libero nunc consequat interdum varius sit amet. Molestie a iaculis at erat pellentesque.
+ 
+ Dui faucibus in ornare quam viverra orci sagittis eu. Purus in mollis nunc sed id semper. Sed arcu non odio euismod lacinia at. Quis commodo odio aenean sed adipiscing diam donec. Quisque id diam vel quam elementum pulvinar. Lorem ipsum dolor sit amet. Purus ut faucibus pulvinar elementum integer enim neque volutpat ac. Fermentum posuere urna nec tincidunt praesent semper feugiat nibh sed. Gravida rutrum quisque non tellus orci. Ipsum dolor sit amet consectetur adipiscing elit pellentesque habitant. Et sollicitudin ac orci phasellus egestas tellus rutrum tellus pellentesque. Eget gravida cum sociis natoque penatibus et magnis. Elementum eu facilisis sed odio morbi quis commodo. Mollis nunc sed id semper risus in hendrerit gravida rutrum. Lorem dolor sed viverra ipsum.
+ 
+ Pellentesque adipiscing commodo elit at imperdiet dui accumsan sit amet. Justo eget magna fermentum iaculis eu non diam. Condimentum mattis pellentesque id nibh tortor id aliquet lectus. Tellus molestie nunc non blandit massa enim. Mauris ultrices eros in cursus turpis. Purus viverra accumsan in nisl nisi scelerisque. Quis lectus nulla at volutpat. Purus ut faucibus pulvinar elementum integer enim. In pellentesque massa placerat duis ultricies lacus sed turpis. Elit sed vulputate mi sit amet mauris commodo. Tellus elementum sagittis vitae et. Duis tristique sollicitudin nibh sit amet commodo nulla facilisi nullam. Lectus vestibulum mattis ullamcorper velit sed ullamcorper morbi tincidunt ornare. Libero id faucibus nisl tincidunt eget nullam. Mattis aliquam faucibus purus in massa tempor. Fames ac turpis egestas sed tempus urna. Gravida in fermentum et sollicitudin ac orci phasellus egestas.
+ 
+ Blandit turpis cursus in hac habitasse. Sed id semper risus in. Amet porttitor eget dolor morbi non arcu. Rhoncus est pellentesque elit ullamcorper dignissim cras tincidunt. Ut morbi tincidunt augue interdum velit. Lorem mollis aliquam ut porttitor leo a. Nunc eget lorem dolor sed viverra. Scelerisque mauris pellentesque pulvinar pellentesque. Elit at imperdiet dui accumsan sit amet. Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. Laoreet non curabitur gravida arcu ac tortor dignissim. Tortor pretium viverra suspendisse potenti nullam ac tortor vitae purus. Lacus sed viverra tellus in hac habitasse platea dictumst vestibulum. Viverra adipiscing at in tellus. Duis at tellus at urna condimentum. Eget gravida cum sociis natoque penatibus et magnis dis parturient. Pharetra massa massa ultricies mi quis hendrerit.
+ 
+ Mauris pellentesque pulvinar pellentesque habitant morbi tristique. Maecenas volutpat blandit aliquam etiam. Sed turpis tincidunt id aliquet. Eget duis at tellus at urna condimentum. Pellentesque habitant morbi tristique senectus et. Amet aliquam id diam maecenas. Volutpat est velit egestas dui id. Vulputate eu scelerisque felis imperdiet proin fermentum leo vel orci. Massa sed elementum tempus egestas sed sed risus pretium. Quam quisque id diam vel quam elementum pulvinar etiam non. Sapien faucibus et molestie ac. Ipsum dolor sit amet consectetur adipiscing. Viverra orci sagittis eu volutpat. Leo urna molestie at elementum. Commodo viverra maecenas accumsan lacus. Non sodales neque sodales ut etiam sit amet. Habitant morbi tristique senectus et netus et malesuada fames. Habitant morbi tristique senectus et netus et malesuada. Blandit aliquam etiam erat velit scelerisque in. Varius duis at consectetur lorem donec massa sapien faucibus et.
+ 
+ Augue mauris augue neque gravida in. Odio ut sem nulla pharetra diam sit amet nisl suscipit. Nulla aliquet enim tortor at auctor urna nunc id. Morbi tristique senectus et netus et malesuada fames ac. Quam id leo in vitae turpis massa sed elementum tempus. Ipsum faucibus vitae aliquet nec ullamcorper sit amet risus nullam. Maecenas volutpat blandit aliquam etiam erat velit scelerisque in. Sagittis nisl rhoncus mattis rhoncus urna neque viverra justo. Massa tempor nec feugiat nisl pretium. Vulputate sapien nec sagittis aliquam malesuada bibendum arcu vitae elementum. Enim lobortis scelerisque fermentum dui faucibus in ornare. Faucibus ornare suspendisse sed nisi lacus. Morbi tristique senectus et netus et malesuada fames. Malesuada pellentesque elit eget gravida cum sociis natoque penatibus et. Dictum non consectetur a erat nam at. Leo urna molestie at elementum eu facilisis sed odio morbi. Quam id leo in vitae turpis massa. Neque egestas congue quisque egestas diam in arcu. Varius morbi enim nunc faucibus a pellentesque sit. Aliquet enim tortor at auctor urna.
+ 
+ Elit scelerisque mauris pellentesque pulvinar pellentesque habitant morbi tristique. Luctus accumsan tortor posuere ac. Eu ultrices vitae auctor eu augue ut lectus arcu bibendum. Pretium nibh ipsum consequat nisl vel pretium lectus. Aliquam etiam erat velit scelerisque in dictum. Sem et tortor consequat id porta nibh venenatis cras sed. A scelerisque purus semper eget duis at tellus at urna. At auctor urna nunc id. Ornare quam viverra orci sagittis eu volutpat odio. Nisl purus in mollis nunc sed id semper. Ornare suspendisse sed nisi lacus sed. Consectetur lorem donec massa sapien faucibus et. Ipsum dolor sit amet consectetur adipiscing elit ut. Porta nibh venenatis cras sed. Dignissim diam quis enim lobortis scelerisque. Quam nulla porttitor massa id. Tellus molestie nunc non blandit massa.
+ 
+ Malesuada fames ac turpis egestas. Suscipit tellus mauris a diam maecenas. Turpis in eu mi bibendum neque egestas. Venenatis tellus in metus vulputate eu scelerisque felis imperdiet. Quis imperdiet massa tincidunt nunc pulvinar sapien et. Urna duis convallis convallis tellus id. Velit egestas dui id ornare arcu odio. Consectetur purus ut faucibus pulvinar elementum integer enim neque. Aenean sed adipiscing diam donec adipiscing tristique. Tortor aliquam nulla facilisi cras fermentum odio eu. Diam in arcu cursus euismod quis viverra nibh cras.
+ 
+ Id ornare arcu odio ut sem. Arcu dictum varius duis at consectetur lorem donec massa sapien. Proin libero nunc consequat interdum varius sit. Ut eu sem integer vitae justo. Vitae elementum curabitur vitae nunc. Diam quam nulla porttitor massa. Lectus mauris ultrices eros in cursus turpis massa tincidunt dui. Natoque penatibus et magnis dis parturient montes. Pellentesque habitant morbi tristique senectus et netus et malesuada fames. Libero nunc consequat interdum varius sit. Rhoncus dolor purus non enim praesent. Pellentesque sit amet porttitor eget. Nibh tortor id aliquet lectus proin nibh. Fermentum iaculis eu non diam phasellus vestibulum lorem sed.
+ 
+ Eu feugiat pretium nibh ipsum consequat nisl vel pretium lectus. Habitant morbi tristique senectus et netus et malesuada fames ac. Urna condimentum mattis pellentesque id. Lorem sed risus ultricies tristique nulla aliquet enim tortor at. Ipsum dolor sit amet consectetur adipiscing elit. Convallis a cras semper auctor neque vitae tempus quam. A diam sollicitudin tempor id eu nisl nunc mi ipsum. Maecenas sed enim ut sem viverra aliquet eget. Massa enim nec dui nunc mattis enim. Nam aliquam sem et tortor consequat. Adipiscing commodo elit at imperdiet dui accumsan sit amet nulla. Nullam eget felis eget nunc lobortis. Mauris a diam maecenas sed enim ut sem viverra. Ornare massa eget egestas purus. In hac habitasse platea dictumst. Ut tortor pretium viverra suspendisse potenti nullam ac tortor. Nisl nunc mi ipsum faucibus. At varius vel pharetra vel. Mauris ultrices eros in cursus turpis massa tincidunt.`,
+		testData.Value3,
+	)
+}
+
+func TestPythonMultiline_EOF(t *testing.T) {
+	if runtime.GOOS == "windows" {
+		t.Skip("Skipping testing on Windows")
+	}
+
+	path := filepath.Join("testdata", "multiline_eof.ini")
+	f, err := LoadSources(LoadOptions{
+		AllowPythonMultilineValues: true,
+		ReaderBufferSize:           64 * 1024,
+	}, path)
+	require.NoError(t, err)
+	require.NotNil(t, f)
+	assert.Len(t, f.Sections(), 1)
+
+	defaultSection := f.Section("")
+	assert.NotNil(t, f.Section(""))
+
+	var testData testData
+	err = defaultSection.MapTo(&testData)
+	require.NoError(t, err)
+	assert.Equal(t, "some text here\n\tsome more text here 2", testData.Value1)
+}
+
+func Test_NestedValuesSpanningSections(t *testing.T) {
+	t.Run("basic nested value", func(t *testing.T) {
+		f, err := LoadSources(LoadOptions{
+			AllowNestedValues: true,
+		}, []byte(`
+[section]
+key1 = value1
+key2 =
+  nested1 = nestedvalue1
+`))
+		require.NoError(t, err)
+		require.NotNil(t, f)
+
+		assert.Equal(t, "value1", f.Section("section").Key("key1").String())
+		assert.Equal(t, "", f.Section("section").Key("key2").String())
+		assert.Equal(t, []string{"nested1 = nestedvalue1"}, f.Section("section").Key("key2").NestedValues())
+	})
+
+	t.Run("no nested values", func(t *testing.T) {
+		f, err := LoadSources(LoadOptions{
+			AllowNestedValues: true,
+		}, []byte(`
+[section]
+key1 = value1
+key2 =
+`))
+		require.NoError(t, err)
+		require.NotNil(t, f)
+
+		assert.Equal(t, "value1", f.Section("section").Key("key1").String())
+		assert.Equal(t, "", f.Section("section").Key("key2").String())
+	})
+
+	t.Run("no nested values and following sections", func(t *testing.T) {
+		f, err := LoadSources(LoadOptions{
+			AllowNestedValues: true,
+		}, []byte(`
+[section]
+key1 = value1
+key2 =
+
+[section2]
+key3 = value3
+`))
+		require.NoError(t, err)
+		require.NotNil(t, f)
+
+		assert.Equal(t, "value1", f.Section("section").Key("key1").String())
+		assert.Equal(t, "", f.Section("section").Key("key2").String())
+		assert.Equal(t, "value3", f.Section("section2").Key("key3").String())
+	})
+
+	t.Run("no nested values and following sections with indentation", func(t *testing.T) {
+		f, err := LoadSources(LoadOptions{
+			AllowNestedValues: true,
+		}, []byte(`
+[section]
+key1 = value1
+key2 =
+
+[section2]
+  key3 = value3
+`))
+		require.NoError(t, err)
+		require.NotNil(t, f)
 
-		So(f.Section("section").Key("key1").String(), ShouldEqual, "value1")
-		So(f.Section("section").Key("key2").String(), ShouldEqual, "value2")
+		assert.Equal(t, "value1", f.Section("section").Key("key1").String())
+		assert.Equal(t, "", f.Section("section").Key("key2").String())
+		assert.Equal(t, "value3", f.Section("section2").Key("key3").String())
 	})
 }
diff --git a/key.go b/key.go
index 3c19741..a19d9f3 100644
--- a/key.go
+++ b/key.go
@@ -54,14 +54,16 @@ func (k *Key) addShadow(val string) error {
 		return errors.New("cannot add shadow to auto-increment or boolean key")
 	}
 
-	// Deduplicate shadows based on their values.
-	if k.value == val {
-		return nil
-	}
-	for i := range k.shadows {
-		if k.shadows[i].value == val {
+	if !k.s.f.options.AllowDuplicateShadowValues {
+		// Deduplicate shadows based on their values.
+		if k.value == val {
 			return nil
 		}
+		for i := range k.shadows {
+			if k.shadows[i].value == val {
+				return nil
+			}
+		}
 	}
 
 	shadow := newKey(k.s, k.name, val)
@@ -108,15 +110,24 @@ func (k *Key) Value() string {
 	return k.value
 }
 
-// ValueWithShadows returns raw values of key and its shadows if any.
+// ValueWithShadows returns raw values of key and its shadows if any. Shadow
+// keys with empty values are ignored from the returned list.
 func (k *Key) ValueWithShadows() []string {
 	if len(k.shadows) == 0 {
+		if k.value == "" {
+			return []string{}
+		}
 		return []string{k.value}
 	}
-	vals := make([]string, len(k.shadows)+1)
-	vals[0] = k.value
-	for i := range k.shadows {
-		vals[i+1] = k.shadows[i].value
+
+	vals := make([]string, 0, len(k.shadows)+1)
+	if k.value != "" {
+		vals = append(vals, k.value)
+	}
+	for _, s := range k.shadows {
+		if s.value != "" {
+			vals = append(vals, s.value)
+		}
 	}
 	return vals
 }
@@ -686,99 +697,124 @@ func (k *Key) StrictTimes(delim string) ([]time.Time, error) {
 // parseBools transforms strings to bools.
 func (k *Key) parseBools(strs []string, addInvalid, returnOnInvalid bool) ([]bool, error) {
 	vals := make([]bool, 0, len(strs))
-	for _, str := range strs {
+	parser := func(str string) (interface{}, error) {
 		val, err := parseBool(str)
-		if err != nil && returnOnInvalid {
-			return nil, err
-		}
-		if err == nil || addInvalid {
-			vals = append(vals, val)
+		return val, err
+	}
+	rawVals, err := k.doParse(strs, addInvalid, returnOnInvalid, parser)
+	if err == nil {
+		for _, val := range rawVals {
+			vals = append(vals, val.(bool))
 		}
 	}
-	return vals, nil
+	return vals, err
 }
 
 // parseFloat64s transforms strings to float64s.
 func (k *Key) parseFloat64s(strs []string, addInvalid, returnOnInvalid bool) ([]float64, error) {
 	vals := make([]float64, 0, len(strs))
-	for _, str := range strs {
+	parser := func(str string) (interface{}, error) {
 		val, err := strconv.ParseFloat(str, 64)
-		if err != nil && returnOnInvalid {
-			return nil, err
-		}
-		if err == nil || addInvalid {
-			vals = append(vals, val)
+		return val, err
+	}
+	rawVals, err := k.doParse(strs, addInvalid, returnOnInvalid, parser)
+	if err == nil {
+		for _, val := range rawVals {
+			vals = append(vals, val.(float64))
 		}
 	}
-	return vals, nil
+	return vals, err
 }
 
 // parseInts transforms strings to ints.
 func (k *Key) parseInts(strs []string, addInvalid, returnOnInvalid bool) ([]int, error) {
 	vals := make([]int, 0, len(strs))
-	for _, str := range strs {
-		valInt64, err := strconv.ParseInt(str, 0, 64)
-		val := int(valInt64)
-		if err != nil && returnOnInvalid {
-			return nil, err
-		}
-		if err == nil || addInvalid {
-			vals = append(vals, val)
+	parser := func(str string) (interface{}, error) {
+		val, err := strconv.ParseInt(str, 0, 64)
+		return val, err
+	}
+	rawVals, err := k.doParse(strs, addInvalid, returnOnInvalid, parser)
+	if err == nil {
+		for _, val := range rawVals {
+			vals = append(vals, int(val.(int64)))
 		}
 	}
-	return vals, nil
+	return vals, err
 }
 
 // parseInt64s transforms strings to int64s.
 func (k *Key) parseInt64s(strs []string, addInvalid, returnOnInvalid bool) ([]int64, error) {
 	vals := make([]int64, 0, len(strs))
-	for _, str := range strs {
+	parser := func(str string) (interface{}, error) {
 		val, err := strconv.ParseInt(str, 0, 64)
-		if err != nil && returnOnInvalid {
-			return nil, err
-		}
-		if err == nil || addInvalid {
-			vals = append(vals, val)
+		return val, err
+	}
+
+	rawVals, err := k.doParse(strs, addInvalid, returnOnInvalid, parser)
+	if err == nil {
+		for _, val := range rawVals {
+			vals = append(vals, val.(int64))
 		}
 	}
-	return vals, nil
+	return vals, err
 }
 
 // parseUints transforms strings to uints.
 func (k *Key) parseUints(strs []string, addInvalid, returnOnInvalid bool) ([]uint, error) {
 	vals := make([]uint, 0, len(strs))
-	for _, str := range strs {
-		val, err := strconv.ParseUint(str, 0, 0)
-		if err != nil && returnOnInvalid {
-			return nil, err
-		}
-		if err == nil || addInvalid {
-			vals = append(vals, uint(val))
+	parser := func(str string) (interface{}, error) {
+		val, err := strconv.ParseUint(str, 0, 64)
+		return val, err
+	}
+
+	rawVals, err := k.doParse(strs, addInvalid, returnOnInvalid, parser)
+	if err == nil {
+		for _, val := range rawVals {
+			vals = append(vals, uint(val.(uint64)))
 		}
 	}
-	return vals, nil
+	return vals, err
 }
 
 // parseUint64s transforms strings to uint64s.
 func (k *Key) parseUint64s(strs []string, addInvalid, returnOnInvalid bool) ([]uint64, error) {
 	vals := make([]uint64, 0, len(strs))
-	for _, str := range strs {
+	parser := func(str string) (interface{}, error) {
 		val, err := strconv.ParseUint(str, 0, 64)
-		if err != nil && returnOnInvalid {
-			return nil, err
-		}
-		if err == nil || addInvalid {
-			vals = append(vals, val)
+		return val, err
+	}
+	rawVals, err := k.doParse(strs, addInvalid, returnOnInvalid, parser)
+	if err == nil {
+		for _, val := range rawVals {
+			vals = append(vals, val.(uint64))
 		}
 	}
-	return vals, nil
+	return vals, err
 }
 
+type Parser func(str string) (interface{}, error)
+
 // parseTimesFormat transforms strings to times in given format.
 func (k *Key) parseTimesFormat(format string, strs []string, addInvalid, returnOnInvalid bool) ([]time.Time, error) {
 	vals := make([]time.Time, 0, len(strs))
-	for _, str := range strs {
+	parser := func(str string) (interface{}, error) {
 		val, err := time.Parse(format, str)
+		return val, err
+	}
+	rawVals, err := k.doParse(strs, addInvalid, returnOnInvalid, parser)
+	if err == nil {
+		for _, val := range rawVals {
+			vals = append(vals, val.(time.Time))
+		}
+	}
+	return vals, err
+}
+
+// doParse transforms strings to different types
+func (k *Key) doParse(strs []string, addInvalid, returnOnInvalid bool, parser Parser) ([]interface{}, error) {
+	vals := make([]interface{}, 0, len(strs))
+	for _, str := range strs {
+		val, err := parser(str)
 		if err != nil && returnOnInvalid {
 			return nil, err
 		}
diff --git a/key_test.go b/key_test.go
index 380f7fe..83095d7 100644
--- a/key_test.go
+++ b/key_test.go
@@ -12,508 +12,576 @@
 // License for the specific language governing permissions and limitations
 // under the License.
 
-package ini_test
+package ini
 
 import (
 	"bytes"
 	"fmt"
+	"runtime"
 	"strings"
 	"testing"
 	"time"
 
-	. "github.com/smartystreets/goconvey/convey"
-	"gopkg.in/ini.v1"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func TestKey_AddShadow(t *testing.T) {
-	Convey("Add shadow to a key", t, func() {
-		f, err := ini.ShadowLoad([]byte(`
+	t.Run("add shadow to a key", func(t *testing.T) {
+		f, err := ShadowLoad([]byte(`
 [notes]
 -: note1`))
-		So(err, ShouldBeNil)
-		So(f, ShouldNotBeNil)
+		require.NoError(t, err)
+		require.NotNil(t, f)
 
 		k, err := f.Section("").NewKey("NAME", "ini")
-		So(err, ShouldBeNil)
-		So(k, ShouldNotBeNil)
+		require.NoError(t, err)
+		require.NotNil(t, k)
 
-		So(k.AddShadow("ini.v1"), ShouldBeNil)
-		So(k.ValueWithShadows(), ShouldResemble, []string{"ini", "ini.v1"})
+		assert.NoError(t, k.AddShadow("ini.v1"))
+		assert.Equal(t, []string{"ini", "ini.v1"}, k.ValueWithShadows())
 
-		Convey("Add shadow to boolean key", func() {
+		t.Run("add shadow to boolean key", func(t *testing.T) {
 			k, err := f.Section("").NewBooleanKey("published")
-			So(err, ShouldBeNil)
-			So(k, ShouldNotBeNil)
-			So(k.AddShadow("beta"), ShouldNotBeNil)
+			require.NoError(t, err)
+			require.NotNil(t, k)
+			assert.Error(t, k.AddShadow("beta"))
 		})
 
-		Convey("Add shadow to auto-increment key", func() {
-			So(f.Section("notes").Key("#1").AddShadow("beta"), ShouldNotBeNil)
+		t.Run("add shadow to auto-increment key", func(t *testing.T) {
+			assert.Error(t, f.Section("notes").Key("#1").AddShadow("beta"))
+		})
+
+		t.Run("deduplicate an existing value", func(t *testing.T) {
+			k := f.Section("").Key("NAME")
+			assert.NoError(t, k.AddShadow("ini"))
+			assert.Equal(t, []string{"ini", "ini.v1"}, k.ValueWithShadows())
+		})
+
+		t.Run("ignore empty shadow values", func(t *testing.T) {
+			k := f.Section("").Key("empty")
+			assert.NoError(t, k.AddShadow(""))
+			assert.NoError(t, k.AddShadow("ini"))
+			assert.Equal(t, []string{"ini"}, k.ValueWithShadows())
 		})
 	})
 
-	Convey("Shadow is not allowed", t, func() {
-		f := ini.Empty()
-		So(f, ShouldNotBeNil)
+	t.Run("allow duplicate shadowed values", func(t *testing.T) {
+		f := Empty(LoadOptions{
+			AllowShadows:               true,
+			AllowDuplicateShadowValues: true,
+		})
+		require.NotNil(t, f)
 
 		k, err := f.Section("").NewKey("NAME", "ini")
-		So(err, ShouldBeNil)
-		So(k, ShouldNotBeNil)
+		require.NoError(t, err)
+		require.NotNil(t, k)
 
-		So(k.AddShadow("ini.v1"), ShouldNotBeNil)
+		assert.NoError(t, k.AddShadow("ini.v1"))
+		assert.NoError(t, k.AddShadow("ini"))
+		assert.NoError(t, k.AddShadow("ini"))
+		assert.Equal(t, []string{"ini", "ini.v1", "ini", "ini"}, k.ValueWithShadows())
+	})
+
+	t.Run("shadow is not allowed", func(t *testing.T) {
+		f := Empty()
+		require.NotNil(t, f)
+
+		k, err := f.Section("").NewKey("NAME", "ini")
+		require.NoError(t, err)
+		require.NotNil(t, k)
+
+		assert.Error(t, k.AddShadow("ini.v1"))
 	})
 }
 
 // Helpers for slice tests.
-func float64sEqual(values []float64, expected ...float64) {
-	So(values, ShouldHaveLength, len(expected))
+func float64sEqual(t *testing.T, values []float64, expected ...float64) {
+	t.Helper()
+
+	assert.Len(t, values, len(expected))
 	for i, v := range expected {
-		So(values[i], ShouldEqual, v)
+		assert.Equal(t, v, values[i])
 	}
 }
 
-func intsEqual(values []int, expected ...int) {
-	So(values, ShouldHaveLength, len(expected))
+func intsEqual(t *testing.T, values []int, expected ...int) {
+	t.Helper()
+
+	assert.Len(t, values, len(expected))
 	for i, v := range expected {
-		So(values[i], ShouldEqual, v)
+		assert.Equal(t, v, values[i])
 	}
 }
 
-func int64sEqual(values []int64, expected ...int64) {
-	So(values, ShouldHaveLength, len(expected))
+func int64sEqual(t *testing.T, values []int64, expected ...int64) {
+	t.Helper()
+
+	assert.Len(t, values, len(expected))
 	for i, v := range expected {
-		So(values[i], ShouldEqual, v)
+		assert.Equal(t, v, values[i])
 	}
 }
 
-func uintsEqual(values []uint, expected ...uint) {
-	So(values, ShouldHaveLength, len(expected))
+func uintsEqual(t *testing.T, values []uint, expected ...uint) {
+	t.Helper()
+
+	assert.Len(t, values, len(expected))
 	for i, v := range expected {
-		So(values[i], ShouldEqual, v)
+		assert.Equal(t, v, values[i])
 	}
 }
 
-func uint64sEqual(values []uint64, expected ...uint64) {
-	So(values, ShouldHaveLength, len(expected))
+func uint64sEqual(t *testing.T, values []uint64, expected ...uint64) {
+	t.Helper()
+
+	assert.Len(t, values, len(expected))
 	for i, v := range expected {
-		So(values[i], ShouldEqual, v)
+		assert.Equal(t, v, values[i])
 	}
 }
 
-func boolsEqual(values []bool, expected ...bool) {
-	So(values, ShouldHaveLength, len(expected))
+func boolsEqual(t *testing.T, values []bool, expected ...bool) {
+	t.Helper()
+
+	assert.Len(t, values, len(expected))
 	for i, v := range expected {
-		So(values[i], ShouldEqual, v)
+		assert.Equal(t, v, values[i])
 	}
 }
 
-func timesEqual(values []time.Time, expected ...time.Time) {
-	So(values, ShouldHaveLength, len(expected))
+func timesEqual(t *testing.T, values []time.Time, expected ...time.Time) {
+	t.Helper()
+
+	assert.Len(t, values, len(expected))
 	for i, v := range expected {
-		So(values[i].String(), ShouldEqual, v.String())
+		assert.Equal(t, v.String(), values[i].String())
 	}
 }
 
 func TestKey_Helpers(t *testing.T) {
-	Convey("Getting and setting values", t, func() {
-		f, err := ini.Load(fullConf)
-		So(err, ShouldBeNil)
-		So(f, ShouldNotBeNil)
+	t.Run("getting and setting values", func(t *testing.T) {
+		f, err := Load(fullConf)
+		require.NoError(t, err)
+		require.NotNil(t, f)
 
-		Convey("Get string representation", func() {
+		t.Run("get string representation", func(t *testing.T) {
 			sec := f.Section("")
-			So(sec, ShouldNotBeNil)
-			So(sec.Key("NAME").Value(), ShouldEqual, "ini")
-			So(sec.Key("NAME").String(), ShouldEqual, "ini")
-			So(sec.Key("NAME").Validate(func(in string) string {
+			require.NotNil(t, sec)
+			assert.Equal(t, "ini", sec.Key("NAME").Value())
+			assert.Equal(t, "ini", sec.Key("NAME").String())
+			assert.Equal(t, "ini", sec.Key("NAME").Validate(func(in string) string {
 				return in
-			}), ShouldEqual, "ini")
-			So(sec.Key("NAME").Comment, ShouldEqual, "; Package name")
-			So(sec.Key("IMPORT_PATH").String(), ShouldEqual, "gopkg.in/ini.v1")
+			}))
+			assert.Equal(t, "; Package name", sec.Key("NAME").Comment)
+			assert.Equal(t, "gopkg.in/ini.v1", sec.Key("IMPORT_PATH").String())
 
-			Convey("With ValueMapper", func() {
+			t.Run("with ValueMapper", func(t *testing.T) {
 				f.ValueMapper = func(in string) string {
 					if in == "gopkg.in/%(NAME)s.%(VERSION)s" {
 						return "github.com/go-ini/ini"
 					}
 					return in
 				}
-				So(sec.Key("IMPORT_PATH").String(), ShouldEqual, "github.com/go-ini/ini")
+				assert.Equal(t, "github.com/go-ini/ini", sec.Key("IMPORT_PATH").String())
 			})
 		})
 
-		Convey("Get values in non-default section", func() {
+		t.Run("get values in non-default section", func(t *testing.T) {
 			sec := f.Section("author")
-			So(sec, ShouldNotBeNil)
-			So(sec.Key("NAME").String(), ShouldEqual, "Unknwon")
-			So(sec.Key("GITHUB").String(), ShouldEqual, "https://github.com/Unknwon")
+			require.NotNil(t, sec)
+			assert.Equal(t, "Unknwon", sec.Key("NAME").String())
+			assert.Equal(t, "https://github.com/Unknwon", sec.Key("GITHUB").String())
 
 			sec = f.Section("package")
-			So(sec, ShouldNotBeNil)
-			So(sec.Key("CLONE_URL").String(), ShouldEqual, "https://gopkg.in/ini.v1")
+			require.NotNil(t, sec)
+			assert.Equal(t, "https://gopkg.in/ini.v1", sec.Key("CLONE_URL").String())
 		})
 
-		Convey("Get auto-increment key names", func() {
+		t.Run("get auto-increment key names", func(t *testing.T) {
 			keys := f.Section("features").Keys()
 			for i, k := range keys {
-				So(k.Name(), ShouldEqual, fmt.Sprintf("#%d", i+1))
+				assert.Equal(t, fmt.Sprintf("#%d", i+1), k.Name())
 			}
 		})
 
-		Convey("Get parent-keys that are available to the child section", func() {
+		t.Run("get parent-keys that are available to the child section", func(t *testing.T) {
 			parentKeys := f.Section("package.sub").ParentKeys()
 			for _, k := range parentKeys {
-				So(k.Name(), ShouldEqual, "CLONE_URL")
+				assert.Equal(t, "CLONE_URL", k.Name())
 			}
 		})
 
-		Convey("Get overwrite value", func() {
-			So(f.Section("author").Key("E-MAIL").String(), ShouldEqual, "u@gogs.io")
+		t.Run("get overwrite value", func(t *testing.T) {
+			assert.Equal(t, "u@gogs.io", f.Section("author").Key("E-MAIL").String())
 		})
 
-		Convey("Get sections", func() {
+		t.Run("get sections", func(t *testing.T) {
 			sections := f.Sections()
-			for i, name := range []string{ini.DefaultSection, "author", "package", "package.sub", "features", "types", "array", "note", "comments", "string escapes", "advance"} {
-				So(sections[i].Name(), ShouldEqual, name)
+			for i, name := range []string{DefaultSection, "author", "package", "package.sub", "features", "types", "array", "note", "comments", "string escapes", "advance"} {
+				assert.Equal(t, name, sections[i].Name())
 			}
 		})
 
-		Convey("Get parent section value", func() {
-			So(f.Section("package.sub").Key("CLONE_URL").String(), ShouldEqual, "https://gopkg.in/ini.v1")
-			So(f.Section("package.fake.sub").Key("CLONE_URL").String(), ShouldEqual, "https://gopkg.in/ini.v1")
+		t.Run("get parent section value", func(t *testing.T) {
+			assert.Equal(t, "https://gopkg.in/ini.v1", f.Section("package.sub").Key("CLONE_URL").String())
+			assert.Equal(t, "https://gopkg.in/ini.v1", f.Section("package.fake.sub").Key("CLONE_URL").String())
 		})
 
-		Convey("Get multiple line value", func() {
-			So(f.Section("author").Key("BIO").String(), ShouldEqual, "Gopher.\nCoding addict.\nGood man.\n")
+		t.Run("get multiple line value", func(t *testing.T) {
+			if runtime.GOOS == "windows" {
+				t.Skip("Skipping testing on Windows")
+			}
+
+			assert.Equal(t, "Gopher.\nCoding addict.\nGood man.\n", f.Section("author").Key("BIO").String())
 		})
 
-		Convey("Get values with type", func() {
+		t.Run("get values with type", func(t *testing.T) {
 			sec := f.Section("types")
 			v1, err := sec.Key("BOOL").Bool()
-			So(err, ShouldBeNil)
-			So(v1, ShouldBeTrue)
+			require.NoError(t, err)
+			assert.True(t, v1)
 
 			v1, err = sec.Key("BOOL_FALSE").Bool()
-			So(err, ShouldBeNil)
-			So(v1, ShouldBeFalse)
+			require.NoError(t, err)
+			assert.False(t, v1)
 
 			v2, err := sec.Key("FLOAT64").Float64()
-			So(err, ShouldBeNil)
-			So(v2, ShouldEqual, 1.25)
+			require.NoError(t, err)
+			assert.Equal(t, 1.25, v2)
 
 			v3, err := sec.Key("INT").Int()
-			So(err, ShouldBeNil)
-			So(v3, ShouldEqual, 10)
+			require.NoError(t, err)
+			assert.Equal(t, 10, v3)
 
 			v4, err := sec.Key("INT").Int64()
-			So(err, ShouldBeNil)
-			So(v4, ShouldEqual, 10)
+			require.NoError(t, err)
+			assert.Equal(t, int64(10), v4)
 
 			v5, err := sec.Key("UINT").Uint()
-			So(err, ShouldBeNil)
-			So(v5, ShouldEqual, 3)
+			require.NoError(t, err)
+			assert.Equal(t, uint(3), v5)
 
 			v6, err := sec.Key("UINT").Uint64()
-			So(err, ShouldBeNil)
-			So(v6, ShouldEqual, 3)
+			require.NoError(t, err)
+			assert.Equal(t, uint64(3), v6)
 
-			t, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z")
-			So(err, ShouldBeNil)
+			ti, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z")
+			require.NoError(t, err)
 			v7, err := sec.Key("TIME").Time()
-			So(err, ShouldBeNil)
-			So(v7.String(), ShouldEqual, t.String())
+			require.NoError(t, err)
+			assert.Equal(t, ti.String(), v7.String())
 
 			v8, err := sec.Key("HEX_NUMBER").Int()
-			So(err, ShouldBeNil)
-			So(v8, ShouldEqual, 0x3000)
-
-			Convey("Must get values with type", func() {
-				So(sec.Key("STRING").MustString("404"), ShouldEqual, "str")
-				So(sec.Key("BOOL").MustBool(), ShouldBeTrue)
-				So(sec.Key("FLOAT64").MustFloat64(), ShouldEqual, 1.25)
-				So(sec.Key("INT").MustInt(), ShouldEqual, 10)
-				So(sec.Key("INT").MustInt64(), ShouldEqual, 10)
-				So(sec.Key("UINT").MustUint(), ShouldEqual, 3)
-				So(sec.Key("UINT").MustUint64(), ShouldEqual, 3)
-				So(sec.Key("TIME").MustTime().String(), ShouldEqual, t.String())
-				So(sec.Key("HEX_NUMBER").MustInt(), ShouldEqual, 0x3000)
+			require.NoError(t, err)
+			assert.Equal(t, 0x3000, v8)
+
+			t.Run("must get values with type", func(t *testing.T) {
+				assert.Equal(t, "str", sec.Key("STRING").MustString("404"))
+				assert.True(t, sec.Key("BOOL").MustBool())
+				assert.Equal(t, float64(1.25), sec.Key("FLOAT64").MustFloat64())
+				assert.Equal(t, int(10), sec.Key("INT").MustInt())
+				assert.Equal(t, int64(10), sec.Key("INT").MustInt64())
+				assert.Equal(t, uint(3), sec.Key("UINT").MustUint())
+				assert.Equal(t, uint64(3), sec.Key("UINT").MustUint64())
+				assert.Equal(t, ti.String(), sec.Key("TIME").MustTime().String())
+				assert.Equal(t, 0x3000, sec.Key("HEX_NUMBER").MustInt())
 
 				dur, err := time.ParseDuration("2h45m")
-				So(err, ShouldBeNil)
-				So(sec.Key("DURATION").MustDuration().Seconds(), ShouldEqual, dur.Seconds())
-
-				Convey("Must get values with default value", func() {
-					So(sec.Key("STRING_404").MustString("404"), ShouldEqual, "404")
-					So(sec.Key("BOOL_404").MustBool(true), ShouldBeTrue)
-					So(sec.Key("FLOAT64_404").MustFloat64(2.5), ShouldEqual, 2.5)
-					So(sec.Key("INT_404").MustInt(15), ShouldEqual, 15)
-					So(sec.Key("INT64_404").MustInt64(15), ShouldEqual, 15)
-					So(sec.Key("UINT_404").MustUint(6), ShouldEqual, 6)
-					So(sec.Key("UINT64_404").MustUint64(6), ShouldEqual, 6)
-					So(sec.Key("HEX_NUMBER_404").MustInt(0x3001), ShouldEqual, 0x3001)
-
-					t, err := time.Parse(time.RFC3339, "2014-01-01T20:17:05Z")
-					So(err, ShouldBeNil)
-					So(sec.Key("TIME_404").MustTime(t).String(), ShouldEqual, t.String())
-
-					So(sec.Key("DURATION_404").MustDuration(dur).Seconds(), ShouldEqual, dur.Seconds())
-
-					Convey("Must should set default as key value", func() {
-						So(sec.Key("STRING_404").String(), ShouldEqual, "404")
-						So(sec.Key("BOOL_404").String(), ShouldEqual, "true")
-						So(sec.Key("FLOAT64_404").String(), ShouldEqual, "2.5")
-						So(sec.Key("INT_404").String(), ShouldEqual, "15")
-						So(sec.Key("INT64_404").String(), ShouldEqual, "15")
-						So(sec.Key("UINT_404").String(), ShouldEqual, "6")
-						So(sec.Key("UINT64_404").String(), ShouldEqual, "6")
-						So(sec.Key("TIME_404").String(), ShouldEqual, "2014-01-01T20:17:05Z")
-						So(sec.Key("DURATION_404").String(), ShouldEqual, "2h45m0s")
-						So(sec.Key("HEX_NUMBER_404").String(), ShouldEqual, "12289")
+				require.NoError(t, err)
+				assert.Equal(t, dur.Seconds(), sec.Key("DURATION").MustDuration().Seconds())
+
+				t.Run("must get values with default value", func(t *testing.T) {
+					assert.Equal(t, "404", sec.Key("STRING_404").MustString("404"))
+					assert.True(t, sec.Key("BOOL_404").MustBool(true))
+					assert.Equal(t, float64(2.5), sec.Key("FLOAT64_404").MustFloat64(2.5))
+					assert.Equal(t, int(15), sec.Key("INT_404").MustInt(15))
+					assert.Equal(t, int64(15), sec.Key("INT64_404").MustInt64(15))
+					assert.Equal(t, uint(6), sec.Key("UINT_404").MustUint(6))
+					assert.Equal(t, uint64(6), sec.Key("UINT64_404").MustUint64(6))
+					assert.Equal(t, 0x3001, sec.Key("HEX_NUMBER_404").MustInt(0x3001))
+
+					ti, err := time.Parse(time.RFC3339, "2014-01-01T20:17:05Z")
+					require.NoError(t, err)
+					assert.Equal(t, ti.String(), sec.Key("TIME_404").MustTime(ti).String())
+
+					assert.Equal(t, dur.Seconds(), sec.Key("DURATION_404").MustDuration(dur).Seconds())
+
+					t.Run("must should set default as key value", func(t *testing.T) {
+						assert.Equal(t, "404", sec.Key("STRING_404").String())
+						assert.Equal(t, "true", sec.Key("BOOL_404").String())
+						assert.Equal(t, "2.5", sec.Key("FLOAT64_404").String())
+						assert.Equal(t, "15", sec.Key("INT_404").String())
+						assert.Equal(t, "15", sec.Key("INT64_404").String())
+						assert.Equal(t, "6", sec.Key("UINT_404").String())
+						assert.Equal(t, "6", sec.Key("UINT64_404").String())
+						assert.Equal(t, "2014-01-01T20:17:05Z", sec.Key("TIME_404").String())
+						assert.Equal(t, "2h45m0s", sec.Key("DURATION_404").String())
+						assert.Equal(t, "12289", sec.Key("HEX_NUMBER_404").String())
 					})
 				})
 			})
 		})
 
-		Convey("Get value with candidates", func() {
+		t.Run("get value with candidates", func(t *testing.T) {
 			sec := f.Section("types")
-			So(sec.Key("STRING").In("", []string{"str", "arr", "types"}), ShouldEqual, "str")
-			So(sec.Key("FLOAT64").InFloat64(0, []float64{1.25, 2.5, 3.75}), ShouldEqual, 1.25)
-			So(sec.Key("INT").InInt(0, []int{10, 20, 30}), ShouldEqual, 10)
-			So(sec.Key("INT").InInt64(0, []int64{10, 20, 30}), ShouldEqual, 10)
-			So(sec.Key("UINT").InUint(0, []uint{3, 6, 9}), ShouldEqual, 3)
-			So(sec.Key("UINT").InUint64(0, []uint64{3, 6, 9}), ShouldEqual, 3)
+			assert.Equal(t, "str", sec.Key("STRING").In("", []string{"str", "arr", "types"}))
+			assert.Equal(t, float64(1.25), sec.Key("FLOAT64").InFloat64(0, []float64{1.25, 2.5, 3.75}))
+			assert.Equal(t, int(10), sec.Key("INT").InInt(0, []int{10, 20, 30}))
+			assert.Equal(t, int64(10), sec.Key("INT").InInt64(0, []int64{10, 20, 30}))
+			assert.Equal(t, uint(3), sec.Key("UINT").InUint(0, []uint{3, 6, 9}))
+			assert.Equal(t, uint64(3), sec.Key("UINT").InUint64(0, []uint64{3, 6, 9}))
 
 			zt, err := time.Parse(time.RFC3339, "0001-01-01T01:00:00Z")
-			So(err, ShouldBeNil)
-			t, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z")
-			So(err, ShouldBeNil)
-			So(sec.Key("TIME").InTime(zt, []time.Time{t, time.Now(), time.Now().Add(1 * time.Second)}).String(), ShouldEqual, t.String())
-
-			Convey("Get value with candidates and default value", func() {
-				So(sec.Key("STRING_404").In("str", []string{"str", "arr", "types"}), ShouldEqual, "str")
-				So(sec.Key("FLOAT64_404").InFloat64(1.25, []float64{1.25, 2.5, 3.75}), ShouldEqual, 1.25)
-				So(sec.Key("INT_404").InInt(10, []int{10, 20, 30}), ShouldEqual, 10)
-				So(sec.Key("INT64_404").InInt64(10, []int64{10, 20, 30}), ShouldEqual, 10)
-				So(sec.Key("UINT_404").InUint(3, []uint{3, 6, 9}), ShouldEqual, 3)
-				So(sec.Key("UINT_404").InUint64(3, []uint64{3, 6, 9}), ShouldEqual, 3)
-				So(sec.Key("TIME_404").InTime(t, []time.Time{time.Now(), time.Now(), time.Now().Add(1 * time.Second)}).String(), ShouldEqual, t.String())
+			require.NoError(t, err)
+			ti, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z")
+			require.NoError(t, err)
+			assert.Equal(t, ti.String(), sec.Key("TIME").InTime(zt, []time.Time{ti, time.Now(), time.Now().Add(1 * time.Second)}).String())
+
+			t.Run("get value with candidates and default value", func(t *testing.T) {
+				assert.Equal(t, "str", sec.Key("STRING_404_2").In("str", []string{"str", "arr", "types"}))
+				assert.Equal(t, float64(1.25), sec.Key("FLOAT64_404_2").InFloat64(1.25, []float64{1.25, 2.5, 3.75}))
+				assert.Equal(t, int(10), sec.Key("INT_404_2").InInt(10, []int{10, 20, 30}))
+				assert.Equal(t, int64(10), sec.Key("INT64_404_2").InInt64(10, []int64{10, 20, 30}))
+				assert.Equal(t, uint(3), sec.Key("UINT_404_2").InUint(3, []uint{3, 6, 9}))
+				assert.Equal(t, uint64(3), sec.Key("UINT_404_2").InUint64(3, []uint64{3, 6, 9}))
+				assert.Equal(t, ti.String(), sec.Key("TIME_404_2").InTime(ti, []time.Time{time.Now(), time.Now(), time.Now().Add(1 * time.Second)}).String())
 			})
 		})
 
-		Convey("Get values in range", func() {
+		t.Run("get values in range", func(t *testing.T) {
 			sec := f.Section("types")
-			So(sec.Key("FLOAT64").RangeFloat64(0, 1, 2), ShouldEqual, 1.25)
-			So(sec.Key("INT").RangeInt(0, 10, 20), ShouldEqual, 10)
-			So(sec.Key("INT").RangeInt64(0, 10, 20), ShouldEqual, 10)
+			assert.Equal(t, float64(1.25), sec.Key("FLOAT64").RangeFloat64(0, 1, 2))
+			assert.Equal(t, int(10), sec.Key("INT").RangeInt(0, 10, 20))
+			assert.Equal(t, int64(10), sec.Key("INT").RangeInt64(0, 10, 20))
 
 			minT, err := time.Parse(time.RFC3339, "0001-01-01T01:00:00Z")
-			So(err, ShouldBeNil)
+			require.NoError(t, err)
 			midT, err := time.Parse(time.RFC3339, "2013-01-01T01:00:00Z")
-			So(err, ShouldBeNil)
+			require.NoError(t, err)
 			maxT, err := time.Parse(time.RFC3339, "9999-01-01T01:00:00Z")
-			So(err, ShouldBeNil)
-			t, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z")
-			So(err, ShouldBeNil)
-			So(sec.Key("TIME").RangeTime(t, minT, maxT).String(), ShouldEqual, t.String())
-
-			Convey("Get value in range with default value", func() {
-				So(sec.Key("FLOAT64").RangeFloat64(5, 0, 1), ShouldEqual, 5)
-				So(sec.Key("INT").RangeInt(7, 0, 5), ShouldEqual, 7)
-				So(sec.Key("INT").RangeInt64(7, 0, 5), ShouldEqual, 7)
-				So(sec.Key("TIME").RangeTime(t, minT, midT).String(), ShouldEqual, t.String())
+			require.NoError(t, err)
+			ti, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z")
+			require.NoError(t, err)
+			assert.Equal(t, ti.String(), sec.Key("TIME").RangeTime(ti, minT, maxT).String())
+
+			t.Run("get value in range with default value", func(t *testing.T) {
+				assert.Equal(t, float64(5), sec.Key("FLOAT64").RangeFloat64(5, 0, 1))
+				assert.Equal(t, 7, sec.Key("INT").RangeInt(7, 0, 5))
+				assert.Equal(t, int64(7), sec.Key("INT").RangeInt64(7, 0, 5))
+				assert.Equal(t, ti.String(), sec.Key("TIME").RangeTime(ti, minT, midT).String())
 			})
 		})
 
-		Convey("Get values into slice", func() {
+		t.Run("get values into slice", func(t *testing.T) {
 			sec := f.Section("array")
-			So(strings.Join(sec.Key("STRINGS").Strings(","), ","), ShouldEqual, "en,zh,de")
-			So(len(sec.Key("STRINGS_404").Strings(",")), ShouldEqual, 0)
+			assert.Equal(t, "en,zh,de", strings.Join(sec.Key("STRINGS").Strings(","), ","))
+			assert.Equal(t, 0, len(sec.Key("STRINGS_404").Strings(",")))
 
 			vals1 := sec.Key("FLOAT64S").Float64s(",")
-			float64sEqual(vals1, 1.1, 2.2, 3.3)
+			float64sEqual(t, vals1, 1.1, 2.2, 3.3)
 
 			vals2 := sec.Key("INTS").Ints(",")
-			intsEqual(vals2, 1, 2, 3)
+			intsEqual(t, vals2, 1, 2, 3)
 
 			vals3 := sec.Key("INTS").Int64s(",")
-			int64sEqual(vals3, 1, 2, 3)
+			int64sEqual(t, vals3, 1, 2, 3)
 
 			vals4 := sec.Key("UINTS").Uints(",")
-			uintsEqual(vals4, 1, 2, 3)
+			uintsEqual(t, vals4, 1, 2, 3)
 
 			vals5 := sec.Key("UINTS").Uint64s(",")
-			uint64sEqual(vals5, 1, 2, 3)
+			uint64sEqual(t, vals5, 1, 2, 3)
 
 			vals6 := sec.Key("BOOLS").Bools(",")
-			boolsEqual(vals6, true, false, false)
+			boolsEqual(t, vals6, true, false, false)
 
-			t, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z")
-			So(err, ShouldBeNil)
+			ti, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z")
+			require.NoError(t, err)
 			vals7 := sec.Key("TIMES").Times(",")
-			timesEqual(vals7, t, t, t)
+			timesEqual(t, vals7, ti, ti, ti)
 		})
 
-		Convey("Test string slice escapes", func() {
+		t.Run("test string slice escapes", func(t *testing.T) {
 			sec := f.Section("string escapes")
-			So(sec.Key("key1").Strings(","), ShouldResemble, []string{"value1", "value2", "value3"})
-			So(sec.Key("key2").Strings(","), ShouldResemble, []string{"value1, value2"})
-			So(sec.Key("key3").Strings(","), ShouldResemble, []string{`val\ue1`, "value2"})
-			So(sec.Key("key4").Strings(","), ShouldResemble, []string{`value1\`, `value\\2`})
-			So(sec.Key("key5").Strings(",,"), ShouldResemble, []string{"value1,, value2"})
-			So(sec.Key("key6").Strings(" "), ShouldResemble, []string{"aaa", "bbb and space", "ccc"})
+			assert.Equal(t, []string{"value1", "value2", "value3"}, sec.Key("key1").Strings(","))
+			assert.Equal(t, []string{"value1, value2"}, sec.Key("key2").Strings(","))
+			assert.Equal(t, []string{`val\ue1`, "value2"}, sec.Key("key3").Strings(","))
+			assert.Equal(t, []string{`value1\`, `value\\2`}, sec.Key("key4").Strings(","))
+			assert.Equal(t, []string{"value1,, value2"}, sec.Key("key5").Strings(",,"))
+			assert.Equal(t, []string{"aaa", "bbb and space", "ccc"}, sec.Key("key6").Strings(" "))
 		})
 
-		Convey("Get valid values into slice", func() {
+		t.Run("get valid values into slice", func(t *testing.T) {
 			sec := f.Section("array")
 			vals1 := sec.Key("FLOAT64S").ValidFloat64s(",")
-			float64sEqual(vals1, 1.1, 2.2, 3.3)
+			float64sEqual(t, vals1, 1.1, 2.2, 3.3)
 
 			vals2 := sec.Key("INTS").ValidInts(",")
-			intsEqual(vals2, 1, 2, 3)
+			intsEqual(t, vals2, 1, 2, 3)
 
 			vals3 := sec.Key("INTS").ValidInt64s(",")
-			int64sEqual(vals3, 1, 2, 3)
+			int64sEqual(t, vals3, 1, 2, 3)
 
 			vals4 := sec.Key("UINTS").ValidUints(",")
-			uintsEqual(vals4, 1, 2, 3)
+			uintsEqual(t, vals4, 1, 2, 3)
 
 			vals5 := sec.Key("UINTS").ValidUint64s(",")
-			uint64sEqual(vals5, 1, 2, 3)
+			uint64sEqual(t, vals5, 1, 2, 3)
 
 			vals6 := sec.Key("BOOLS").ValidBools(",")
-			boolsEqual(vals6, true, false, false)
+			boolsEqual(t, vals6, true, false, false)
 
-			t, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z")
-			So(err, ShouldBeNil)
+			ti, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z")
+			require.NoError(t, err)
 			vals7 := sec.Key("TIMES").ValidTimes(",")
-			timesEqual(vals7, t, t, t)
+			timesEqual(t, vals7, ti, ti, ti)
 		})
 
-		Convey("Get values one type into slice of another type", func() {
+		t.Run("get values one type into slice of another type", func(t *testing.T) {
 			sec := f.Section("array")
 			vals1 := sec.Key("STRINGS").ValidFloat64s(",")
-			So(vals1, ShouldBeEmpty)
+			assert.Empty(t, vals1)
 
 			vals2 := sec.Key("STRINGS").ValidInts(",")
-			So(vals2, ShouldBeEmpty)
+			assert.Empty(t, vals2)
 
 			vals3 := sec.Key("STRINGS").ValidInt64s(",")
-			So(vals3, ShouldBeEmpty)
+			assert.Empty(t, vals3)
 
 			vals4 := sec.Key("STRINGS").ValidUints(",")
-			So(vals4, ShouldBeEmpty)
+			assert.Empty(t, vals4)
 
 			vals5 := sec.Key("STRINGS").ValidUint64s(",")
-			So(vals5, ShouldBeEmpty)
+			assert.Empty(t, vals5)
 
 			vals6 := sec.Key("STRINGS").ValidBools(",")
-			So(vals6, ShouldBeEmpty)
+			assert.Empty(t, vals6)
 
 			vals7 := sec.Key("STRINGS").ValidTimes(",")
-			So(vals7, ShouldBeEmpty)
+			assert.Empty(t, vals7)
 		})
 
-		Convey("Get valid values into slice without errors", func() {
+		t.Run("get valid values into slice without errors", func(t *testing.T) {
 			sec := f.Section("array")
 			vals1, err := sec.Key("FLOAT64S").StrictFloat64s(",")
-			So(err, ShouldBeNil)
-			float64sEqual(vals1, 1.1, 2.2, 3.3)
+			require.NoError(t, err)
+			float64sEqual(t, vals1, 1.1, 2.2, 3.3)
 
 			vals2, err := sec.Key("INTS").StrictInts(",")
-			So(err, ShouldBeNil)
-			intsEqual(vals2, 1, 2, 3)
+			require.NoError(t, err)
+			intsEqual(t, vals2, 1, 2, 3)
 
 			vals3, err := sec.Key("INTS").StrictInt64s(",")
-			So(err, ShouldBeNil)
-			int64sEqual(vals3, 1, 2, 3)
+			require.NoError(t, err)
+			int64sEqual(t, vals3, 1, 2, 3)
 
 			vals4, err := sec.Key("UINTS").StrictUints(",")
-			So(err, ShouldBeNil)
-			uintsEqual(vals4, 1, 2, 3)
+			require.NoError(t, err)
+			uintsEqual(t, vals4, 1, 2, 3)
 
 			vals5, err := sec.Key("UINTS").StrictUint64s(",")
-			So(err, ShouldBeNil)
-			uint64sEqual(vals5, 1, 2, 3)
+			require.NoError(t, err)
+			uint64sEqual(t, vals5, 1, 2, 3)
 
 			vals6, err := sec.Key("BOOLS").StrictBools(",")
-			So(err, ShouldBeNil)
-			boolsEqual(vals6, true, false, false)
+			require.NoError(t, err)
+			boolsEqual(t, vals6, true, false, false)
 
-			t, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z")
-			So(err, ShouldBeNil)
+			ti, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z")
+			require.NoError(t, err)
 			vals7, err := sec.Key("TIMES").StrictTimes(",")
-			So(err, ShouldBeNil)
-			timesEqual(vals7, t, t, t)
+			require.NoError(t, err)
+			timesEqual(t, vals7, ti, ti, ti)
 		})
 
-		Convey("Get invalid values into slice", func() {
+		t.Run("get invalid values into slice", func(t *testing.T) {
 			sec := f.Section("array")
 			vals1, err := sec.Key("STRINGS").StrictFloat64s(",")
-			So(vals1, ShouldBeEmpty)
-			So(err, ShouldNotBeNil)
+			assert.Empty(t, vals1)
+			assert.Error(t, err)
 
 			vals2, err := sec.Key("STRINGS").StrictInts(",")
-			So(vals2, ShouldBeEmpty)
-			So(err, ShouldNotBeNil)
+			assert.Empty(t, vals2)
+			assert.Error(t, err)
 
 			vals3, err := sec.Key("STRINGS").StrictInt64s(",")
-			So(vals3, ShouldBeEmpty)
-			So(err, ShouldNotBeNil)
+			assert.Empty(t, vals3)
+			assert.Error(t, err)
 
 			vals4, err := sec.Key("STRINGS").StrictUints(",")
-			So(vals4, ShouldBeEmpty)
-			So(err, ShouldNotBeNil)
+			assert.Empty(t, vals4)
+			assert.Error(t, err)
 
 			vals5, err := sec.Key("STRINGS").StrictUint64s(",")
-			So(vals5, ShouldBeEmpty)
-			So(err, ShouldNotBeNil)
+			assert.Empty(t, vals5)
+			assert.Error(t, err)
 
 			vals6, err := sec.Key("STRINGS").StrictBools(",")
-			So(vals6, ShouldBeEmpty)
-			So(err, ShouldNotBeNil)
+			assert.Empty(t, vals6)
+			assert.Error(t, err)
 
 			vals7, err := sec.Key("STRINGS").StrictTimes(",")
-			So(vals7, ShouldBeEmpty)
-			So(err, ShouldNotBeNil)
+			assert.Empty(t, vals7)
+			assert.Error(t, err)
 		})
 	})
 }
 
+func TestKey_ValueWithShadows(t *testing.T) {
+	t.Run("", func(t *testing.T) {
+		f, err := ShadowLoad([]byte(`
+keyName = value1
+keyName = value2
+`))
+		require.NoError(t, err)
+		require.NotNil(t, f)
+
+		k := f.Section("").Key("FakeKey")
+		require.NotNil(t, k)
+		assert.Equal(t, []string{}, k.ValueWithShadows())
+
+		k = f.Section("").Key("keyName")
+		require.NotNil(t, k)
+		assert.Equal(t, []string{"value1", "value2"}, k.ValueWithShadows())
+	})
+}
+
 func TestKey_StringsWithShadows(t *testing.T) {
-	Convey("Get strings of shadows of a key", t, func() {
-		f, err := ini.ShadowLoad([]byte(""))
-		So(err, ShouldBeNil)
-		So(f, ShouldNotBeNil)
+	t.Run("get strings of shadows of a key", func(t *testing.T) {
+		f, err := ShadowLoad([]byte(""))
+		require.NoError(t, err)
+		require.NotNil(t, f)
 
 		k, err := f.Section("").NewKey("NUMS", "1,2")
-		So(err, ShouldBeNil)
-		So(k, ShouldNotBeNil)
+		require.NoError(t, err)
+		require.NotNil(t, k)
 		k, err = f.Section("").NewKey("NUMS", "4,5,6")
-		So(err, ShouldBeNil)
-		So(k, ShouldNotBeNil)
+		require.NoError(t, err)
+		require.NotNil(t, k)
 
-		So(k.StringsWithShadows(","), ShouldResemble, []string{"1", "2", "4", "5", "6"})
+		assert.Equal(t, []string{"1", "2", "4", "5", "6"}, k.StringsWithShadows(","))
 	})
 }
 
 func TestKey_SetValue(t *testing.T) {
-	Convey("Set value of key", t, func() {
-		f := ini.Empty()
-		So(f, ShouldNotBeNil)
+	t.Run("set value of key", func(t *testing.T) {
+		f := Empty()
+		require.NotNil(t, f)
 
 		k, err := f.Section("").NewKey("NAME", "ini")
-		So(err, ShouldBeNil)
-		So(k, ShouldNotBeNil)
-		So(k.Value(), ShouldEqual, "ini")
+		require.NoError(t, err)
+		require.NotNil(t, k)
+		assert.Equal(t, "ini", k.Value())
 
 		k.SetValue("ini.v1")
-		So(k.Value(), ShouldEqual, "ini.v1")
+		assert.Equal(t, "ini.v1", k.Value())
 	})
 }
 
 func TestKey_NestedValues(t *testing.T) {
-	Convey("Read and write nested values", t, func() {
-		f, err := ini.LoadSources(ini.LoadOptions{
+	t.Run("read and write nested values", func(t *testing.T) {
+		f, err := LoadSources(LoadOptions{
 			AllowNestedValues: true,
 		}, []byte(`
 aws_access_key_id = foo
@@ -522,48 +590,50 @@ region = us-west-2
 s3 =
   max_concurrent_requests=10
   max_queue_size=1000`))
-		So(err, ShouldBeNil)
-		So(f, ShouldNotBeNil)
+		require.NoError(t, err)
+		require.NotNil(t, f)
 
-		So(f.Section("").Key("s3").NestedValues(), ShouldResemble, []string{"max_concurrent_requests=10", "max_queue_size=1000"})
+		assert.Equal(t, []string{"max_concurrent_requests=10", "max_queue_size=1000"}, f.Section("").Key("s3").NestedValues())
 
 		var buf bytes.Buffer
 		_, err = f.WriteTo(&buf)
-		So(err, ShouldBeNil)
-		So(buf.String(), ShouldEqual, `aws_access_key_id     = foo
+		require.NoError(t, err)
+		assert.Equal(t, `aws_access_key_id     = foo
 aws_secret_access_key = bar
 region                = us-west-2
 s3                    = 
   max_concurrent_requests=10
   max_queue_size=1000
 
-`)
+`,
+			buf.String(),
+		)
 	})
 }
 
 func TestRecursiveValues(t *testing.T) {
-	Convey("Recursive values should not reflect on same key", t, func() {
-		f, err := ini.Load([]byte(`
+	t.Run("recursive values should not reflect on same key", func(t *testing.T) {
+		f, err := Load([]byte(`
 NAME = ini
 expires = yes
 [package]
 NAME = %(NAME)s
 expires = %(expires)s`))
-		So(err, ShouldBeNil)
-		So(f, ShouldNotBeNil)
+		require.NoError(t, err)
+		require.NotNil(t, f)
 
-		So(f.Section("package").Key("NAME").String(), ShouldEqual, "ini")
-		So(f.Section("package").Key("expires").String(), ShouldEqual, "yes")
+		assert.Equal(t, "ini", f.Section("package").Key("NAME").String())
+		assert.Equal(t, "yes", f.Section("package").Key("expires").String())
 	})
 
-	Convey("Recursive value with no target found", t, func() {
-		f, err := ini.Load([]byte(`
+	t.Run("recursive value with no target found", func(t *testing.T) {
+		f, err := Load([]byte(`
 [foo]
 bar = %(missing)s
 `))
-		So(err, ShouldBeNil)
-		So(f, ShouldNotBeNil)
+		require.NoError(t, err)
+		require.NotNil(t, f)
 
-		So(f.Section("foo").Key("bar").String(), ShouldEqual, "%(missing)s")
+		assert.Equal(t, "%(missing)s", f.Section("foo").Key("bar").String())
 	})
 }
diff --git a/parser.go b/parser.go
index f023db5..ac1c980 100644
--- a/parser.go
+++ b/parser.go
@@ -84,7 +84,10 @@ func (p *parser) BOM() error {
 	case mask[0] == 254 && mask[1] == 255:
 		fallthrough
 	case mask[0] == 255 && mask[1] == 254:
-		p.buf.Read(mask)
+		_, err = p.buf.Read(mask)
+		if err != nil {
+			return err
+		}
 	case mask[0] == 239 && mask[1] == 187:
 		mask, err := p.buf.Peek(3)
 		if err != nil && err != io.EOF {
@@ -93,7 +96,10 @@ func (p *parser) BOM() error {
 			return nil
 		}
 		if mask[2] == 191 {
-			p.buf.Read(mask)
+			_, err = p.buf.Read(mask)
+			if err != nil {
+				return err
+			}
 		}
 	}
 	return nil
@@ -125,7 +131,7 @@ func readKeyName(delimiters string, in []byte) (string, int, error) {
 	// Check if key name surrounded by quotes.
 	var keyQuote string
 	if line[0] == '"' {
-		if len(line) > 6 && string(line[0:3]) == `"""` {
+		if len(line) > 6 && line[0:3] == `"""` {
 			keyQuote = `"""`
 		} else {
 			keyQuote = `"`
@@ -135,7 +141,7 @@ func readKeyName(delimiters string, in []byte) (string, int, error) {
 	}
 
 	// Get out key name
-	endIdx := -1
+	var endIdx int
 	if len(keyQuote) > 0 {
 		startIdx := len(keyQuote)
 		// FIXME: fail case -> """"""name"""=value
@@ -226,7 +232,7 @@ func (p *parser) readValue(in []byte, bufferSize int) (string, error) {
 	}
 
 	var valQuote string
-	if len(line) > 3 && string(line[0:3]) == `"""` {
+	if len(line) > 3 && line[0:3] == `"""` {
 		valQuote = `"""`
 	} else if line[0] == '`' {
 		valQuote = "`"
@@ -283,12 +289,8 @@ func (p *parser) readValue(in []byte, bufferSize int) (string, error) {
 		hasSurroundedQuote(line, '"')) && !p.options.PreserveSurroundedQuote {
 		line = line[1 : len(line)-1]
 	} else if len(valQuote) == 0 && p.options.UnescapeValueCommentSymbols {
-		if strings.Contains(line, `\;`) {
-			line = strings.Replace(line, `\;`, ";", -1)
-		}
-		if strings.Contains(line, `\#`) {
-			line = strings.Replace(line, `\#`, "#", -1)
-		}
+		line = strings.ReplaceAll(line, `\;`, ";")
+		line = strings.ReplaceAll(line, `\#`, "#")
 	} else if p.options.AllowPythonMultilineValues && lastChar == '\n' {
 		return p.readPythonMultilines(line, bufferSize)
 	}
@@ -300,15 +302,9 @@ func (p *parser) readPythonMultilines(line string, bufferSize int) (string, erro
 	parserBufferPeekResult, _ := p.buf.Peek(bufferSize)
 	peekBuffer := bytes.NewBuffer(parserBufferPeekResult)
 
-	indentSize := 0
 	for {
 		peekData, peekErr := peekBuffer.ReadBytes('\n')
-		if peekErr != nil {
-			if peekErr == io.EOF {
-				p.debug("readPythonMultilines: io.EOF, peekData: %q, line: %q", string(peekData), line)
-				return line, nil
-			}
-
+		if peekErr != nil && peekErr != io.EOF {
 			p.debug("readPythonMultilines: failed to peek with error: %v", peekErr)
 			return "", peekErr
 		}
@@ -327,19 +323,6 @@ func (p *parser) readPythonMultilines(line string, bufferSize int) (string, erro
 			return line, nil
 		}
 
-		// Determine indent size and line prefix.
-		currentIndentSize := len(peekMatches[1])
-		if indentSize < 1 {
-			indentSize = currentIndentSize
-			p.debug("readPythonMultilines: indent size is %d", indentSize)
-		}
-
-		// Make sure each line is indented at least as far as first line.
-		if currentIndentSize < indentSize {
-			p.debug("readPythonMultilines: end of value, current indent: %d, expected indent: %d, line: %q", currentIndentSize, indentSize, line)
-			return line, nil
-		}
-
 		// Advance the parser reader (buffer) in-sync with the peek buffer.
 		_, err := p.buf.Discard(len(peekData))
 		if err != nil {
@@ -347,8 +330,7 @@ func (p *parser) readPythonMultilines(line string, bufferSize int) (string, erro
 			return "", err
 		}
 
-		// Handle indented empty line.
-		line += "\n" + peekMatches[1][indentSize:] + peekMatches[2]
+		line += "\n" + peekMatches[0]
 	}
 }
 
@@ -371,7 +353,7 @@ func (f *File) parse(reader io.Reader) (err error) {
 
 	// Ignore error because default section name is never empty string.
 	name := DefaultSection
-	if f.options.Insensitive {
+	if f.options.Insensitive || f.options.InsensitiveSections {
 		name = strings.ToLower(DefaultSection)
 	}
 	section, _ := f.NewSection(name)
@@ -413,7 +395,10 @@ func (f *File) parse(reader io.Reader) (err error) {
 		if f.options.AllowNestedValues &&
 			isLastValueEmpty && len(line) > 0 {
 			if line[0] == ' ' || line[0] == '\t' {
-				lastRegularKey.addNestedValue(string(bytes.TrimSpace(line)))
+				err = lastRegularKey.addNestedValue(string(bytes.TrimSpace(line)))
+				if err != nil {
+					return err
+				}
 				continue
 			}
 		}
@@ -456,11 +441,13 @@ func (f *File) parse(reader io.Reader) (err error) {
 			// Reset auto-counter and comments
 			p.comment.Reset()
 			p.count = 1
+			// Nested values can't span sections
+			isLastValueEmpty = false
 
 			inUnparseableSection = false
 			for i := range f.options.UnparseableSections {
 				if f.options.UnparseableSections[i] == name ||
-					(f.options.Insensitive && strings.ToLower(f.options.UnparseableSections[i]) == strings.ToLower(name)) {
+					((f.options.Insensitive || f.options.InsensitiveSections) && strings.EqualFold(f.options.UnparseableSections[i], name)) {
 					inUnparseableSection = true
 					continue
 				}
diff --git a/parser_test.go b/parser_test.go
index bd3c6ac..7016d67 100644
--- a/parser_test.go
+++ b/parser_test.go
@@ -12,66 +12,66 @@
 // License for the specific language governing permissions and limitations
 // under the License.
 
-package ini_test
+package ini
 
 import (
 	"testing"
 
-	. "github.com/smartystreets/goconvey/convey"
-	"gopkg.in/ini.v1"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func TestBOM(t *testing.T) {
-	Convey("Test handling BOM", t, func() {
-		Convey("UTF-8-BOM", func() {
-			f, err := ini.Load("testdata/UTF-8-BOM.ini")
-			So(err, ShouldBeNil)
-			So(f, ShouldNotBeNil)
+	t.Run("test handling BOM", func(t *testing.T) {
+		t.Run("UTF-8-BOM", func(t *testing.T) {
+			f, err := Load("testdata/UTF-8-BOM.ini")
+			require.NoError(t, err)
+			require.NotNil(t, f)
 
-			So(f.Section("author").Key("E-MAIL").String(), ShouldEqual, "example@email.com")
+			assert.Equal(t, "example@email.com", f.Section("author").Key("E-MAIL").String())
 		})
 
-		Convey("UTF-16-LE-BOM", func() {
-			f, err := ini.Load("testdata/UTF-16-LE-BOM.ini")
-			So(err, ShouldBeNil)
-			So(f, ShouldNotBeNil)
+		t.Run("UTF-16-LE-BOM", func(t *testing.T) {
+			f, err := Load("testdata/UTF-16-LE-BOM.ini")
+			require.NoError(t, err)
+			require.NotNil(t, f)
 		})
 
-		Convey("UTF-16-BE-BOM", func() {
+		t.Run("UTF-16-BE-BOM", func(t *testing.T) {
 		})
 	})
 }
 
 func TestBadLoad(t *testing.T) {
-	Convey("Load with bad data", t, func() {
-		Convey("Bad section name", func() {
-			_, err := ini.Load([]byte("[]"))
-			So(err, ShouldNotBeNil)
+	t.Run("load with bad data", func(t *testing.T) {
+		t.Run("bad section name", func(t *testing.T) {
+			_, err := Load([]byte("[]"))
+			require.Error(t, err)
 
-			_, err = ini.Load([]byte("["))
-			So(err, ShouldNotBeNil)
+			_, err = Load([]byte("["))
+			require.Error(t, err)
 		})
 
-		Convey("Bad keys", func() {
-			_, err := ini.Load([]byte(`"""name`))
-			So(err, ShouldNotBeNil)
+		t.Run("bad keys", func(t *testing.T) {
+			_, err := Load([]byte(`"""name`))
+			require.Error(t, err)
 
-			_, err = ini.Load([]byte(`"""name"""`))
-			So(err, ShouldNotBeNil)
+			_, err = Load([]byte(`"""name"""`))
+			require.Error(t, err)
 
-			_, err = ini.Load([]byte(`""=1`))
-			So(err, ShouldNotBeNil)
+			_, err = Load([]byte(`""=1`))
+			require.Error(t, err)
 
-			_, err = ini.Load([]byte(`=`))
-			So(err, ShouldNotBeNil)
+			_, err = Load([]byte(`=`))
+			require.Error(t, err)
 
-			_, err = ini.Load([]byte(`name`))
-			So(err, ShouldNotBeNil)
+			_, err = Load([]byte(`name`))
+			require.Error(t, err)
 		})
 
-		Convey("Bad values", func() {
-			_, err := ini.Load([]byte(`name="""Unknwon`))
-			So(err, ShouldNotBeNil)
+		t.Run("bad values", func(t *testing.T) {
+			_, err := Load([]byte(`name="""Unknwon`))
+			require.Error(t, err)
 		})
 	})
 }
diff --git a/section.go b/section.go
index 6ba5ac2..a3615d8 100644
--- a/section.go
+++ b/section.go
@@ -66,7 +66,7 @@ func (s *Section) SetBody(body string) {
 func (s *Section) NewKey(name, val string) (*Key, error) {
 	if len(name) == 0 {
 		return nil, errors.New("error creating new key: empty key name")
-	} else if s.f.options.Insensitive {
+	} else if s.f.options.Insensitive || s.f.options.InsensitiveKeys {
 		name = strings.ToLower(name)
 	}
 
@@ -109,7 +109,7 @@ func (s *Section) GetKey(name string) (*Key, error) {
 	if s.f.BlockMode {
 		s.f.lock.RLock()
 	}
-	if s.f.options.Insensitive {
+	if s.f.options.Insensitive || s.f.options.InsensitiveKeys {
 		name = strings.ToLower(name)
 	}
 	key := s.keys[name]
@@ -121,7 +121,7 @@ func (s *Section) GetKey(name string) (*Key, error) {
 		// Check if it is a child-section.
 		sname := s.name
 		for {
-			if i := strings.LastIndex(sname, "."); i > -1 {
+			if i := strings.LastIndex(sname, s.f.options.ChildSectionDelimiter); i > -1 {
 				sname = sname[:i]
 				sec, err := s.f.GetSection(sname)
 				if err != nil {
@@ -188,7 +188,7 @@ func (s *Section) ParentKeys() []*Key {
 	var parentKeys []*Key
 	sname := s.name
 	for {
-		if i := strings.LastIndex(sname, "."); i > -1 {
+		if i := strings.LastIndex(sname, s.f.options.ChildSectionDelimiter); i > -1 {
 			sname = sname[:i]
 			sec, err := s.f.GetSection(sname)
 			if err != nil {
@@ -217,7 +217,7 @@ func (s *Section) KeysHash() map[string]string {
 		defer s.f.lock.RUnlock()
 	}
 
-	hash := map[string]string{}
+	hash := make(map[string]string, len(s.keysHash))
 	for key, value := range s.keysHash {
 		hash[key] = value
 	}
@@ -245,7 +245,7 @@ func (s *Section) DeleteKey(name string) {
 // For example, "[parent.child1]" and "[parent.child12]" are child sections
 // of section "[parent]".
 func (s *Section) ChildSections() []*Section {
-	prefix := s.name + "."
+	prefix := s.name + s.f.options.ChildSectionDelimiter
 	children := make([]*Section, 0, 3)
 	for _, name := range s.f.sectionList {
 		if strings.HasPrefix(name, prefix) {
diff --git a/section_test.go b/section_test.go
index 37da867..09ed366 100644
--- a/section_test.go
+++ b/section_test.go
@@ -12,267 +12,267 @@
 // License for the specific language governing permissions and limitations
 // under the License.
 
-package ini_test
+package ini
 
 import (
 	"testing"
 
-	. "github.com/smartystreets/goconvey/convey"
-	"gopkg.in/ini.v1"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func TestSection_SetBody(t *testing.T) {
-	Convey("Set body of raw section", t, func() {
-		f := ini.Empty()
-		So(f, ShouldNotBeNil)
+	t.Run("set body of raw section", func(t *testing.T) {
+		f := Empty()
+		require.NotNil(t, f)
 
 		sec, err := f.NewRawSection("comments", `1111111111111111111000000000000000001110000
 111111111111111111100000000000111000000000`)
-		So(err, ShouldBeNil)
-		So(sec, ShouldNotBeNil)
-		So(sec.Body(), ShouldEqual, `1111111111111111111000000000000000001110000
-111111111111111111100000000000111000000000`)
+		require.NoError(t, err)
+		require.NotNil(t, sec)
+		assert.Equal(t, `1111111111111111111000000000000000001110000
+111111111111111111100000000000111000000000`, sec.Body())
 
 		sec.SetBody("1111111111111111111000000000000000001110000")
-		So(sec.Body(), ShouldEqual, `1111111111111111111000000000000000001110000`)
+		assert.Equal(t, `1111111111111111111000000000000000001110000`, sec.Body())
 
-		Convey("Set for non-raw section", func() {
+		t.Run("set for non-raw section", func(t *testing.T) {
 			sec, err := f.NewSection("author")
-			So(err, ShouldBeNil)
-			So(sec, ShouldNotBeNil)
-			So(sec.Body(), ShouldBeEmpty)
+			require.NoError(t, err)
+			require.NotNil(t, sec)
+			assert.Empty(t, sec.Body())
 
 			sec.SetBody("1111111111111111111000000000000000001110000")
-			So(sec.Body(), ShouldBeEmpty)
+			assert.Empty(t, sec.Body())
 		})
 	})
 }
 
 func TestSection_NewKey(t *testing.T) {
-	Convey("Create a new key", t, func() {
-		f := ini.Empty()
-		So(f, ShouldNotBeNil)
+	t.Run("create a new key", func(t *testing.T) {
+		f := Empty()
+		require.NotNil(t, f)
 
 		k, err := f.Section("").NewKey("NAME", "ini")
-		So(err, ShouldBeNil)
-		So(k, ShouldNotBeNil)
-		So(k.Name(), ShouldEqual, "NAME")
-		So(k.Value(), ShouldEqual, "ini")
+		require.NoError(t, err)
+		require.NotNil(t, k)
+		assert.Equal(t, "NAME", k.Name())
+		assert.Equal(t, "ini", k.Value())
 
-		Convey("With duplicated name", func() {
+		t.Run("with duplicated name", func(t *testing.T) {
 			k, err := f.Section("").NewKey("NAME", "ini.v1")
-			So(err, ShouldBeNil)
-			So(k, ShouldNotBeNil)
+			require.NoError(t, err)
+			require.NotNil(t, k)
 
 			// Overwrite previous existed key
-			So(k.Value(), ShouldEqual, "ini.v1")
+			assert.Equal(t, "ini.v1", k.Value())
 		})
 
-		Convey("With empty string", func() {
+		t.Run("with empty string", func(t *testing.T) {
 			_, err := f.Section("").NewKey("", "")
-			So(err, ShouldNotBeNil)
+			require.Error(t, err)
 		})
 	})
 
-	Convey("Create keys with same name and allow shadow", t, func() {
-		f, err := ini.ShadowLoad([]byte(""))
-		So(err, ShouldBeNil)
-		So(f, ShouldNotBeNil)
+	t.Run("create keys with same name and allow shadow", func(t *testing.T) {
+		f, err := ShadowLoad([]byte(""))
+		require.NoError(t, err)
+		require.NotNil(t, f)
 
 		k, err := f.Section("").NewKey("NAME", "ini")
-		So(err, ShouldBeNil)
-		So(k, ShouldNotBeNil)
+		require.NoError(t, err)
+		require.NotNil(t, k)
 		k, err = f.Section("").NewKey("NAME", "ini.v1")
-		So(err, ShouldBeNil)
-		So(k, ShouldNotBeNil)
+		require.NoError(t, err)
+		require.NotNil(t, k)
 
-		So(k.ValueWithShadows(), ShouldResemble, []string{"ini", "ini.v1"})
+		assert.Equal(t, []string{"ini", "ini.v1"}, k.ValueWithShadows())
 	})
 }
 
 func TestSection_NewBooleanKey(t *testing.T) {
-	Convey("Create a new boolean key", t, func() {
-		f := ini.Empty()
-		So(f, ShouldNotBeNil)
+	t.Run("create a new boolean key", func(t *testing.T) {
+		f := Empty()
+		require.NotNil(t, f)
 
 		k, err := f.Section("").NewBooleanKey("start-ssh-server")
-		So(err, ShouldBeNil)
-		So(k, ShouldNotBeNil)
-		So(k.Name(), ShouldEqual, "start-ssh-server")
-		So(k.Value(), ShouldEqual, "true")
+		require.NoError(t, err)
+		require.NotNil(t, k)
+		assert.Equal(t, "start-ssh-server", k.Name())
+		assert.Equal(t, "true", k.Value())
 
-		Convey("With empty string", func() {
+		t.Run("with empty string", func(t *testing.T) {
 			_, err := f.Section("").NewBooleanKey("")
-			So(err, ShouldNotBeNil)
+			require.Error(t, err)
 		})
 	})
 }
 
 func TestSection_GetKey(t *testing.T) {
-	Convey("Get a key", t, func() {
-		f := ini.Empty()
-		So(f, ShouldNotBeNil)
+	t.Run("get a key", func(t *testing.T) {
+		f := Empty()
+		require.NotNil(t, f)
 
 		k, err := f.Section("").NewKey("NAME", "ini")
-		So(err, ShouldBeNil)
-		So(k, ShouldNotBeNil)
+		require.NoError(t, err)
+		require.NotNil(t, k)
 
 		k, err = f.Section("").GetKey("NAME")
-		So(err, ShouldBeNil)
-		So(k, ShouldNotBeNil)
-		So(k.Name(), ShouldEqual, "NAME")
-		So(k.Value(), ShouldEqual, "ini")
+		require.NoError(t, err)
+		require.NotNil(t, k)
+		assert.Equal(t, "NAME", k.Name())
+		assert.Equal(t, "ini", k.Value())
 
-		Convey("Key not exists", func() {
+		t.Run("key not exists", func(t *testing.T) {
 			_, err := f.Section("").GetKey("404")
-			So(err, ShouldNotBeNil)
+			require.Error(t, err)
 		})
 
-		Convey("Key exists in parent section", func() {
+		t.Run("key exists in parent section", func(t *testing.T) {
 			k, err := f.Section("parent").NewKey("AGE", "18")
-			So(err, ShouldBeNil)
-			So(k, ShouldNotBeNil)
+			require.NoError(t, err)
+			require.NotNil(t, k)
 
 			k, err = f.Section("parent.child.son").GetKey("AGE")
-			So(err, ShouldBeNil)
-			So(k, ShouldNotBeNil)
-			So(k.Value(), ShouldEqual, "18")
+			require.NoError(t, err)
+			require.NotNil(t, k)
+			assert.Equal(t, "18", k.Value())
 		})
 	})
 }
 
 func TestSection_HasKey(t *testing.T) {
-	Convey("Check if a key exists", t, func() {
-		f := ini.Empty()
-		So(f, ShouldNotBeNil)
+	t.Run("check if a key exists", func(t *testing.T) {
+		f := Empty()
+		require.NotNil(t, f)
 
 		k, err := f.Section("").NewKey("NAME", "ini")
-		So(err, ShouldBeNil)
-		So(k, ShouldNotBeNil)
+		require.NoError(t, err)
+		require.NotNil(t, k)
 
-		So(f.Section("").HasKey("NAME"), ShouldBeTrue)
-		So(f.Section("").HasKey("NAME"), ShouldBeTrue)
-		So(f.Section("").HasKey("404"), ShouldBeFalse)
-		So(f.Section("").HasKey("404"), ShouldBeFalse)
+		assert.True(t, f.Section("").HasKey("NAME"))
+		assert.True(t, f.Section("").HasKey("NAME"))
+		assert.False(t, f.Section("").HasKey("404"))
+		assert.False(t, f.Section("").HasKey("404"))
 	})
 }
 
 func TestSection_HasValue(t *testing.T) {
-	Convey("Check if contains a value in any key", t, func() {
-		f := ini.Empty()
-		So(f, ShouldNotBeNil)
+	t.Run("check if contains a value in any key", func(t *testing.T) {
+		f := Empty()
+		require.NotNil(t, f)
 
 		k, err := f.Section("").NewKey("NAME", "ini")
-		So(err, ShouldBeNil)
-		So(k, ShouldNotBeNil)
+		require.NoError(t, err)
+		require.NotNil(t, k)
 
-		So(f.Section("").HasValue("ini"), ShouldBeTrue)
-		So(f.Section("").HasValue("404"), ShouldBeFalse)
+		assert.True(t, f.Section("").HasValue("ini"))
+		assert.False(t, f.Section("").HasValue("404"))
 	})
 }
 
 func TestSection_Key(t *testing.T) {
-	Convey("Get a key", t, func() {
-		f := ini.Empty()
-		So(f, ShouldNotBeNil)
+	t.Run("get a key", func(t *testing.T) {
+		f := Empty()
+		require.NotNil(t, f)
 
 		k, err := f.Section("").NewKey("NAME", "ini")
-		So(err, ShouldBeNil)
-		So(k, ShouldNotBeNil)
+		require.NoError(t, err)
+		require.NotNil(t, k)
 
 		k = f.Section("").Key("NAME")
-		So(k, ShouldNotBeNil)
-		So(k.Name(), ShouldEqual, "NAME")
-		So(k.Value(), ShouldEqual, "ini")
+		require.NotNil(t, k)
+		assert.Equal(t, "NAME", k.Name())
+		assert.Equal(t, "ini", k.Value())
 
-		Convey("Key not exists", func() {
+		t.Run("key not exists", func(t *testing.T) {
 			k := f.Section("").Key("404")
-			So(k, ShouldNotBeNil)
-			So(k.Name(), ShouldEqual, "404")
+			require.NotNil(t, k)
+			assert.Equal(t, "404", k.Name())
 		})
 
-		Convey("Key exists in parent section", func() {
+		t.Run("key exists in parent section", func(t *testing.T) {
 			k, err := f.Section("parent").NewKey("AGE", "18")
-			So(err, ShouldBeNil)
-			So(k, ShouldNotBeNil)
+			require.NoError(t, err)
+			require.NotNil(t, k)
 
 			k = f.Section("parent.child.son").Key("AGE")
-			So(k, ShouldNotBeNil)
-			So(k.Value(), ShouldEqual, "18")
+			require.NotNil(t, k)
+			assert.Equal(t, "18", k.Value())
 		})
 	})
 }
 
 func TestSection_Keys(t *testing.T) {
-	Convey("Get all keys in a section", t, func() {
-		f := ini.Empty()
-		So(f, ShouldNotBeNil)
+	t.Run("get all keys in a section", func(t *testing.T) {
+		f := Empty()
+		require.NotNil(t, f)
 
 		k, err := f.Section("").NewKey("NAME", "ini")
-		So(err, ShouldBeNil)
-		So(k, ShouldNotBeNil)
+		require.NoError(t, err)
+		require.NotNil(t, k)
 		k, err = f.Section("").NewKey("VERSION", "v1")
-		So(err, ShouldBeNil)
-		So(k, ShouldNotBeNil)
+		require.NoError(t, err)
+		require.NotNil(t, k)
 		k, err = f.Section("").NewKey("IMPORT_PATH", "gopkg.in/ini.v1")
-		So(err, ShouldBeNil)
-		So(k, ShouldNotBeNil)
+		require.NoError(t, err)
+		require.NotNil(t, k)
 
 		keys := f.Section("").Keys()
 		names := []string{"NAME", "VERSION", "IMPORT_PATH"}
-		So(len(keys), ShouldEqual, len(names))
+		assert.Equal(t, len(names), len(keys))
 		for i, name := range names {
-			So(keys[i].Name(), ShouldEqual, name)
+			assert.Equal(t, name, keys[i].Name())
 		}
 	})
 }
 
 func TestSection_ParentKeys(t *testing.T) {
-	Convey("Get all keys of parent sections", t, func() {
-		f := ini.Empty()
-		So(f, ShouldNotBeNil)
+	t.Run("get all keys of parent sections", func(t *testing.T) {
+		f := Empty()
+		require.NotNil(t, f)
 
 		k, err := f.Section("package").NewKey("NAME", "ini")
-		So(err, ShouldBeNil)
-		So(k, ShouldNotBeNil)
+		require.NoError(t, err)
+		require.NotNil(t, k)
 		k, err = f.Section("package").NewKey("VERSION", "v1")
-		So(err, ShouldBeNil)
-		So(k, ShouldNotBeNil)
+		require.NoError(t, err)
+		require.NotNil(t, k)
 		k, err = f.Section("package").NewKey("IMPORT_PATH", "gopkg.in/ini.v1")
-		So(err, ShouldBeNil)
-		So(k, ShouldNotBeNil)
+		require.NoError(t, err)
+		require.NotNil(t, k)
 
 		keys := f.Section("package.sub.sub2").ParentKeys()
 		names := []string{"NAME", "VERSION", "IMPORT_PATH"}
-		So(len(keys), ShouldEqual, len(names))
+		assert.Equal(t, len(names), len(keys))
 		for i, name := range names {
-			So(keys[i].Name(), ShouldEqual, name)
+			assert.Equal(t, name, keys[i].Name())
 		}
 	})
 }
 
 func TestSection_KeyStrings(t *testing.T) {
-	Convey("Get all key names in a section", t, func() {
-		f := ini.Empty()
-		So(f, ShouldNotBeNil)
+	t.Run("get all key names in a section", func(t *testing.T) {
+		f := Empty()
+		require.NotNil(t, f)
 
 		k, err := f.Section("").NewKey("NAME", "ini")
-		So(err, ShouldBeNil)
-		So(k, ShouldNotBeNil)
+		require.NoError(t, err)
+		require.NotNil(t, k)
 		k, err = f.Section("").NewKey("VERSION", "v1")
-		So(err, ShouldBeNil)
-		So(k, ShouldNotBeNil)
+		require.NoError(t, err)
+		require.NotNil(t, k)
 		k, err = f.Section("").NewKey("IMPORT_PATH", "gopkg.in/ini.v1")
-		So(err, ShouldBeNil)
-		So(k, ShouldNotBeNil)
+		require.NoError(t, err)
+		require.NotNil(t, k)
 
-		So(f.Section("").KeyStrings(), ShouldResemble, []string{"NAME", "VERSION", "IMPORT_PATH"})
+		assert.Equal(t, []string{"NAME", "VERSION", "IMPORT_PATH"}, f.Section("").KeyStrings())
 	})
 }
 
 func TestSection_KeyHash(t *testing.T) {
-	Convey("Get clone of key hash", t, func() {
-		f, err := ini.Load([]byte(`
+	t.Run("get clone of key hash", func(t *testing.T) {
+		f, err := Load([]byte(`
 key = one
 [log]
 name = app
@@ -283,10 +283,10 @@ key = two
 name = app2
 file = b.log
 `))
-		So(err, ShouldBeNil)
-		So(f, ShouldNotBeNil)
+		require.NoError(t, err)
+		require.NotNil(t, f)
 
-		So(f.Section("").Key("key").String(), ShouldEqual, "two")
+		assert.Equal(t, "two", f.Section("").Key("key").String())
 
 		hash := f.Section("log").KeysHash()
 		relation := map[string]string{
@@ -294,22 +294,22 @@ file = b.log
 			"file": "b.log",
 		}
 		for k, v := range hash {
-			So(v, ShouldEqual, relation[k])
+			assert.Equal(t, relation[k], v)
 		}
 	})
 }
 
 func TestSection_DeleteKey(t *testing.T) {
-	Convey("Delete a key", t, func() {
-		f := ini.Empty()
-		So(f, ShouldNotBeNil)
+	t.Run("delete a key", func(t *testing.T) {
+		f := Empty()
+		require.NotNil(t, f)
 
 		k, err := f.Section("").NewKey("NAME", "ini")
-		So(err, ShouldBeNil)
-		So(k, ShouldNotBeNil)
+		require.NoError(t, err)
+		require.NotNil(t, k)
 
-		So(f.Section("").HasKey("NAME"), ShouldBeTrue)
+		assert.True(t, f.Section("").HasKey("NAME"))
 		f.Section("").DeleteKey("NAME")
-		So(f.Section("").HasKey("NAME"), ShouldBeFalse)
+		assert.False(t, f.Section("").HasKey("NAME"))
 	})
 }
diff --git a/struct.go b/struct.go
index 6b95849..a486b2f 100644
--- a/struct.go
+++ b/struct.go
@@ -263,22 +263,21 @@ func setWithProperType(t reflect.Type, key *Key, field reflect.Value, delim stri
 	return nil
 }
 
-func parseTagOptions(tag string) (rawName string, omitEmpty bool, allowShadow bool, allowNonUnique bool) {
-	opts := strings.SplitN(tag, ",", 4)
+func parseTagOptions(tag string) (rawName string, omitEmpty bool, allowShadow bool, allowNonUnique bool, extends bool) {
+	opts := strings.SplitN(tag, ",", 5)
 	rawName = opts[0]
-	if len(opts) > 1 {
-		omitEmpty = opts[1] == "omitempty"
+	for _, opt := range opts[1:] {
+		omitEmpty = omitEmpty || (opt == "omitempty")
+		allowShadow = allowShadow || (opt == "allowshadow")
+		allowNonUnique = allowNonUnique || (opt == "nonunique")
+		extends = extends || (opt == "extends")
 	}
-	if len(opts) > 2 {
-		allowShadow = opts[2] == "allowshadow"
-	}
-	if len(opts) > 3 {
-		allowNonUnique = opts[3] == "nonunique"
-	}
-	return rawName, omitEmpty, allowShadow, allowNonUnique
+	return rawName, omitEmpty, allowShadow, allowNonUnique, extends
 }
 
-func (s *Section) mapToField(val reflect.Value, isStrict bool) error {
+// mapToField maps the given value to the matching field of the given section.
+// The sectionIndex is the index (if non unique sections are enabled) to which the value should be added.
+func (s *Section) mapToField(val reflect.Value, isStrict bool, sectionIndex int, sectionName string) error {
 	if val.Kind() == reflect.Ptr {
 		val = val.Elem()
 	}
@@ -293,7 +292,7 @@ func (s *Section) mapToField(val reflect.Value, isStrict bool) error {
 			continue
 		}
 
-		rawName, _, allowShadow, allowNonUnique := parseTagOptions(tag)
+		rawName, _, allowShadow, allowNonUnique, extends := parseTagOptions(tag)
 		fieldName := s.parseFieldName(tpField.Name, rawName)
 		if len(fieldName) == 0 || !field.CanSet() {
 			continue
@@ -301,19 +300,36 @@ func (s *Section) mapToField(val reflect.Value, isStrict bool) error {
 
 		isStruct := tpField.Type.Kind() == reflect.Struct
 		isStructPtr := tpField.Type.Kind() == reflect.Ptr && tpField.Type.Elem().Kind() == reflect.Struct
-		isAnonymous := tpField.Type.Kind() == reflect.Ptr && tpField.Anonymous
-		if isAnonymous {
+		isAnonymousPtr := tpField.Type.Kind() == reflect.Ptr && tpField.Anonymous
+		if isAnonymousPtr {
 			field.Set(reflect.New(tpField.Type.Elem()))
 		}
 
-		if isAnonymous || isStruct || isStructPtr {
-			if sec, err := s.f.GetSection(fieldName); err == nil {
+		if extends && (isAnonymousPtr || (isStruct && tpField.Anonymous)) {
+			if isStructPtr && field.IsNil() {
+				field.Set(reflect.New(tpField.Type.Elem()))
+			}
+			fieldSection := s
+			if rawName != "" {
+				sectionName = s.name + s.f.options.ChildSectionDelimiter + rawName
+				if secs, err := s.f.SectionsByName(sectionName); err == nil && sectionIndex < len(secs) {
+					fieldSection = secs[sectionIndex]
+				}
+			}
+			if err := fieldSection.mapToField(field, isStrict, sectionIndex, sectionName); err != nil {
+				return fmt.Errorf("map to field %q: %v", fieldName, err)
+			}
+		} else if isAnonymousPtr || isStruct || isStructPtr {
+			if secs, err := s.f.SectionsByName(fieldName); err == nil {
+				if len(secs) <= sectionIndex {
+					return fmt.Errorf("there are not enough sections (%d <= %d) for the field %q", len(secs), sectionIndex, fieldName)
+				}
 				// Only set the field to non-nil struct value if we have a section for it.
 				// Otherwise, we end up with a non-nil struct ptr even though there is no data.
 				if isStructPtr && field.IsNil() {
 					field.Set(reflect.New(tpField.Type.Elem()))
 				}
-				if err = sec.mapToField(field, isStrict); err != nil {
+				if err = secs[sectionIndex].mapToField(field, isStrict, sectionIndex, fieldName); err != nil {
 					return fmt.Errorf("map to field %q: %v", fieldName, err)
 				}
 				continue
@@ -350,9 +366,9 @@ func (s *Section) mapToSlice(secName string, val reflect.Value, isStrict bool) (
 	}
 
 	typ := val.Type().Elem()
-	for _, sec := range secs {
+	for i, sec := range secs {
 		elem := reflect.New(typ)
-		if err = sec.mapToField(elem, isStrict); err != nil {
+		if err = sec.mapToField(elem, isStrict, i, sec.name); err != nil {
 			return reflect.Value{}, fmt.Errorf("map to field from section %q: %v", secName, err)
 		}
 
@@ -382,7 +398,7 @@ func (s *Section) mapTo(v interface{}, isStrict bool) error {
 		return nil
 	}
 
-	return s.mapToField(val, isStrict)
+	return s.mapToField(val, isStrict, 0, s.name)
 }
 
 // MapTo maps section to given struct.
@@ -474,7 +490,7 @@ func reflectSliceWithProperType(key *Key, field reflect.Value, delim string, all
 				_ = keyWithShadows.AddShadow(val)
 			}
 		}
-		key = keyWithShadows
+		*key = *keyWithShadows
 		return nil
 	}
 
@@ -564,6 +580,10 @@ func (s *Section) reflectFrom(val reflect.Value) error {
 	typ := val.Type()
 
 	for i := 0; i < typ.NumField(); i++ {
+		if !val.Field(i).CanInterface() {
+			continue
+		}
+
 		field := val.Field(i)
 		tpField := typ.Field(i)
 
@@ -572,7 +592,7 @@ func (s *Section) reflectFrom(val reflect.Value) error {
 			continue
 		}
 
-		rawName, omitEmpty, allowShadow, allowNonUnique := parseTagOptions(tag)
+		rawName, omitEmpty, allowShadow, allowNonUnique, extends := parseTagOptions(tag)
 		if omitEmpty && isEmptyValue(field) {
 			continue
 		}
@@ -586,7 +606,14 @@ func (s *Section) reflectFrom(val reflect.Value) error {
 			continue
 		}
 
-		if (tpField.Type.Kind() == reflect.Ptr && tpField.Anonymous) ||
+		if extends && tpField.Anonymous && (tpField.Type.Kind() == reflect.Ptr || tpField.Type.Kind() == reflect.Struct) {
+			if err := s.reflectFrom(field); err != nil {
+				return fmt.Errorf("reflect from field %q: %v", fieldName, err)
+			}
+			continue
+		}
+
+		if (tpField.Type.Kind() == reflect.Ptr && tpField.Type.Elem().Kind() == reflect.Struct) ||
 			(tpField.Type.Kind() == reflect.Struct && tpField.Type.Name() != "Time") {
 			// Note: The only error here is section doesn't exist.
 			sec, err := s.f.GetSection(fieldName)
@@ -695,7 +722,6 @@ func (s *Section) ReflectFrom(v interface{}) error {
 	}
 
 	if typ.Kind() == reflect.Ptr {
-		typ = typ.Elem()
 		val = val.Elem()
 	} else {
 		return errors.New("not a pointer to a struct")
diff --git a/struct_test.go b/struct_test.go
index 2b8fc29..6bacaea 100644
--- a/struct_test.go
+++ b/struct_test.go
@@ -12,7 +12,7 @@
 // License for the specific language governing permissions and limitations
 // under the License.
 
-package ini_test
+package ini
 
 import (
 	"bytes"
@@ -21,9 +21,8 @@ import (
 	"testing"
 	"time"
 
-	. "github.com/smartystreets/goconvey/convey"
-
-	"gopkg.in/ini.v1"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 type testNested struct {
@@ -58,8 +57,8 @@ type testStruct struct {
 	Unused         int `ini:"-"`
 	Unsigned       uint
 	Omitted        bool     `ini:"omitthis,omitempty"`
-	Shadows        []string `ini:",,allowshadow"`
-	ShadowInts     []int    `ini:"Shadows,,allowshadow"`
+	Shadows        []string `ini:",allowshadow"`
+	ShadowInts     []int    `ini:"Shadows,allowshadow"`
 	BoolPtr        *bool
 	BoolPtrNil     *bool
 	FloatPtr       *float64
@@ -90,7 +89,16 @@ type testPeer struct {
 
 type testNonUniqueSectionsStruct struct {
 	Interface testInterface
-	Peer      []testPeer `ini:",,,nonunique"`
+	Peer      []testPeer `ini:",nonunique"`
+}
+
+type BaseStruct struct {
+	Base bool
+}
+
+type testExtend struct {
+	BaseStruct `ini:",extends"`
+	Extend     bool
 }
 
 const confDataStruct = `
@@ -141,6 +149,10 @@ GPA = 2.8
 [foo.bar]
 Here = there
 When = then
+
+[extended]
+Base = true
+Extend = true
 `
 
 const confNonUniqueSectionDataStruct = `[Interface]
@@ -202,89 +214,88 @@ Cities =
 `
 
 func Test_MapToStruct(t *testing.T) {
-	Convey("Map to struct", t, func() {
-		Convey("Map file to struct", func() {
+	t.Run("map to struct", func(t *testing.T) {
+		t.Run("map file to struct", func(t *testing.T) {
 			ts := new(testStruct)
-			So(ini.MapTo(ts, []byte(confDataStruct)), ShouldBeNil)
+			assert.NoError(t, MapTo(ts, []byte(confDataStruct)))
 
-			So(ts.Name, ShouldEqual, "Unknwon")
-			So(ts.Age, ShouldEqual, 21)
-			So(ts.Male, ShouldBeTrue)
-			So(ts.Money, ShouldEqual, 1.25)
-			So(ts.Unsigned, ShouldEqual, 3)
+			assert.Equal(t, "Unknwon", ts.Name)
+			assert.Equal(t, 21, ts.Age)
+			assert.True(t, ts.Male)
+			assert.Equal(t, 1.25, ts.Money)
+			assert.Equal(t, uint(3), ts.Unsigned)
 
-			t, err := time.Parse(time.RFC3339, "1993-10-07T20:17:05Z")
-			So(err, ShouldBeNil)
-			So(ts.Born.String(), ShouldEqual, t.String())
+			ti, err := time.Parse(time.RFC3339, "1993-10-07T20:17:05Z")
+			require.NoError(t, err)
+			assert.Equal(t, ti.String(), ts.Born.String())
 
 			dur, err := time.ParseDuration("2h45m")
-			So(err, ShouldBeNil)
-			So(ts.Time.Seconds(), ShouldEqual, dur.Seconds())
-
-			So(ts.OldVersionTime*time.Second, ShouldEqual, 30*time.Second)
-
-			So(strings.Join(ts.Others.Cities, ","), ShouldEqual, "HangZhou,Boston")
-			So(ts.Others.Visits[0].String(), ShouldEqual, t.String())
-			So(fmt.Sprint(ts.Others.Years), ShouldEqual, "[1993 1994]")
-			So(fmt.Sprint(ts.Others.Numbers), ShouldEqual, "[10010 10086]")
-			So(fmt.Sprint(ts.Others.Ages), ShouldEqual, "[18 19]")
-			So(fmt.Sprint(ts.Others.Populations), ShouldEqual, "[12345678 98765432]")
-			So(fmt.Sprint(ts.Others.Coordinates), ShouldEqual, "[192.168 10.11]")
-			So(fmt.Sprint(ts.Others.Flags), ShouldEqual, "[true false]")
-			So(ts.Others.Note, ShouldEqual, "Hello world!")
-			So(ts.TestEmbeded.GPA, ShouldEqual, 2.8)
-
-			So(strings.Join(ts.OthersPtr.Cities, ","), ShouldEqual, "HangZhou,Boston")
-			So(ts.OthersPtr.Visits[0].String(), ShouldEqual, t.String())
-			So(fmt.Sprint(ts.OthersPtr.Years), ShouldEqual, "[1993 1994]")
-			So(fmt.Sprint(ts.OthersPtr.Numbers), ShouldEqual, "[10010 10086]")
-			So(fmt.Sprint(ts.OthersPtr.Ages), ShouldEqual, "[18 19]")
-			So(fmt.Sprint(ts.OthersPtr.Populations), ShouldEqual, "[12345678 98765432]")
-			So(fmt.Sprint(ts.OthersPtr.Coordinates), ShouldEqual, "[192.168 10.11]")
-			So(fmt.Sprint(ts.OthersPtr.Flags), ShouldEqual, "[true false]")
-			So(ts.OthersPtr.Note, ShouldEqual, "Hello world!")
-
-			So(ts.NilPtr, ShouldBeNil)
-
-			So(*ts.BoolPtr, ShouldEqual, false)
-			So(ts.BoolPtrNil, ShouldEqual, nil)
-			So(*ts.FloatPtr, ShouldEqual, 0)
-			So(ts.FloatPtrNil, ShouldEqual, nil)
-			So(*ts.IntPtr, ShouldEqual, 0)
-			So(ts.IntPtrNil, ShouldEqual, nil)
-			So(*ts.UintPtr, ShouldEqual, 0)
-			So(ts.UintPtrNil, ShouldEqual, nil)
-			So(*ts.StringPtr, ShouldEqual, "")
-			So(ts.StringPtrNil, ShouldEqual, nil)
-			So(*ts.TimePtr, ShouldNotEqual, nil)
-			So(ts.TimePtrNil, ShouldEqual, nil)
-			So(*ts.DurationPtr, ShouldEqual, 0)
-			So(ts.DurationPtrNil, ShouldEqual, nil)
-
+			require.NoError(t, err)
+			assert.Equal(t, dur.Seconds(), ts.Time.Seconds())
+
+			assert.Equal(t, 30*time.Second, ts.OldVersionTime*time.Second)
+
+			assert.Equal(t, "HangZhou,Boston", strings.Join(ts.Others.Cities, ","))
+			assert.Equal(t, ti.String(), ts.Others.Visits[0].String())
+			assert.Equal(t, "[1993 1994]", fmt.Sprint(ts.Others.Years))
+			assert.Equal(t, "[10010 10086]", fmt.Sprint(ts.Others.Numbers))
+			assert.Equal(t, "[18 19]", fmt.Sprint(ts.Others.Ages))
+			assert.Equal(t, "[12345678 98765432]", fmt.Sprint(ts.Others.Populations))
+			assert.Equal(t, "[192.168 10.11]", fmt.Sprint(ts.Others.Coordinates))
+			assert.Equal(t, "[true false]", fmt.Sprint(ts.Others.Flags))
+			assert.Equal(t, "Hello world!", ts.Others.Note)
+			assert.Equal(t, 2.8, ts.TestEmbeded.GPA)
+
+			assert.Equal(t, "HangZhou,Boston", strings.Join(ts.OthersPtr.Cities, ","))
+			assert.Equal(t, ti.String(), ts.OthersPtr.Visits[0].String())
+			assert.Equal(t, "[1993 1994]", fmt.Sprint(ts.OthersPtr.Years))
+			assert.Equal(t, "[10010 10086]", fmt.Sprint(ts.OthersPtr.Numbers))
+			assert.Equal(t, "[18 19]", fmt.Sprint(ts.OthersPtr.Ages))
+			assert.Equal(t, "[12345678 98765432]", fmt.Sprint(ts.OthersPtr.Populations))
+			assert.Equal(t, "[192.168 10.11]", fmt.Sprint(ts.OthersPtr.Coordinates))
+			assert.Equal(t, "[true false]", fmt.Sprint(ts.OthersPtr.Flags))
+			assert.Equal(t, "Hello world!", ts.OthersPtr.Note)
+
+			assert.Nil(t, ts.NilPtr)
+
+			assert.Equal(t, false, *ts.BoolPtr)
+			assert.Nil(t, ts.BoolPtrNil)
+			assert.Equal(t, float64(0), *ts.FloatPtr)
+			assert.Nil(t, ts.FloatPtrNil)
+			assert.Equal(t, 0, *ts.IntPtr)
+			assert.Nil(t, ts.IntPtrNil)
+			assert.Equal(t, uint(0), *ts.UintPtr)
+			assert.Nil(t, ts.UintPtrNil)
+			assert.Equal(t, "", *ts.StringPtr)
+			assert.Nil(t, ts.StringPtrNil)
+			assert.NotNil(t, *ts.TimePtr)
+			assert.Nil(t, ts.TimePtrNil)
+			assert.Equal(t, time.Duration(0), *ts.DurationPtr)
+			assert.Nil(t, ts.DurationPtrNil)
 		})
 
-		Convey("Map section to struct", func() {
+		t.Run("map section to struct", func(t *testing.T) {
 			foobar := new(fooBar)
-			f, err := ini.Load([]byte(confDataStruct))
-			So(err, ShouldBeNil)
+			f, err := Load([]byte(confDataStruct))
+			require.NoError(t, err)
 
-			So(f.Section("foo.bar").MapTo(foobar), ShouldBeNil)
-			So(foobar.Here, ShouldEqual, "there")
-			So(foobar.When, ShouldEqual, "then")
+			assert.NoError(t, f.Section("foo.bar").MapTo(foobar))
+			assert.Equal(t, "there", foobar.Here)
+			assert.Equal(t, "then", foobar.When)
 		})
 
-		Convey("Map to non-pointer struct", func() {
-			f, err := ini.Load([]byte(confDataStruct))
-			So(err, ShouldBeNil)
-			So(f, ShouldNotBeNil)
+		t.Run("map to non-pointer struct", func(t *testing.T) {
+			f, err := Load([]byte(confDataStruct))
+			require.NoError(t, err)
+			require.NotNil(t, f)
 
-			So(f.MapTo(testStruct{}), ShouldNotBeNil)
+			assert.Error(t, f.MapTo(testStruct{}))
 		})
 
-		Convey("Map to unsupported type", func() {
-			f, err := ini.Load([]byte(confDataStruct))
-			So(err, ShouldBeNil)
-			So(f, ShouldNotBeNil)
+		t.Run("map to unsupported type", func(t *testing.T) {
+			f, err := Load([]byte(confDataStruct))
+			require.NoError(t, err)
+			require.NotNil(t, f)
 
 			f.NameMapper = func(raw string) string {
 				if raw == "Byte" {
@@ -292,54 +303,64 @@ func Test_MapToStruct(t *testing.T) {
 				}
 				return raw
 			}
-			So(f.MapTo(&unsupport{}), ShouldNotBeNil)
-			So(f.MapTo(&unsupport2{}), ShouldNotBeNil)
-			So(f.MapTo(&unsupport4{}), ShouldNotBeNil)
+			assert.Error(t, f.MapTo(&unsupport{}))
+			assert.Error(t, f.MapTo(&unsupport2{}))
+			assert.Error(t, f.MapTo(&unsupport4{}))
 		})
 
-		Convey("Map to omitempty field", func() {
+		t.Run("map to omitempty field", func(t *testing.T) {
 			ts := new(testStruct)
-			So(ini.MapTo(ts, []byte(confDataStruct)), ShouldBeNil)
+			assert.NoError(t, MapTo(ts, []byte(confDataStruct)))
 
-			So(ts.Omitted, ShouldEqual, true)
+			assert.Equal(t, true, ts.Omitted)
 		})
 
-		Convey("Map with shadows", func() {
-			f, err := ini.LoadSources(ini.LoadOptions{AllowShadows: true}, []byte(confDataStruct))
-			So(err, ShouldBeNil)
+		t.Run("map with shadows", func(t *testing.T) {
+			f, err := LoadSources(LoadOptions{AllowShadows: true}, []byte(confDataStruct))
+			require.NoError(t, err)
 			ts := new(testStruct)
-			So(f.MapTo(ts), ShouldBeNil)
+			assert.NoError(t, f.MapTo(ts))
 
-			So(strings.Join(ts.Shadows, " "), ShouldEqual, "1 2 3 4")
-			So(fmt.Sprintf("%v", ts.ShadowInts), ShouldEqual, "[1 2 3 4]")
+			assert.Equal(t, "1 2 3 4", strings.Join(ts.Shadows, " "))
+			assert.Equal(t, "[1 2 3 4]", fmt.Sprintf("%v", ts.ShadowInts))
 		})
 
-		Convey("Map from invalid data source", func() {
-			So(ini.MapTo(&testStruct{}, "hi"), ShouldNotBeNil)
+		t.Run("map from invalid data source", func(t *testing.T) {
+			assert.Error(t, MapTo(&testStruct{}, "hi"))
 		})
 
-		Convey("Map to wrong types and gain default values", func() {
-			f, err := ini.Load([]byte(invalidDataConfStruct))
-			So(err, ShouldBeNil)
-
-			t, err := time.Parse(time.RFC3339, "1993-10-07T20:17:05Z")
-			So(err, ShouldBeNil)
-			dv := &defaultValue{"Joe", 10, true, nil, 1.25, t, []string{"HangZhou", "Boston"}}
-			So(f.MapTo(dv), ShouldBeNil)
-			So(dv.Name, ShouldEqual, "Joe")
-			So(dv.Age, ShouldEqual, 10)
-			So(dv.Male, ShouldBeTrue)
-			So(dv.Money, ShouldEqual, 1.25)
-			So(dv.Born.String(), ShouldEqual, t.String())
-			So(strings.Join(dv.Cities, ","), ShouldEqual, "HangZhou,Boston")
+		t.Run("map to wrong types and gain default values", func(t *testing.T) {
+			f, err := Load([]byte(invalidDataConfStruct))
+			require.NoError(t, err)
+
+			ti, err := time.Parse(time.RFC3339, "1993-10-07T20:17:05Z")
+			require.NoError(t, err)
+			dv := &defaultValue{"Joe", 10, true, nil, 1.25, ti, []string{"HangZhou", "Boston"}}
+			assert.NoError(t, f.MapTo(dv))
+			assert.Equal(t, "Joe", dv.Name)
+			assert.Equal(t, 10, dv.Age)
+			assert.True(t, dv.Male)
+			assert.Equal(t, 1.25, dv.Money)
+			assert.Equal(t, ti.String(), dv.Born.String())
+			assert.Equal(t, "HangZhou,Boston", strings.Join(dv.Cities, ","))
+		})
+
+		t.Run("map to extended base", func(t *testing.T) {
+			f, err := Load([]byte(confDataStruct))
+			require.NoError(t, err)
+			require.NotNil(t, f)
+			te := testExtend{}
+			assert.NoError(t, f.Section("extended").MapTo(&te))
+			assert.True(t, te.Base)
+			assert.True(t, te.Extend)
 		})
 	})
 
-	Convey("Map to struct in strict mode", t, func() {
-		f, err := ini.Load([]byte(`
+	t.Run("map to struct in strict mode", func(t *testing.T) {
+		f, err := Load([]byte(`
 name=bruce
 age=a30`))
-		So(err, ShouldBeNil)
+		require.NoError(t, err)
 
 		type Strict struct {
 			Name string `ini:"name"`
@@ -347,79 +368,123 @@ age=a30`))
 		}
 		s := new(Strict)
 
-		So(f.Section("").StrictMapTo(s), ShouldNotBeNil)
+		assert.Error(t, f.Section("").StrictMapTo(s))
 	})
 
-	Convey("Map slice in strict mode", t, func() {
-		f, err := ini.Load([]byte(`
+	t.Run("map slice in strict mode", func(t *testing.T) {
+		f, err := Load([]byte(`
 names=alice, bruce`))
-		So(err, ShouldBeNil)
+		require.NoError(t, err)
 
 		type Strict struct {
 			Names []string `ini:"names"`
 		}
 		s := new(Strict)
 
-		So(f.Section("").StrictMapTo(s), ShouldBeNil)
-		So(fmt.Sprint(s.Names), ShouldEqual, "[alice bruce]")
+		assert.NoError(t, f.Section("").StrictMapTo(s))
+		assert.Equal(t, "[alice bruce]", fmt.Sprint(s.Names))
 	})
 }
 
 func Test_MapToStructNonUniqueSections(t *testing.T) {
-	Convey("Map to struct non unique", t, func() {
-		Convey("Map file to struct non unique", func() {
-			f, err := ini.LoadSources(ini.LoadOptions{AllowNonUniqueSections: true}, []byte(confNonUniqueSectionDataStruct))
-			So(err, ShouldBeNil)
+	t.Run("map to struct non unique", func(t *testing.T) {
+		t.Run("map file to struct non unique", func(t *testing.T) {
+			f, err := LoadSources(LoadOptions{AllowNonUniqueSections: true}, []byte(confNonUniqueSectionDataStruct))
+			require.NoError(t, err)
 			ts := new(testNonUniqueSectionsStruct)
 
-			So(f.MapTo(ts), ShouldBeNil)
+			assert.NoError(t, f.MapTo(ts))
 
-			So(ts.Interface.Address, ShouldEqual, "10.2.0.1/24")
-			So(ts.Interface.ListenPort, ShouldEqual, 34777)
-			So(ts.Interface.PrivateKey, ShouldEqual, "privServerKey")
+			assert.Equal(t, "10.2.0.1/24", ts.Interface.Address)
+			assert.Equal(t, 34777, ts.Interface.ListenPort)
+			assert.Equal(t, "privServerKey", ts.Interface.PrivateKey)
 
-			So(ts.Peer[0].PublicKey, ShouldEqual, "pubClientKey")
-			So(ts.Peer[0].PresharedKey, ShouldEqual, "psKey")
-			So(ts.Peer[0].AllowedIPs[0], ShouldEqual, "10.2.0.2/32")
-			So(ts.Peer[0].AllowedIPs[1], ShouldEqual, "fd00:2::2/128")
+			assert.Equal(t, "pubClientKey", ts.Peer[0].PublicKey)
+			assert.Equal(t, "psKey", ts.Peer[0].PresharedKey)
+			assert.Equal(t, "10.2.0.2/32", ts.Peer[0].AllowedIPs[0])
+			assert.Equal(t, "fd00:2::2/128", ts.Peer[0].AllowedIPs[1])
 
-			So(ts.Peer[1].PublicKey, ShouldEqual, "pubClientKey2")
-			So(ts.Peer[1].PresharedKey, ShouldEqual, "psKey2")
-			So(ts.Peer[1].AllowedIPs[0], ShouldEqual, "10.2.0.3/32")
-			So(ts.Peer[1].AllowedIPs[1], ShouldEqual, "fd00:2::3/128")
+			assert.Equal(t, "pubClientKey2", ts.Peer[1].PublicKey)
+			assert.Equal(t, "psKey2", ts.Peer[1].PresharedKey)
+			assert.Equal(t, "10.2.0.3/32", ts.Peer[1].AllowedIPs[0])
+			assert.Equal(t, "fd00:2::3/128", ts.Peer[1].AllowedIPs[1])
 		})
 
-		Convey("Map non unique section to struct", func() {
+		t.Run("map non unique section to struct", func(t *testing.T) {
 			newPeer := new(testPeer)
 			newPeerSlice := make([]testPeer, 0)
 
-			f, err := ini.LoadSources(ini.LoadOptions{AllowNonUniqueSections: true}, []byte(confNonUniqueSectionDataStruct))
-			So(err, ShouldBeNil)
+			f, err := LoadSources(LoadOptions{AllowNonUniqueSections: true}, []byte(confNonUniqueSectionDataStruct))
+			require.NoError(t, err)
 
 			// try only first one
-			So(f.Section("Peer").MapTo(newPeer), ShouldBeNil)
-			So(newPeer.PublicKey, ShouldEqual, "pubClientKey")
-			So(newPeer.PresharedKey, ShouldEqual, "psKey")
-			So(newPeer.AllowedIPs[0], ShouldEqual, "10.2.0.2/32")
-			So(newPeer.AllowedIPs[1], ShouldEqual, "fd00:2::2/128")
+			assert.NoError(t, f.Section("Peer").MapTo(newPeer))
+			assert.Equal(t, "pubClientKey", newPeer.PublicKey)
+			assert.Equal(t, "psKey", newPeer.PresharedKey)
+			assert.Equal(t, "10.2.0.2/32", newPeer.AllowedIPs[0])
+			assert.Equal(t, "fd00:2::2/128", newPeer.AllowedIPs[1])
 
 			// try all
-			So(f.Section("Peer").MapTo(&newPeerSlice), ShouldBeNil)
-			So(newPeerSlice[0].PublicKey, ShouldEqual, "pubClientKey")
-			So(newPeerSlice[0].PresharedKey, ShouldEqual, "psKey")
-			So(newPeerSlice[0].AllowedIPs[0], ShouldEqual, "10.2.0.2/32")
-			So(newPeerSlice[0].AllowedIPs[1], ShouldEqual, "fd00:2::2/128")
-
-			So(newPeerSlice[1].PublicKey, ShouldEqual, "pubClientKey2")
-			So(newPeerSlice[1].PresharedKey, ShouldEqual, "psKey2")
-			So(newPeerSlice[1].AllowedIPs[0], ShouldEqual, "10.2.0.3/32")
-			So(newPeerSlice[1].AllowedIPs[1], ShouldEqual, "fd00:2::3/128")
+			assert.NoError(t, f.Section("Peer").MapTo(&newPeerSlice))
+			assert.Equal(t, "pubClientKey", newPeerSlice[0].PublicKey)
+			assert.Equal(t, "psKey", newPeerSlice[0].PresharedKey)
+			assert.Equal(t, "10.2.0.2/32", newPeerSlice[0].AllowedIPs[0])
+			assert.Equal(t, "fd00:2::2/128", newPeerSlice[0].AllowedIPs[1])
+
+			assert.Equal(t, "pubClientKey2", newPeerSlice[1].PublicKey)
+			assert.Equal(t, "psKey2", newPeerSlice[1].PresharedKey)
+			assert.Equal(t, "10.2.0.3/32", newPeerSlice[1].AllowedIPs[0])
+			assert.Equal(t, "fd00:2::3/128", newPeerSlice[1].AllowedIPs[1])
+		})
+
+		t.Run("map non unique sections with subsections to struct", func(t *testing.T) {
+			iniFile, err := LoadSources(LoadOptions{AllowNonUniqueSections: true}, strings.NewReader(`
+[Section]
+FieldInSubSection = 1
+FieldInSubSection2 = 2
+FieldInSection = 3
+
+[Section]
+FieldInSubSection = 4
+FieldInSubSection2 = 5
+FieldInSection = 6
+`))
+			require.NoError(t, err)
+
+			type SubSection struct {
+				FieldInSubSection string `ini:"FieldInSubSection"`
+			}
+			type SubSection2 struct {
+				FieldInSubSection2 string `ini:"FieldInSubSection2"`
+			}
+
+			type Section struct {
+				SubSection     `ini:"Section"`
+				SubSection2    `ini:"Section"`
+				FieldInSection string `ini:"FieldInSection"`
+			}
+
+			type File struct {
+				Sections []Section `ini:"Section,nonunique"`
+			}
+
+			f := new(File)
+			err = iniFile.MapTo(f)
+			require.NoError(t, err)
+
+			assert.Equal(t, "1", f.Sections[0].FieldInSubSection)
+			assert.Equal(t, "2", f.Sections[0].FieldInSubSection2)
+			assert.Equal(t, "3", f.Sections[0].FieldInSection)
+
+			assert.Equal(t, "4", f.Sections[1].FieldInSubSection)
+			assert.Equal(t, "5", f.Sections[1].FieldInSubSection2)
+			assert.Equal(t, "6", f.Sections[1].FieldInSection)
 		})
 	})
 }
 
 func Test_ReflectFromStruct(t *testing.T) {
-	Convey("Reflect from struct", t, func() {
+	t.Run("reflect from struct", func(t *testing.T) {
 		type Embeded struct {
 			Dates       []time.Time `delim:"|" comment:"Time data"`
 			Places      []string
@@ -440,14 +505,15 @@ func Test_ReflectFromStruct(t *testing.T) {
 			GPA       float64
 			Date      time.Time
 			NeverMind string `ini:"-"`
+			ignored   string
 			*Embeded  `ini:"infos" comment:"Embeded section"`
 		}
 
-		t, err := time.Parse(time.RFC3339, "1993-10-07T20:17:05Z")
-		So(err, ShouldBeNil)
-		a := &Author{"Unknwon", true, nil, 21, 100, 2.8, t, "",
+		ti, err := time.Parse(time.RFC3339, "1993-10-07T20:17:05Z")
+		require.NoError(t, err)
+		a := &Author{"Unknwon", true, nil, 21, 100, 2.8, ti, "", "ignored",
 			&Embeded{
-				[]time.Time{t, t},
+				[]time.Time{ti, ti},
 				[]string{"HangZhou", "Boston"},
 				[]int{1993, 1994},
 				[]int64{10010, 10086},
@@ -457,13 +523,13 @@ func Test_ReflectFromStruct(t *testing.T) {
 				[]bool{true, false},
 				[]int{},
 			}}
-		cfg := ini.Empty()
-		So(ini.ReflectFrom(cfg, a), ShouldBeNil)
+		cfg := Empty()
+		assert.NoError(t, ReflectFrom(cfg, a))
 
 		var buf bytes.Buffer
 		_, err = cfg.WriteTo(&buf)
-		So(err, ShouldBeNil)
-		So(buf.String(), ShouldEqual, `NAME     = Unknwon
+		require.NoError(t, err)
+		assert.Equal(t, `NAME     = Unknwon
 Male     = true
 Optional = 
 ; Author's age
@@ -485,38 +551,88 @@ Coordinates = 192.168,10.11
 Flags       = true,false
 None        = 
 
-`)
+`,
+			buf.String(),
+		)
 
-		Convey("Reflect from non-point struct", func() {
-			So(ini.ReflectFrom(cfg, Author{}), ShouldNotBeNil)
+		t.Run("reflect from non-point struct", func(t *testing.T) {
+			assert.Error(t, ReflectFrom(cfg, Author{}))
 		})
 
-		Convey("Reflect from struct with omitempty", func() {
-			cfg := ini.Empty()
+		t.Run("reflect from struct with omitempty", func(t *testing.T) {
+			cfg := Empty()
 			type SpecialStruct struct {
 				FirstName  string    `ini:"first_name"`
-				LastName   string    `ini:"last_name"`
+				LastName   string    `ini:"last_name,omitempty"`
 				JustOmitMe string    `ini:"omitempty"`
 				LastLogin  time.Time `ini:"last_login,omitempty"`
 				LastLogin2 time.Time `ini:",omitempty"`
 				NotEmpty   int       `ini:"omitempty"`
+				Number     int64     `ini:",omitempty"`
+				Ages       uint      `ini:",omitempty"`
+				Population uint64    `ini:",omitempty"`
+				Coordinate float64   `ini:",omitempty"`
+				Flag       bool      `ini:",omitempty"`
+				Note       *string   `ini:",omitempty"`
+			}
+			special := &SpecialStruct{
+				FirstName: "John",
+				LastName:  "Doe",
+				NotEmpty:  9,
 			}
 
-			So(ini.ReflectFrom(cfg, &SpecialStruct{FirstName: "John", LastName: "Doe", NotEmpty: 9}), ShouldBeNil)
+			assert.NoError(t, ReflectFrom(cfg, special))
 
 			var buf bytes.Buffer
 			_, err = cfg.WriteTo(&buf)
-			So(buf.String(), ShouldEqual, `first_name = John
+			assert.Equal(t, `first_name = John
 last_name  = Doe
 omitempty  = 9
 
-`)
+`,
+				buf.String(),
+			)
+		})
+
+		t.Run("reflect from struct with non-anonymous structure pointer", func(t *testing.T) {
+			cfg := Empty()
+			type Rpc struct {
+				Enable  bool   `ini:"enable"`
+				Type    string `ini:"type"`
+				Address string `ini:"addr"`
+				Name    string `ini:"name"`
+			}
+			type Cfg struct {
+				Rpc *Rpc `ini:"rpc"`
+			}
+
+			config := &Cfg{
+				Rpc: &Rpc{
+					Enable:  true,
+					Type:    "type",
+					Address: "address",
+					Name:    "name",
+				},
+			}
+			assert.NoError(t, cfg.ReflectFrom(config))
+
+			var buf bytes.Buffer
+			_, err = cfg.WriteTo(&buf)
+			assert.Equal(t, `[rpc]
+enable = true
+type   = type
+addr   = address
+name   = name
+
+`,
+				buf.String(),
+			)
 		})
 	})
 }
 
 func Test_ReflectFromStructNonUniqueSections(t *testing.T) {
-	Convey("Reflect from struct with non unique sections", t, func() {
+	t.Run("reflect from struct with non unique sections", func(t *testing.T) {
 		nonUnique := &testNonUniqueSectionsStruct{
 			Interface: testInterface{
 				Address:    "10.2.0.1/24",
@@ -537,16 +653,16 @@ func Test_ReflectFromStructNonUniqueSections(t *testing.T) {
 			},
 		}
 
-		cfg := ini.Empty(ini.LoadOptions{
+		cfg := Empty(LoadOptions{
 			AllowNonUniqueSections: true,
 		})
 
-		So(ini.ReflectFrom(cfg, nonUnique), ShouldBeNil)
+		assert.NoError(t, ReflectFrom(cfg, nonUnique))
 
 		var buf bytes.Buffer
 		_, err := cfg.WriteTo(&buf)
-		So(err, ShouldBeNil)
-		So(buf.String(), ShouldEqual, confNonUniqueSectionDataStruct)
+		require.NoError(t, err)
+		assert.Equal(t, confNonUniqueSectionDataStruct, buf.String())
 
 		// note: using ReflectFrom from should overwrite the existing sections
 		err = cfg.Section("Peer").ReflectFrom([]*testPeer{
@@ -562,12 +678,12 @@ func Test_ReflectFromStructNonUniqueSections(t *testing.T) {
 			},
 		})
 
-		So(err, ShouldBeNil)
+		require.NoError(t, err)
 
 		buf = bytes.Buffer{}
 		_, err = cfg.WriteTo(&buf)
-		So(err, ShouldBeNil)
-		So(buf.String(), ShouldEqual, `[Interface]
+		require.NoError(t, err)
+		assert.Equal(t, `[Interface]
 Address    = 10.2.0.1/24
 ListenPort = 34777
 PrivateKey = privServerKey
@@ -582,7 +698,9 @@ PublicKey    = pubClientKey4
 PresharedKey = psKey4
 AllowedIPs   = 10.2.0.5/32,fd00:2::5/128
 
-`)
+`,
+			buf.String(),
+		)
 
 		// note: using ReflectFrom from should overwrite the existing sections
 		err = cfg.Section("Peer").ReflectFrom(&testPeer{
@@ -591,12 +709,12 @@ AllowedIPs   = 10.2.0.5/32,fd00:2::5/128
 			AllowedIPs:   []string{"10.2.0.6/32,fd00:2::6/128"},
 		})
 
-		So(err, ShouldBeNil)
+		require.NoError(t, err)
 
 		buf = bytes.Buffer{}
 		_, err = cfg.WriteTo(&buf)
-		So(err, ShouldBeNil)
-		So(buf.String(), ShouldEqual, `[Interface]
+		require.NoError(t, err)
+		assert.Equal(t, `[Interface]
 Address    = 10.2.0.1/24
 ListenPort = 34777
 PrivateKey = privServerKey
@@ -606,41 +724,110 @@ PublicKey    = pubClientKey5
 PresharedKey = psKey5
 AllowedIPs   = 10.2.0.6/32,fd00:2::6/128
 
-`)
+`,
+			buf.String(),
+		)
 	})
 }
 
 // Inspired by https://github.com/go-ini/ini/issues/196
 func TestMapToAndReflectFromStructWithShadows(t *testing.T) {
-	Convey("Map to struct and then reflect with shadows should generate original config content", t, func() {
+	t.Run("map to struct and then reflect with shadows should generate original config content", func(t *testing.T) {
 		type include struct {
 			Paths []string `ini:"path,omitempty,allowshadow"`
 		}
 
-		cfg, err := ini.LoadSources(ini.LoadOptions{
+		cfg, err := LoadSources(LoadOptions{
 			AllowShadows: true,
 		}, []byte(`
 [include]
 path = /tmp/gpm-profiles/test5.profile
 path = /tmp/gpm-profiles/test1.profile`))
-		So(err, ShouldBeNil)
+		require.NoError(t, err)
 
 		sec := cfg.Section("include")
 		inc := new(include)
 		err = sec.MapTo(inc)
-		So(err, ShouldBeNil)
+		require.NoError(t, err)
 
 		err = sec.ReflectFrom(inc)
-		So(err, ShouldBeNil)
+		require.NoError(t, err)
 
 		var buf bytes.Buffer
 		_, err = cfg.WriteTo(&buf)
-		So(err, ShouldBeNil)
-		So(buf.String(), ShouldEqual, `[include]
+		require.NoError(t, err)
+		assert.Equal(t, `[include]
 path = /tmp/gpm-profiles/test5.profile
 path = /tmp/gpm-profiles/test1.profile
 
-`)
+`,
+			buf.String(),
+		)
+
+		t.Run("reflect from struct with shadows", func(t *testing.T) {
+			cfg := Empty(LoadOptions{
+				AllowShadows: true,
+			})
+			type ShadowStruct struct {
+				StringArray      []string    `ini:"sa,allowshadow"`
+				EmptyStringArrat []string    `ini:"empty,omitempty,allowshadow"`
+				Allowshadow      []string    `ini:"allowshadow,allowshadow"`
+				Dates            []time.Time `ini:",allowshadow"`
+				Places           []string    `ini:",allowshadow"`
+				Years            []int       `ini:",allowshadow"`
+				Numbers          []int64     `ini:",allowshadow"`
+				Ages             []uint      `ini:",allowshadow"`
+				Populations      []uint64    `ini:",allowshadow"`
+				Coordinates      []float64   `ini:",allowshadow"`
+				Flags            []bool      `ini:",allowshadow"`
+				None             []int       `ini:",allowshadow"`
+			}
+
+			shadow := &ShadowStruct{
+				StringArray: []string{"s1", "s2"},
+				Allowshadow: []string{"s3", "s4"},
+				Dates: []time.Time{time.Date(2020, 9, 12, 00, 00, 00, 651387237, time.UTC),
+					time.Date(2020, 9, 12, 00, 00, 00, 651387237, time.UTC)},
+				Places:      []string{"HangZhou", "Boston"},
+				Years:       []int{1993, 1994},
+				Numbers:     []int64{10010, 10086},
+				Ages:        []uint{18, 19},
+				Populations: []uint64{12345678, 98765432},
+				Coordinates: []float64{192.168, 10.11},
+				Flags:       []bool{true, false},
+				None:        []int{},
+			}
+
+			assert.NoError(t, ReflectFrom(cfg, shadow))
+
+			var buf bytes.Buffer
+			_, err := cfg.WriteTo(&buf)
+			require.NoError(t, err)
+			assert.Equal(t, `sa          = s1
+sa          = s2
+allowshadow = s3
+allowshadow = s4
+Dates       = 2020-09-12T00:00:00Z
+Places      = HangZhou
+Places      = Boston
+Years       = 1993
+Years       = 1994
+Numbers     = 10010
+Numbers     = 10086
+Ages        = 18
+Ages        = 19
+Populations = 12345678
+Populations = 98765432
+Coordinates = 192.168
+Coordinates = 10.11
+Flags       = true
+Flags       = false
+None        = 
+
+`,
+				buf.String(),
+			)
+		})
 	})
 }
 
@@ -649,17 +836,17 @@ type testMapper struct {
 }
 
 func Test_NameGetter(t *testing.T) {
-	Convey("Test name mappers", t, func() {
-		So(ini.MapToWithMapper(&testMapper{}, ini.TitleUnderscore, []byte("packag_name=ini")), ShouldBeNil)
+	t.Run("test name mappers", func(t *testing.T) {
+		assert.NoError(t, MapToWithMapper(&testMapper{}, TitleUnderscore, []byte("packag_name=ini")))
 
-		cfg, err := ini.Load([]byte("PACKAGE_NAME=ini"))
-		So(err, ShouldBeNil)
-		So(cfg, ShouldNotBeNil)
+		cfg, err := Load([]byte("PACKAGE_NAME=ini"))
+		require.NoError(t, err)
+		require.NotNil(t, cfg)
 
-		cfg.NameMapper = ini.SnackCase
+		cfg.NameMapper = SnackCase
 		tg := new(testMapper)
-		So(cfg.MapTo(tg), ShouldBeNil)
-		So(tg.PackageName, ShouldEqual, "ini")
+		assert.NoError(t, cfg.MapTo(tg))
+		assert.Equal(t, "ini", tg.PackageName)
 	})
 }
 
@@ -668,13 +855,13 @@ type testDurationStruct struct {
 }
 
 func Test_Duration(t *testing.T) {
-	Convey("Duration less than 16m50s", t, func() {
+	t.Run("duration less than 16m50s", func(t *testing.T) {
 		ds := new(testDurationStruct)
-		So(ini.MapTo(ds, []byte("Duration=16m49s")), ShouldBeNil)
+		assert.NoError(t, MapTo(ds, []byte("Duration=16m49s")))
 
 		dur, err := time.ParseDuration("16m49s")
-		So(err, ShouldBeNil)
-		So(ds.Duration.Seconds(), ShouldEqual, dur.Seconds())
+		require.NoError(t, err)
+		assert.Equal(t, dur.Seconds(), ds.Duration.Seconds())
 	})
 }
 
@@ -685,7 +872,7 @@ type Employer struct {
 
 type Employers []*Employer
 
-func (es Employers) ReflectINIStruct(f *ini.File) error {
+func (es Employers) ReflectINIStruct(f *File) error {
 	for _, e := range es {
 		f.Section(e.Name).Key("Title").SetValue(e.Title)
 	}
@@ -694,7 +881,7 @@ func (es Employers) ReflectINIStruct(f *ini.File) error {
 
 // Inspired by https://github.com/go-ini/ini/issues/199
 func Test_StructReflector(t *testing.T) {
-	Convey("Reflect with StructReflector interface", t, func() {
+	t.Run("reflect with StructReflector interface", func(t *testing.T) {
 		p := &struct {
 			FirstName string
 			Employer  Employers
@@ -712,14 +899,14 @@ func Test_StructReflector(t *testing.T) {
 			},
 		}
 
-		f := ini.Empty()
-		So(f.ReflectFrom(p), ShouldBeNil)
+		f := Empty()
+		assert.NoError(t, f.ReflectFrom(p))
 
 		var buf bytes.Buffer
 		_, err := f.WriteTo(&buf)
-		So(err, ShouldBeNil)
+		require.NoError(t, err)
 
-		So(buf.String(), ShouldEqual, `FirstName = Andrew
+		assert.Equal(t, `FirstName = Andrew
 
 [Employer "VMware"]
 Title = Staff II Engineer
@@ -727,6 +914,8 @@ Title = Staff II Engineer
 [Employer "EMC"]
 Title = Consultant Engineer
 
-`)
+`,
+			buf.String(),
+		)
 	})
 }
diff --git a/testdata/TestFile_WriteTo.golden b/testdata/TestFile_WriteTo.golden
index 1b28686..885924f 100644
--- a/testdata/TestFile_WriteTo.golden
+++ b/testdata/TestFile_WriteTo.golden
@@ -84,5 +84,5 @@ true                   = 2+3=5
 ADDRESS                = """404 road,
 NotFound, State, 50000"""
 two_lines              = how about continuation lines?
-lots_of_lines          = 1 2 3 4 
+lots_of_lines          = "1 2 3 4 "
 
diff --git a/testdata/multiline.ini b/testdata/multiline.ini
index c3f6ee4..5e6af11 100644
--- a/testdata/multiline.ini
+++ b/testdata/multiline.ini
@@ -5,8 +5,8 @@ value1 = some text here
 	
 
 value2 = there is an empty line above
-	that is not indented so it should not be part
-	of the value
+    that is not indented so it should not be part
+    of the value
 
 value3 = .
  
diff --git a/testdata/multiline_eof.ini b/testdata/multiline_eof.ini
new file mode 100644
index 0000000..1ef99ba
--- /dev/null
+++ b/testdata/multiline_eof.ini
@@ -0,0 +1,2 @@
+value1 = some text here
+	some more text here 2
\ No newline at end of file

Debdiff

[The following lists of changes regard files as different if they have different names, permissions or owners.]

Files in second set of .debs but not in first

-rw-r--r--  root/root   /usr/share/gocode/src/github.com/go-ini/ini/testdata/multiline_eof.ini

Files in first set of .debs but not in second

-rw-r--r--  root/root   /usr/share/gocode/src/github.com/go-ini/ini/ini_python_multiline_test.go

No differences were encountered in the control files

More details

Full run details