Misc config loading fixes

The main motivation behind this is simplicity and correctnes, but the new small config library is also faster:

```
BenchmarkDefaultConfigProvider/Viper-16         	  252418	      4546 ns/op	    2720 B/op	      30 allocs/op
BenchmarkDefaultConfigProvider/Custom-16        	  450756	      2651 ns/op	    1008 B/op	       6 allocs/op
```

Fixes #8633
Fixes #8618
Fixes #8630
Updates #8591
Closes #6680
Closes #5192
This commit is contained in:
Bjørn Erik Pedersen
2021-06-09 10:58:18 +02:00
parent a886dd53b8
commit d392893cd7
107 changed files with 2159 additions and 1060 deletions

View File

@@ -21,14 +21,12 @@ import (
"github.com/gohugoio/hugo/common/types"
qt "github.com/frankban/quicktest"
"github.com/spf13/viper"
)
func TestBuild(t *testing.T) {
c := qt.New(t)
v := viper.New()
v := New()
v.Set("build", map[string]interface{}{
"useResourceCacheWhen": "always",
})

113
config/compositeConfig.go Normal file
View File

@@ -0,0 +1,113 @@
// Copyright 2021 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package config
import (
"github.com/gohugoio/hugo/common/maps"
)
// NewCompositeConfig creates a new composite Provider with a read-only base
// and a writeable layer.
func NewCompositeConfig(base, layer Provider) Provider {
return &compositeConfig{
base: base,
layer: layer,
}
}
// compositeConfig contains a read only config base with
// a possibly writeable config layer on top.
type compositeConfig struct {
base Provider
layer Provider
}
func (c *compositeConfig) GetBool(key string) bool {
if c.layer.IsSet(key) {
return c.layer.GetBool(key)
}
return c.base.GetBool(key)
}
func (c *compositeConfig) GetInt(key string) int {
if c.layer.IsSet(key) {
return c.layer.GetInt(key)
}
return c.base.GetInt(key)
}
func (c *compositeConfig) Merge(key string, value interface{}) {
c.layer.Merge(key, value)
}
func (c *compositeConfig) GetParams(key string) maps.Params {
if c.layer.IsSet(key) {
return c.layer.GetParams(key)
}
return c.base.GetParams(key)
}
func (c *compositeConfig) GetStringMap(key string) map[string]interface{} {
if c.layer.IsSet(key) {
return c.layer.GetStringMap(key)
}
return c.base.GetStringMap(key)
}
func (c *compositeConfig) GetStringMapString(key string) map[string]string {
if c.layer.IsSet(key) {
return c.layer.GetStringMapString(key)
}
return c.base.GetStringMapString(key)
}
func (c *compositeConfig) GetStringSlice(key string) []string {
if c.layer.IsSet(key) {
return c.layer.GetStringSlice(key)
}
return c.base.GetStringSlice(key)
}
func (c *compositeConfig) Get(key string) interface{} {
if c.layer.IsSet(key) {
return c.layer.Get(key)
}
return c.base.Get(key)
}
func (c *compositeConfig) IsSet(key string) bool {
if c.layer.IsSet(key) {
return true
}
return c.base.IsSet(key)
}
func (c *compositeConfig) GetString(key string) string {
if c.layer.IsSet(key) {
return c.layer.GetString(key)
}
return c.base.GetString(key)
}
func (c *compositeConfig) Set(key string, value interface{}) {
c.layer.Set(key, value)
}
func (c *compositeConfig) WalkParams(walkFn func(params ...KeyParams) bool) {
panic("not supported")
}
func (c *compositeConfig) SetDefaultMergeStrategy() {
panic("not supported")
}

View File

@@ -0,0 +1,40 @@
// Copyright 2021 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package config
import (
"testing"
qt "github.com/frankban/quicktest"
)
func TestCompositeConfig(t *testing.T) {
c := qt.New(t)
c.Run("Set and get", func(c *qt.C) {
base, layer := New(), New()
cfg := NewCompositeConfig(base, layer)
layer.Set("a1", "av")
base.Set("b1", "bv")
cfg.Set("c1", "cv")
c.Assert(cfg.Get("a1"), qt.Equals, "av")
c.Assert(cfg.Get("b1"), qt.Equals, "bv")
c.Assert(cfg.Get("c1"), qt.Equals, "cv")
c.Assert(cfg.IsSet("c1"), qt.IsTrue)
c.Assert(layer.IsSet("c1"), qt.IsTrue)
c.Assert(base.IsSet("c1"), qt.IsFalse)
})
}

View File

@@ -20,7 +20,6 @@ import (
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/parser/metadecoders"
"github.com/spf13/afero"
"github.com/spf13/viper"
)
var (
@@ -43,15 +42,11 @@ func IsValidConfigFilename(filename string) bool {
// FromConfigString creates a config from the given YAML, JSON or TOML config. This is useful in tests.
func FromConfigString(config, configType string) (Provider, error) {
v := newViper()
m, err := readConfig(metadecoders.FormatFromString(configType), []byte(config))
if err != nil {
return nil, err
}
v.MergeConfigMap(m)
return v, nil
return NewFrom(m), nil
}
// FromFile loads the configuration from the given filename.
@@ -60,15 +55,7 @@ func FromFile(fs afero.Fs, filename string) (Provider, error) {
if err != nil {
return nil, err
}
v := newViper()
err = v.MergeConfigMap(m)
if err != nil {
return nil, err
}
return v, nil
return NewFrom(m), nil
}
// FromFileToMap is the same as FromFile, but it returns the config values
@@ -116,9 +103,3 @@ func init() {
func RenameKeys(m map[string]interface{}) {
keyAliases.Rename(m)
}
func newViper() *viper.Viper {
v := viper.New()
return v
}

View File

@@ -14,6 +14,7 @@
package config
import (
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/types"
)
@@ -22,11 +23,15 @@ type Provider interface {
GetString(key string) string
GetInt(key string) int
GetBool(key string) bool
GetParams(key string) maps.Params
GetStringMap(key string) map[string]interface{}
GetStringMapString(key string) map[string]string
GetStringSlice(key string) []string
Get(key string) interface{}
Set(key string, value interface{})
Merge(key string, value interface{})
SetDefaultMergeStrategy()
WalkParams(walkFn func(params ...KeyParams) bool)
IsSet(key string) bool
}

View File

@@ -17,12 +17,11 @@ import (
"testing"
qt "github.com/frankban/quicktest"
"github.com/spf13/viper"
)
func TestGetStringSlicePreserveString(t *testing.T) {
c := qt.New(t)
cfg := viper.New()
cfg := New()
s := "This is a string"
sSlice := []string{"This", "is", "a", "slice"}

View File

@@ -0,0 +1,372 @@
// Copyright 2021 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package config
import (
"fmt"
"sort"
"strings"
"sync"
"github.com/spf13/cast"
"github.com/gohugoio/hugo/common/maps"
)
var (
// ConfigRootKeysSet contains all of the config map root keys.
// TODO(bep) use this for something (docs etc.)
ConfigRootKeysSet = map[string]bool{
"build": true,
"caches": true,
"frontmatter": true,
"languages": true,
"imaging": true,
"markup": true,
"mediatypes": true,
"menus": true,
"minify": true,
"module": true,
"outputformats": true,
"params": true,
"permalinks": true,
"related": true,
"sitemap": true,
"taxonomies": true,
}
// ConfigRootKeys is a sorted version of ConfigRootKeysSet.
ConfigRootKeys []string
)
func init() {
for k := range ConfigRootKeysSet {
ConfigRootKeys = append(ConfigRootKeys, k)
}
sort.Strings(ConfigRootKeys)
}
// New creates a Provider backed by an empty maps.Params.
func New() Provider {
return &defaultConfigProvider{
root: make(maps.Params),
}
}
// NewFrom creates a Provider backed by params.
func NewFrom(params maps.Params) Provider {
maps.PrepareParams(params)
return &defaultConfigProvider{
root: params,
}
}
// defaultConfigProvider is a Provider backed by a map where all keys are lower case.
// All methods are thread safe.
type defaultConfigProvider struct {
mu sync.RWMutex
root maps.Params
keyCache sync.Map
}
func (c *defaultConfigProvider) Get(k string) interface{} {
if k == "" {
return c.root
}
c.mu.RLock()
key, m := c.getNestedKeyAndMap(strings.ToLower(k), false)
if m == nil {
return nil
}
v := m[key]
c.mu.RUnlock()
return v
}
func (c *defaultConfigProvider) GetBool(k string) bool {
v := c.Get(k)
return cast.ToBool(v)
}
func (c *defaultConfigProvider) GetInt(k string) int {
v := c.Get(k)
return cast.ToInt(v)
}
func (c *defaultConfigProvider) IsSet(k string) bool {
var found bool
c.mu.RLock()
key, m := c.getNestedKeyAndMap(strings.ToLower(k), false)
if m != nil {
_, found = m[key]
}
c.mu.RUnlock()
return found
}
func (c *defaultConfigProvider) GetString(k string) string {
v := c.Get(k)
return cast.ToString(v)
}
func (c *defaultConfigProvider) GetParams(k string) maps.Params {
v := c.Get(k)
if v == nil {
return nil
}
return v.(maps.Params)
}
func (c *defaultConfigProvider) GetStringMap(k string) map[string]interface{} {
v := c.Get(k)
return maps.ToStringMap(v)
}
func (c *defaultConfigProvider) GetStringMapString(k string) map[string]string {
v := c.Get(k)
return maps.ToStringMapString(v)
}
func (c *defaultConfigProvider) GetStringSlice(k string) []string {
v := c.Get(k)
return cast.ToStringSlice(v)
}
func (c *defaultConfigProvider) Set(k string, v interface{}) {
c.mu.Lock()
defer c.mu.Unlock()
k = strings.ToLower(k)
if k == "" {
if p, ok := maps.ToParamsAndPrepare(v); ok {
// Set the values directly in root.
c.root.Set(p)
} else {
c.root[k] = v
}
return
}
switch vv := v.(type) {
case map[string]interface{}:
var p maps.Params = vv
v = p
maps.PrepareParams(p)
}
key, m := c.getNestedKeyAndMap(k, true)
if existing, found := m[key]; found {
if p1, ok := existing.(maps.Params); ok {
if p2, ok := v.(maps.Params); ok {
p1.Set(p2)
return
}
}
}
m[key] = v
}
func (c *defaultConfigProvider) Merge(k string, v interface{}) {
c.mu.Lock()
defer c.mu.Unlock()
k = strings.ToLower(k)
if k == "" {
rs, f := c.root.GetMergeStrategy()
if f && rs == maps.ParamsMergeStrategyNone {
// The user has set a "no merge" strategy on this,
// nothing more to do.
return
}
if p, ok := maps.ToParamsAndPrepare(v); ok {
// As there may be keys in p not in root, we need to handle
// those as a special case.
for kk, vv := range p {
if pp, ok := vv.(maps.Params); ok {
if ppp, ok := c.root[kk]; ok {
ppp.(maps.Params).Merge(pp)
} else {
// We need to use the default merge strategy for
// this key.
np := make(maps.Params)
strategy := c.determineMergeStrategy(KeyParams{Key: "", Params: c.root}, KeyParams{Key: kk, Params: np})
np.SetDefaultMergeStrategy(strategy)
np.Merge(pp)
if len(np) > 0 {
c.root[kk] = np
}
}
}
}
// Merge the rest.
c.root.Merge(p)
} else {
panic(fmt.Sprintf("unsupported type %T received in Merge", v))
}
return
}
switch vv := v.(type) {
case map[string]interface{}:
var p maps.Params = vv
v = p
maps.PrepareParams(p)
}
key, m := c.getNestedKeyAndMap(k, true)
if existing, found := m[key]; found {
if p1, ok := existing.(maps.Params); ok {
if p2, ok := v.(maps.Params); ok {
p1.Merge(p2)
}
}
} else {
m[key] = v
}
}
func (c *defaultConfigProvider) WalkParams(walkFn func(params ...KeyParams) bool) {
var walk func(params ...KeyParams)
walk = func(params ...KeyParams) {
if walkFn(params...) {
return
}
p1 := params[len(params)-1]
i := len(params)
for k, v := range p1.Params {
if p2, ok := v.(maps.Params); ok {
paramsplus1 := make([]KeyParams, i+1)
copy(paramsplus1, params)
paramsplus1[i] = KeyParams{Key: k, Params: p2}
walk(paramsplus1...)
}
}
}
walk(KeyParams{Key: "", Params: c.root})
}
func (c *defaultConfigProvider) determineMergeStrategy(params ...KeyParams) maps.ParamsMergeStrategy {
if len(params) == 0 {
return maps.ParamsMergeStrategyNone
}
var (
strategy maps.ParamsMergeStrategy
prevIsRoot bool
curr = params[len(params)-1]
)
if len(params) > 1 {
prev := params[len(params)-2]
prevIsRoot = prev.Key == ""
// Inherit from parent (but not from the root unless it's set by user).
s, found := prev.Params.GetMergeStrategy()
if !prevIsRoot && !found {
panic("invalid state, merge strategy not set on parent")
}
if found || !prevIsRoot {
strategy = s
}
}
switch curr.Key {
case "":
// Don't set a merge strategy on the root unless set by user.
// This will be handled as a special case.
case "params":
strategy = maps.ParamsMergeStrategyDeep
case "outputformats", "mediatypes":
if prevIsRoot {
strategy = maps.ParamsMergeStrategyShallow
}
case "menus":
isMenuKey := prevIsRoot
if !isMenuKey {
// Can also be set below languages.
// root > languages > en > menus
if len(params) == 4 && params[1].Key == "languages" {
isMenuKey = true
}
}
if isMenuKey {
strategy = maps.ParamsMergeStrategyShallow
}
default:
if strategy == "" {
strategy = maps.ParamsMergeStrategyNone
}
}
return strategy
}
type KeyParams struct {
Key string
Params maps.Params
}
func (c *defaultConfigProvider) SetDefaultMergeStrategy() {
c.WalkParams(func(params ...KeyParams) bool {
if len(params) == 0 {
return false
}
p := params[len(params)-1].Params
var found bool
if _, found = p.GetMergeStrategy(); found {
// Set by user.
return false
}
strategy := c.determineMergeStrategy(params...)
if strategy != "" {
p.SetDefaultMergeStrategy(strategy)
}
return false
})
}
func (c *defaultConfigProvider) getNestedKeyAndMap(key string, create bool) (string, maps.Params) {
var parts []string
v, ok := c.keyCache.Load(key)
if ok {
parts = v.([]string)
} else {
parts = strings.Split(key, ".")
c.keyCache.Store(key, parts)
}
current := c.root
for i := 0; i < len(parts)-1; i++ {
next, found := current[parts[i]]
if !found {
if create {
next = make(maps.Params)
current[parts[i]] = next
} else {
return "", nil
}
}
current = next.(maps.Params)
}
return parts[len(parts)-1], current
}

View File

@@ -0,0 +1,315 @@
// Copyright 2021 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package config
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
"testing"
"github.com/spf13/viper"
"github.com/gohugoio/hugo/common/para"
"github.com/gohugoio/hugo/common/maps"
qt "github.com/frankban/quicktest"
)
func TestDefaultConfigProvider(t *testing.T) {
c := qt.New(t)
c.Run("Set and get", func(c *qt.C) {
cfg := New()
var k string
var v interface{}
k, v = "foo", "bar"
cfg.Set(k, v)
c.Assert(cfg.Get(k), qt.Equals, v)
c.Assert(cfg.Get(strings.ToUpper(k)), qt.Equals, v)
c.Assert(cfg.GetString(k), qt.Equals, v)
k, v = "foo", 42
cfg.Set(k, v)
c.Assert(cfg.Get(k), qt.Equals, v)
c.Assert(cfg.GetInt(k), qt.Equals, v)
c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{
"foo": 42,
})
})
c.Run("Set and get map", func(c *qt.C) {
cfg := New()
cfg.Set("foo", map[string]interface{}{
"bar": "baz",
})
c.Assert(cfg.Get("foo"), qt.DeepEquals, maps.Params{
"bar": "baz",
})
c.Assert(cfg.GetStringMap("foo"), qt.DeepEquals, map[string]interface{}{"bar": string("baz")})
c.Assert(cfg.GetStringMapString("foo"), qt.DeepEquals, map[string]string{"bar": string("baz")})
})
c.Run("Set and get nested", func(c *qt.C) {
cfg := New()
cfg.Set("a", map[string]interface{}{
"B": "bv",
})
cfg.Set("a.c", "cv")
c.Assert(cfg.Get("a"), qt.DeepEquals, maps.Params{
"b": "bv",
"c": "cv",
})
c.Assert(cfg.Get("a.c"), qt.Equals, "cv")
cfg.Set("b.a", "av")
c.Assert(cfg.Get("b"), qt.DeepEquals, maps.Params{
"a": "av",
})
cfg.Set("b", map[string]interface{}{
"b": "bv",
})
c.Assert(cfg.Get("b"), qt.DeepEquals, maps.Params{
"a": "av",
"b": "bv",
})
cfg = New()
cfg.Set("a", "av")
cfg.Set("", map[string]interface{}{
"a": "av2",
"b": "bv2",
})
c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{
"a": "av2",
"b": "bv2",
})
cfg = New()
cfg.Set("a", "av")
cfg.Set("", map[string]interface{}{
"b": "bv2",
})
c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{
"a": "av",
"b": "bv2",
})
cfg = New()
cfg.Set("", map[string]interface{}{
"foo": map[string]interface{}{
"a": "av",
},
})
cfg.Set("", map[string]interface{}{
"foo": map[string]interface{}{
"b": "bv2",
},
})
c.Assert(cfg.Get("foo"), qt.DeepEquals, maps.Params{
"a": "av",
"b": "bv2",
})
})
c.Run("Merge default strategy", func(c *qt.C) {
cfg := New()
cfg.Set("a", map[string]interface{}{
"B": "bv",
})
cfg.Merge("a", map[string]interface{}{
"B": "bv2",
"c": "cv2",
})
c.Assert(cfg.Get("a"), qt.DeepEquals, maps.Params{
"b": "bv",
"c": "cv2",
})
cfg = New()
cfg.Set("a", "av")
cfg.Merge("", map[string]interface{}{
"a": "av2",
"b": "bv2",
})
c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{
"a": "av",
"b": "bv2",
})
})
c.Run("Merge shallow", func(c *qt.C) {
cfg := New()
cfg.Set("a", map[string]interface{}{
"_merge": "shallow",
"B": "bv",
"c": map[string]interface{}{
"b": "bv",
},
})
cfg.Merge("a", map[string]interface{}{
"c": map[string]interface{}{
"d": "dv2",
},
"e": "ev2",
})
c.Assert(cfg.Get("a"), qt.DeepEquals, maps.Params{
"e": "ev2",
"_merge": maps.ParamsMergeStrategyShallow,
"b": "bv",
"c": maps.Params{
"b": "bv",
},
})
})
c.Run("IsSet", func(c *qt.C) {
cfg := New()
cfg.Set("a", map[string]interface{}{
"B": "bv",
})
c.Assert(cfg.IsSet("A"), qt.IsTrue)
c.Assert(cfg.IsSet("a.b"), qt.IsTrue)
c.Assert(cfg.IsSet("z"), qt.IsFalse)
})
c.Run("Para", func(c *qt.C) {
cfg := New()
p := para.New(4)
r, _ := p.Start(context.Background())
setAndGet := func(k string, v int) error {
vs := strconv.Itoa(v)
cfg.Set(k, v)
err := errors.New("get failed")
if cfg.Get(k) != v {
return err
}
if cfg.GetInt(k) != v {
return err
}
if cfg.GetString(k) != vs {
return err
}
if !cfg.IsSet(k) {
return err
}
return nil
}
for i := 0; i < 20; i++ {
i := i
r.Run(func() error {
const v = 42
k := fmt.Sprintf("k%d", i)
if err := setAndGet(k, v); err != nil {
return err
}
m := maps.Params{
"new": 42,
}
cfg.Merge("", m)
return nil
})
}
c.Assert(r.Wait(), qt.IsNil)
})
}
func BenchmarkDefaultConfigProvider(b *testing.B) {
type cfger interface {
Get(key string) interface{}
Set(key string, value interface{})
IsSet(key string) bool
}
newMap := func() map[string]interface{} {
return map[string]interface{}{
"a": map[string]interface{}{
"b": map[string]interface{}{
"c": 32,
"d": 43,
},
},
"b": 62,
}
}
runMethods := func(b *testing.B, cfg cfger) {
m := newMap()
cfg.Set("mymap", m)
cfg.Set("num", 32)
if !(cfg.IsSet("mymap") && cfg.IsSet("mymap.a") && cfg.IsSet("mymap.a.b") && cfg.IsSet("mymap.a.b.c")) {
b.Fatal("IsSet failed")
}
if cfg.Get("num") != 32 {
b.Fatal("Get failed")
}
if cfg.Get("mymap.a.b.c") != 32 {
b.Fatal("Get failed")
}
}
b.Run("Viper", func(b *testing.B) {
v := viper.New()
for i := 0; i < b.N; i++ {
runMethods(b, v)
}
})
b.Run("Custom", func(b *testing.B) {
cfg := New()
for i := 0; i < b.N; i++ {
runMethods(b, cfg)
}
})
}

45
config/docshelper.go Normal file
View File

@@ -0,0 +1,45 @@
// Copyright 2021 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package config
import (
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/docshelper"
)
// This is is just some helpers used to create some JSON used in the Hugo docs.
func init() {
docsProvider := func() docshelper.DocProvider {
cfg := New()
for _, configRoot := range ConfigRootKeys {
cfg.Set(configRoot, make(maps.Params))
}
lang := maps.Params{
"en": maps.Params{
"menus": maps.Params{},
"params": maps.Params{},
},
}
cfg.Set("languages", lang)
cfg.SetDefaultMergeStrategy()
configHelpers := map[string]interface{}{
"mergeStrategy": cfg.Get(""),
}
return docshelper.DocProvider{"config": configHelpers}
}
docshelper.AddDocProviderFunc(docsProvider)
}

View File

@@ -18,7 +18,6 @@ import (
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/config"
"github.com/spf13/viper"
)
func TestDecodeConfigFromTOML(t *testing.T) {
@@ -94,7 +93,7 @@ PrivacyENhanced = true
func TestDecodeConfigDefault(t *testing.T) {
c := qt.New(t)
pc, err := DecodeConfig(viper.New())
pc, err := DecodeConfig(config.New())
c.Assert(err, qt.IsNil)
c.Assert(pc, qt.Not(qt.IsNil))
c.Assert(pc.YouTube.PrivacyEnhanced, qt.Equals, false)

View File

@@ -18,7 +18,7 @@ import (
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/config"
"github.com/spf13/viper"
)
func TestDecodeConfigFromTOML(t *testing.T) {
@@ -55,7 +55,7 @@ disableInlineCSS = true
func TestUseSettingsFromRootIfSet(t *testing.T) {
c := qt.New(t)
cfg := viper.New()
cfg := config.New()
cfg.Set("disqusShortname", "root_short")
cfg.Set("googleAnalytics", "ga_root")