all: Refactor to nonglobal Viper, i18n etc.

This is a final rewrite that removes all the global state in Hugo, which also enables
the use if `t.Parallel` in tests.

Updates #2701
Fixes #3016
This commit is contained in:
Bjørn Erik Pedersen
2017-02-05 10:20:06 +07:00
parent e34af6ee30
commit 93ca7c9e95
99 changed files with 2843 additions and 2458 deletions

View File

@@ -21,6 +21,8 @@ import (
"path/filepath"
"strings"
"sync"
"github.com/eknkc/amber"
"github.com/spf13/afero"
bp "github.com/spf13/hugo/bufferpool"
@@ -31,6 +33,9 @@ import (
// TODO(bep) globals get rid of the rest of the jww.ERR etc.
// Protecting global map access (Amber)
var amberMu sync.Mutex
type templateErr struct {
name string
err error
@@ -132,6 +137,7 @@ func (t *GoHTMLTemplate) initFuncs(d *deps.Deps) {
t.amberFuncMap = template.FuncMap{}
amberMu.Lock()
for k, v := range amber.FuncMap {
t.amberFuncMap[k] = v
}
@@ -143,6 +149,7 @@ func (t *GoHTMLTemplate) initFuncs(d *deps.Deps) {
panic("should never be invoked")
}
}
amberMu.Unlock()
}
@@ -362,7 +369,9 @@ func (t *GoHTMLTemplate) AddTemplateFile(name, baseTemplatePath, path string) er
return err
}
amberMu.Lock()
templ, err := t.CompileAmberWithTemplate(b, path, t.New(templateName))
amberMu.Unlock()
if err != nil {
return err
}
@@ -482,11 +491,11 @@ func (t *GoHTMLTemplate) loadTemplates(absPath string, prefix string) {
}
if needsBase {
layoutDir := helpers.GetLayoutDirPath()
layoutDir := t.PathSpec.GetLayoutDirPath()
currBaseFilename := fmt.Sprintf("%s-%s", helpers.Filename(path), baseFileName)
templateDir := filepath.Dir(path)
themeDir := filepath.Join(helpers.GetThemeDir())
relativeThemeLayoutsDir := filepath.Join(helpers.GetRelativeThemeDir(), "layouts")
themeDir := filepath.Join(t.PathSpec.GetThemeDir())
relativeThemeLayoutsDir := filepath.Join(t.PathSpec.GetRelativeThemeDir(), "layouts")
var baseTemplatedDir string

View File

@@ -113,6 +113,7 @@ F3: {{ Echo (printf "themes/%s-theme" .Site.Params.LOWER) }}
)
func TestParamsKeysToLower(t *testing.T) {
t.Parallel()
require.Error(t, applyTemplateTransformers(nil))
@@ -190,6 +191,7 @@ func BenchmarkTemplateParamsKeysToLower(b *testing.B) {
}
func TestParamsKeysToLowerVars(t *testing.T) {
t.Parallel()
var (
ctx = map[string]interface{}{
"Params": map[string]interface{}{
@@ -227,6 +229,7 @@ Blue: {{ $__amber_1.Blue}}
}
func TestParamsKeysToLowerInBlockTemplate(t *testing.T) {
t.Parallel()
var (
ctx = map[string]interface{}{

View File

@@ -21,6 +21,7 @@ import (
)
func TestTruncate(t *testing.T) {
t.Parallel()
var err error
cases := []struct {
v1 interface{}

View File

@@ -46,7 +46,6 @@ import (
"github.com/spf13/hugo/deps"
"github.com/spf13/hugo/helpers"
jww "github.com/spf13/jwalterweatherman"
"github.com/spf13/viper"
// Importing image codecs for image.DecodeConfig
_ "image/gif"
@@ -58,7 +57,6 @@ import (
type templateFuncster struct {
funcMap template.FuncMap
cachedPartials partialCache
*deps.Deps
}
@@ -398,6 +396,7 @@ func intersect(l1, l2 interface{}) (interface{}, error) {
}
// ResetCaches resets all caches that might be used during build.
// TODO(bep) globals move image config cache to funcster
func ResetCaches() {
resetImageConfigCache()
}
@@ -1357,31 +1356,29 @@ func returnWhenSet(a, k interface{}) interface{} {
}
// highlight returns an HTML string with syntax highlighting applied.
func highlight(in interface{}, lang, opts string) (template.HTML, error) {
func (t *templateFuncster) highlight(in interface{}, lang, opts string) (template.HTML, error) {
str, err := cast.ToStringE(in)
if err != nil {
return "", err
}
return template.HTML(helpers.Highlight(html.UnescapeString(str), lang, opts)), nil
return template.HTML(helpers.Highlight(t.Cfg, html.UnescapeString(str), lang, opts)), nil
}
var markdownTrimPrefix = []byte("<p>")
var markdownTrimSuffix = []byte("</p>\n")
// markdownify renders a given string from Markdown to HTML.
func markdownify(in interface{}) (template.HTML, error) {
func (t *templateFuncster) markdownify(in interface{}) (template.HTML, error) {
text, err := cast.ToStringE(in)
if err != nil {
return "", err
}
language := viper.Get("currentContentLanguage").(*helpers.Language)
m := helpers.RenderBytes(&helpers.RenderingContext{
ConfigProvider: language,
Content: []byte(text), PageFmt: "markdown"})
m := t.ContentSpec.RenderBytes(&helpers.RenderingContext{
Cfg: t.Cfg,
Content: []byte(text), PageFmt: "markdown"})
m = bytes.TrimPrefix(m, markdownTrimPrefix)
m = bytes.TrimSuffix(m, markdownTrimSuffix)
return template.HTML(m), nil
@@ -2143,7 +2140,7 @@ func (t *templateFuncster) initFuncMap() {
"getenv": getenv,
"gt": gt,
"hasPrefix": hasPrefix,
"highlight": highlight,
"highlight": t.highlight,
"htmlEscape": htmlEscape,
"htmlUnescape": htmlUnescape,
"humanize": humanize,
@@ -2159,7 +2156,7 @@ func (t *templateFuncster) initFuncMap() {
"le": le,
"lower": lower,
"lt": lt,
"markdownify": markdownify,
"markdownify": t.markdownify,
"md5": md5,
"mod": mod,
"modBool": modBool,
@@ -2211,8 +2208,8 @@ func (t *templateFuncster) initFuncMap() {
"upper": upper,
"urlize": t.PathSpec.URLize,
"where": where,
"i18n": i18nTranslate,
"T": i18nTranslate,
"i18n": t.Translate,
"T": t.Translate,
}
t.funcMap = funcMap

View File

@@ -42,7 +42,9 @@ import (
"github.com/spf13/afero"
"github.com/spf13/cast"
"github.com/spf13/hugo/config"
"github.com/spf13/hugo/hugofs"
"github.com/spf13/hugo/i18n"
jww "github.com/spf13/jwalterweatherman"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
@@ -53,12 +55,16 @@ var (
logger = jww.NewNotepad(jww.LevelFatal, jww.LevelFatal, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime)
)
func newDefaultDepsCfg() deps.DepsCfg {
func newDepsConfig(cfg config.Provider) deps.DepsCfg {
l := helpers.NewLanguage("en", cfg)
l.Set("i18nDir", "i18n")
return deps.DepsCfg{
Language: helpers.NewLanguage("en"),
Fs: hugofs.NewMem(),
Logger: logger,
TemplateProvider: DefaultTemplateProvider,
Language: l,
Cfg: cfg,
Fs: hugofs.NewMem(l),
Logger: logger,
TemplateProvider: DefaultTemplateProvider,
TranslationProvider: i18n.NewTranslationProvider(),
}
}
@@ -88,22 +94,17 @@ func tstIsLt(tp tstCompareType) bool {
return tp == tstLt || tp == tstLe
}
func tstInitTemplates() {
viper.Set("CurrentContentLanguage", helpers.NewLanguage("en"))
helpers.ResetConfigProvider()
}
func TestFuncsInTemplate(t *testing.T) {
testReset()
t.Parallel()
workingDir := "/home/hugo"
viper.Set("workingDir", workingDir)
viper.Set("currentContentLanguage", helpers.NewDefaultLanguage())
viper.Set("multilingual", true)
v := viper.New()
fs := hugofs.NewMem()
v.Set("workingDir", workingDir)
v.Set("multilingual", true)
fs := hugofs.NewMem(v)
afero.WriteFile(fs.Source, filepath.Join(workingDir, "README.txt"), []byte("Hugo Rocks!"), 0755)
@@ -268,11 +269,10 @@ urlize: bat-man
data.Section = "blog"
data.Params = map[string]interface{}{"langCode": "en"}
viper.Set("baseURL", "http://mysite.com/hugo/")
v.Set("baseURL", "http://mysite.com/hugo/")
v.Set("CurrentContentLanguage", helpers.NewLanguage("en", v))
tstInitTemplates()
config := newDefaultDepsCfg()
config := newDepsConfig(v)
config.WithTemplate = func(templ tplapi.Template) error {
if _, err := templ.New("test").Parse(in); err != nil {
t.Fatal("Got error on parse", err)
@@ -282,7 +282,7 @@ urlize: bat-man
config.Fs = fs
d := deps.New(config)
if err := d.LoadTemplates(); err != nil {
if err := d.LoadResources(); err != nil {
t.Fatal(err)
}
@@ -300,6 +300,7 @@ urlize: bat-man
}
func TestCompare(t *testing.T) {
t.Parallel()
for _, this := range []struct {
tstCompareType
funcUnderTest func(a, b interface{}) bool
@@ -370,6 +371,7 @@ func doTestCompare(t *testing.T, tp tstCompareType, funcUnderTest func(a, b inte
}
func TestMod(t *testing.T) {
t.Parallel()
for i, this := range []struct {
a interface{}
b interface{}
@@ -405,6 +407,7 @@ func TestMod(t *testing.T) {
}
func TestModBool(t *testing.T) {
t.Parallel()
for i, this := range []struct {
a interface{}
b interface{}
@@ -445,6 +448,7 @@ func TestModBool(t *testing.T) {
}
func TestFirst(t *testing.T) {
t.Parallel()
for i, this := range []struct {
count interface{}
sequence interface{}
@@ -480,6 +484,7 @@ func TestFirst(t *testing.T) {
}
func TestLast(t *testing.T) {
t.Parallel()
for i, this := range []struct {
count interface{}
sequence interface{}
@@ -515,6 +520,7 @@ func TestLast(t *testing.T) {
}
func TestAfter(t *testing.T) {
t.Parallel()
for i, this := range []struct {
count interface{}
sequence interface{}
@@ -550,6 +556,7 @@ func TestAfter(t *testing.T) {
}
func TestShuffleInputAndOutputFormat(t *testing.T) {
t.Parallel()
for i, this := range []struct {
sequence interface{}
success bool
@@ -588,6 +595,7 @@ func TestShuffleInputAndOutputFormat(t *testing.T) {
}
func TestShuffleRandomising(t *testing.T) {
t.Parallel()
// Note that this test can fail with false negative result if the shuffle
// of the sequence happens to be the same as the original sequence. However
// the propability of the event is 10^-158 which is negligible.
@@ -615,6 +623,7 @@ func TestShuffleRandomising(t *testing.T) {
}
func TestDictionary(t *testing.T) {
t.Parallel()
for i, this := range []struct {
v1 []interface{}
expecterr bool
@@ -647,13 +656,15 @@ func blankImage(width, height int) []byte {
}
func TestImageConfig(t *testing.T) {
testReset()
t.Parallel()
workingDir := "/home/hugo"
viper.Set("workingDir", workingDir)
v := viper.New()
f := newTestFuncster()
v.Set("workingDir", workingDir)
f := newTestFuncsterWithViper(v)
for i, this := range []struct {
resetCache bool
@@ -754,6 +765,7 @@ func TestImageConfig(t *testing.T) {
}
func TestIn(t *testing.T) {
t.Parallel()
for i, this := range []struct {
v1 interface{}
v2 interface{}
@@ -783,6 +795,7 @@ func TestIn(t *testing.T) {
}
func TestSlicestr(t *testing.T) {
t.Parallel()
var err error
for i, this := range []struct {
v1 interface{}
@@ -848,6 +861,7 @@ func TestSlicestr(t *testing.T) {
}
func TestHasPrefix(t *testing.T) {
t.Parallel()
cases := []struct {
s interface{}
prefix interface{}
@@ -875,6 +889,7 @@ func TestHasPrefix(t *testing.T) {
}
func TestSubstr(t *testing.T) {
t.Parallel()
var err error
var n int
for i, this := range []struct {
@@ -952,6 +967,7 @@ func TestSubstr(t *testing.T) {
}
func TestSplit(t *testing.T) {
t.Parallel()
for i, this := range []struct {
v1 interface{}
v2 string
@@ -982,6 +998,7 @@ func TestSplit(t *testing.T) {
}
func TestIntersect(t *testing.T) {
t.Parallel()
for i, this := range []struct {
sequence1 interface{}
sequence2 interface{}
@@ -1025,6 +1042,7 @@ func TestIntersect(t *testing.T) {
}
func TestIsSet(t *testing.T) {
t.Parallel()
aSlice := []interface{}{1, 2, 3, 5}
aMap := map[string]interface{}{"a": 1, "b": 2}
@@ -1074,6 +1092,7 @@ type TstX struct {
}
func TestTimeUnix(t *testing.T) {
t.Parallel()
var sec int64 = 1234567890
tv := reflect.ValueOf(time.Unix(sec, 0))
i := 1
@@ -1096,6 +1115,7 @@ func TestTimeUnix(t *testing.T) {
}
func TestEvaluateSubElem(t *testing.T) {
t.Parallel()
tstx := TstX{A: "foo", B: "bar"}
var inner struct {
S fmt.Stringer
@@ -1146,6 +1166,7 @@ func TestEvaluateSubElem(t *testing.T) {
}
func TestCheckCondition(t *testing.T) {
t.Parallel()
type expect struct {
result bool
isError bool
@@ -1266,6 +1287,7 @@ func TestCheckCondition(t *testing.T) {
}
func TestWhere(t *testing.T) {
t.Parallel()
type Mid struct {
Tst TstX
@@ -1671,6 +1693,7 @@ func TestWhere(t *testing.T) {
}
func TestDelimit(t *testing.T) {
t.Parallel()
for i, this := range []struct {
sequence interface{}
delimiter interface{}
@@ -1720,6 +1743,7 @@ func TestDelimit(t *testing.T) {
}
func TestSort(t *testing.T) {
t.Parallel()
type ts struct {
MyInt int
MyFloat float64
@@ -1932,6 +1956,7 @@ func TestSort(t *testing.T) {
}
func TestReturnWhenSet(t *testing.T) {
t.Parallel()
for i, this := range []struct {
data interface{}
key interface{}
@@ -1957,7 +1982,10 @@ func TestReturnWhenSet(t *testing.T) {
}
func TestMarkdownify(t *testing.T) {
viper.Set("currentContentLanguage", helpers.NewDefaultLanguage())
t.Parallel()
v := viper.New()
f := newTestFuncsterWithViper(v)
for i, this := range []struct {
in interface{}
@@ -1966,7 +1994,7 @@ func TestMarkdownify(t *testing.T) {
{"Hello **World!**", template.HTML("Hello <strong>World!</strong>")},
{[]byte("Hello Bytes **World!**"), template.HTML("Hello Bytes <strong>World!</strong>")},
} {
result, err := markdownify(this.in)
result, err := f.markdownify(this.in)
if err != nil {
t.Fatalf("[%d] unexpected error in markdownify: %s", i, err)
}
@@ -1975,12 +2003,13 @@ func TestMarkdownify(t *testing.T) {
}
}
if _, err := markdownify(t); err == nil {
if _, err := f.markdownify(t); err == nil {
t.Fatalf("markdownify should have errored")
}
}
func TestApply(t *testing.T) {
t.Parallel()
f := newTestFuncster()
@@ -2024,6 +2053,7 @@ func TestApply(t *testing.T) {
}
func TestChomp(t *testing.T) {
t.Parallel()
base := "\n This is\na story "
for i, item := range []string{
"\n", "\n\n",
@@ -2046,6 +2076,7 @@ func TestChomp(t *testing.T) {
}
func TestLower(t *testing.T) {
t.Parallel()
cases := []struct {
s interface{}
want string
@@ -2069,6 +2100,7 @@ func TestLower(t *testing.T) {
}
func TestTitle(t *testing.T) {
t.Parallel()
cases := []struct {
s interface{}
want string
@@ -2092,6 +2124,7 @@ func TestTitle(t *testing.T) {
}
func TestUpper(t *testing.T) {
t.Parallel()
cases := []struct {
s interface{}
want string
@@ -2115,8 +2148,12 @@ func TestUpper(t *testing.T) {
}
func TestHighlight(t *testing.T) {
t.Parallel()
code := "func boo() {}"
highlighted, err := highlight(code, "go", "")
f := newTestFuncster()
highlighted, err := f.highlight(code, "go", "")
if err != nil {
t.Fatal("Highlight returned error:", err)
@@ -2127,7 +2164,7 @@ func TestHighlight(t *testing.T) {
t.Errorf("Highlight mismatch, got %v", highlighted)
}
_, err = highlight(t, "go", "")
_, err = f.highlight(t, "go", "")
if err == nil {
t.Error("Expected highlight error")
@@ -2135,6 +2172,7 @@ func TestHighlight(t *testing.T) {
}
func TestInflect(t *testing.T) {
t.Parallel()
for i, this := range []struct {
inflectFunc func(i interface{}) (string, error)
in interface{}
@@ -2169,6 +2207,7 @@ func TestInflect(t *testing.T) {
}
func TestCounterFuncs(t *testing.T) {
t.Parallel()
for i, this := range []struct {
countFunc func(i interface{}) (int, error)
in string
@@ -2195,6 +2234,7 @@ func TestCounterFuncs(t *testing.T) {
}
func TestReplace(t *testing.T) {
t.Parallel()
v, _ := replace("aab", "a", "b")
assert.Equal(t, "bbb", v)
v, _ = replace("11a11", 1, 2)
@@ -2210,6 +2250,7 @@ func TestReplace(t *testing.T) {
}
func TestReplaceRE(t *testing.T) {
t.Parallel()
for i, val := range []struct {
pattern interface{}
repl interface{}
@@ -2234,6 +2275,7 @@ func TestReplaceRE(t *testing.T) {
}
func TestFindRE(t *testing.T) {
t.Parallel()
for i, this := range []struct {
expr string
content interface{}
@@ -2264,6 +2306,7 @@ func TestFindRE(t *testing.T) {
}
func TestTrim(t *testing.T) {
t.Parallel()
for i, this := range []struct {
v1 interface{}
@@ -2294,6 +2337,7 @@ func TestTrim(t *testing.T) {
}
func TestDateFormat(t *testing.T) {
t.Parallel()
for i, this := range []struct {
layout string
value interface{}
@@ -2328,6 +2372,7 @@ func TestDateFormat(t *testing.T) {
}
func TestDefaultFunc(t *testing.T) {
t.Parallel()
then := time.Now()
now := time.Now()
@@ -2385,6 +2430,7 @@ func TestDefaultFunc(t *testing.T) {
}
func TestDefault(t *testing.T) {
t.Parallel()
for i, this := range []struct {
input interface{}
tpl string
@@ -2414,6 +2460,7 @@ func TestDefault(t *testing.T) {
}
func TestSafeHTML(t *testing.T) {
t.Parallel()
for i, this := range []struct {
str string
tmplStr string
@@ -2454,6 +2501,7 @@ func TestSafeHTML(t *testing.T) {
}
func TestSafeHTMLAttr(t *testing.T) {
t.Parallel()
for i, this := range []struct {
str string
tmplStr string
@@ -2494,6 +2542,7 @@ func TestSafeHTMLAttr(t *testing.T) {
}
func TestSafeCSS(t *testing.T) {
t.Parallel()
for i, this := range []struct {
str string
tmplStr string
@@ -2535,6 +2584,7 @@ func TestSafeCSS(t *testing.T) {
// TODO(bep) what is this? Also look above.
func TestSafeJS(t *testing.T) {
t.Parallel()
for i, this := range []struct {
str string
tmplStr string
@@ -2576,6 +2626,7 @@ func TestSafeJS(t *testing.T) {
// TODO(bep) what is this?
func TestSafeURL(t *testing.T) {
t.Parallel()
for i, this := range []struct {
str string
tmplStr string
@@ -2616,6 +2667,7 @@ func TestSafeURL(t *testing.T) {
}
func TestBase64Decode(t *testing.T) {
t.Parallel()
testStr := "abc123!?$*&()'-=@~"
enc := base64.StdEncoding.EncodeToString([]byte(testStr))
result, err := base64Decode(enc)
@@ -2635,6 +2687,7 @@ func TestBase64Decode(t *testing.T) {
}
func TestBase64Encode(t *testing.T) {
t.Parallel()
testStr := "YWJjMTIzIT8kKiYoKSctPUB+"
dec, err := base64.StdEncoding.DecodeString(testStr)
@@ -2659,6 +2712,7 @@ func TestBase64Encode(t *testing.T) {
}
func TestMD5(t *testing.T) {
t.Parallel()
for i, this := range []struct {
input string
expectedHash string
@@ -2683,6 +2737,7 @@ func TestMD5(t *testing.T) {
}
func TestSHA1(t *testing.T) {
t.Parallel()
for i, this := range []struct {
input string
expectedHash string
@@ -2707,6 +2762,7 @@ func TestSHA1(t *testing.T) {
}
func TestSHA256(t *testing.T) {
t.Parallel()
for i, this := range []struct {
input string
expectedHash string
@@ -2731,13 +2787,15 @@ func TestSHA256(t *testing.T) {
}
func TestReadFile(t *testing.T) {
testReset()
t.Parallel()
workingDir := "/home/hugo"
viper.Set("workingDir", workingDir)
v := viper.New()
f := newTestFuncster()
v.Set("workingDir", workingDir)
f := newTestFuncsterWithViper(v)
afero.WriteFile(f.Fs.Source, filepath.Join(workingDir, "/f/f1.txt"), []byte("f1-content"), 0755)
afero.WriteFile(f.Fs.Source, filepath.Join("/home", "f2.txt"), []byte("f2-content"), 0755)
@@ -2770,6 +2828,7 @@ func TestReadFile(t *testing.T) {
}
func TestPartialCached(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
partial string
@@ -2793,7 +2852,6 @@ func TestPartialCached(t *testing.T) {
data.Section = "blog"
data.Params = map[string]interface{}{"langCode": "en"}
tstInitTemplates()
for i, tc := range testCases {
var tmp string
if tc.variant != "" {
@@ -2802,9 +2860,9 @@ func TestPartialCached(t *testing.T) {
tmp = tc.tmpl
}
cfg := newDefaultDepsCfg()
config := newDepsConfig(viper.New())
cfg.WithTemplate = func(templ tplapi.Template) error {
config.WithTemplate = func(templ tplapi.Template) error {
err := templ.AddTemplate("testroot", tmp)
if err != nil {
return err
@@ -2817,8 +2875,8 @@ func TestPartialCached(t *testing.T) {
return nil
}
de := deps.New(cfg)
require.NoError(t, de.LoadTemplates())
de := deps.New(config)
require.NoError(t, de.LoadResources())
buf := new(bytes.Buffer)
templ := de.Tmpl.Lookup("testroot")
@@ -2842,8 +2900,8 @@ func TestPartialCached(t *testing.T) {
}
func BenchmarkPartial(b *testing.B) {
cfg := newDefaultDepsCfg()
cfg.WithTemplate = func(templ tplapi.Template) error {
config := newDepsConfig(viper.New())
config.WithTemplate = func(templ tplapi.Template) error {
err := templ.AddTemplate("testroot", `{{ partial "bench1" . }}`)
if err != nil {
return err
@@ -2856,8 +2914,8 @@ func BenchmarkPartial(b *testing.B) {
return nil
}
de := deps.New(cfg)
require.NoError(b, de.LoadTemplates())
de := deps.New(config)
require.NoError(b, de.LoadResources())
buf := new(bytes.Buffer)
tmpl := de.Tmpl.Lookup("testroot")
@@ -2873,8 +2931,8 @@ func BenchmarkPartial(b *testing.B) {
}
func BenchmarkPartialCached(b *testing.B) {
cfg := newDefaultDepsCfg()
cfg.WithTemplate = func(templ tplapi.Template) error {
config := newDepsConfig(viper.New())
config.WithTemplate = func(templ tplapi.Template) error {
err := templ.AddTemplate("testroot", `{{ partialCached "bench1" . }}`)
if err != nil {
return err
@@ -2887,8 +2945,8 @@ func BenchmarkPartialCached(b *testing.B) {
return nil
}
de := deps.New(cfg)
require.NoError(b, de.LoadTemplates())
de := deps.New(config)
require.NoError(b, de.LoadResources())
buf := new(bytes.Buffer)
tmpl := de.Tmpl.Lookup("testroot")
@@ -2904,9 +2962,14 @@ func BenchmarkPartialCached(b *testing.B) {
}
func newTestFuncster() *templateFuncster {
cfg := newDefaultDepsCfg()
d := deps.New(cfg)
if err := d.LoadTemplates(); err != nil {
return newTestFuncsterWithViper(viper.New())
}
func newTestFuncsterWithViper(v *viper.Viper) *templateFuncster {
config := newDepsConfig(v)
d := deps.New(config)
if err := d.LoadResources(); err != nil {
panic(err)
}
@@ -2914,8 +2977,8 @@ func newTestFuncster() *templateFuncster {
}
func newTestTemplate(t *testing.T, name, template string) *template.Template {
cfg := newDefaultDepsCfg()
cfg.WithTemplate = func(templ tplapi.Template) error {
config := newDepsConfig(viper.New())
config.WithTemplate = func(templ tplapi.Template) error {
err := templ.AddTemplate(name, template)
if err != nil {
return err
@@ -2923,8 +2986,8 @@ func newTestTemplate(t *testing.T, name, template string) *template.Template {
return nil
}
de := deps.New(cfg)
require.NoError(t, de.LoadTemplates())
de := deps.New(config)
require.NoError(t, de.LoadResources())
return de.Tmpl.Lookup(name)
}

View File

@@ -1,100 +0,0 @@
// Copyright 2015 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 tpl
import (
"github.com/nicksnyder/go-i18n/i18n/bundle"
"github.com/spf13/hugo/helpers"
jww "github.com/spf13/jwalterweatherman"
"github.com/spf13/viper"
)
var (
// Logi18nWarnings set to true to print warnings about missing language strings
Logi18nWarnings bool
i18nWarningLogger = helpers.NewDistinctFeedbackLogger()
currentLanguage *helpers.Language
)
type translate struct {
translateFuncs map[string]bundle.TranslateFunc
current bundle.TranslateFunc
}
// TODO(bep) global translator
var translator *translate
// SetTranslateLang sets the translations language to use during template processing.
// This construction is unfortunate, but the template system is currently global.
func SetTranslateLang(language *helpers.Language) error {
currentLanguage = language
if f, ok := translator.translateFuncs[language.Lang]; ok {
translator.current = f
} else {
jww.WARN.Printf("Translation func for language %v not found, use default.", language.Lang)
translator.current = translator.translateFuncs[viper.GetString("defaultContentLanguage")]
}
return nil
}
// SetI18nTfuncs sets the language bundle to be used for i18n.
func SetI18nTfuncs(bndl *bundle.Bundle) {
translator = &translate{translateFuncs: make(map[string]bundle.TranslateFunc)}
defaultContentLanguage := viper.GetString("defaultContentLanguage")
var (
defaultT bundle.TranslateFunc
err error
)
defaultT, err = bndl.Tfunc(defaultContentLanguage)
if err != nil {
jww.WARN.Printf("No translation bundle found for default language %q", defaultContentLanguage)
}
enableMissingTranslationPlaceholders := viper.GetBool("enableMissingTranslationPlaceholders")
for _, lang := range bndl.LanguageTags() {
currentLang := lang
translator.translateFuncs[currentLang] = func(translationID string, args ...interface{}) string {
tFunc, err := bndl.Tfunc(currentLang)
if err != nil {
jww.WARN.Printf("could not load translations for language %q (%s), will use default content language.\n", lang, err)
} else if translated := tFunc(translationID, args...); translated != translationID {
return translated
}
if Logi18nWarnings {
i18nWarningLogger.Printf("i18n|MISSING_TRANSLATION|%s|%s", currentLang, translationID)
}
if enableMissingTranslationPlaceholders {
return "[i18n] " + translationID
}
if defaultT != nil {
if translated := defaultT(translationID, args...); translated != translationID {
return translated
}
}
return ""
}
}
}
func i18nTranslate(id string, args ...interface{}) (string, error) {
if translator == nil || translator.current == nil {
helpers.DistinctErrorLog.Printf("i18n not initialized, check that you have language file (in i18n) that matches the site language or the default language.")
return "", nil
}
return translator.current(id, args...), nil
}

View File

@@ -1,149 +0,0 @@
// Copyright 2015 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 tpl
import (
"testing"
"github.com/nicksnyder/go-i18n/i18n/bundle"
"github.com/spf13/hugo/helpers"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
)
type i18nTest struct {
data map[string][]byte
args interface{}
lang, id, expected, expectedFlag string
}
var i18nTests = []i18nTest{
// All translations present
{
data: map[string][]byte{
"en.yaml": []byte("- id: \"hello\"\n translation: \"Hello, World!\""),
"es.yaml": []byte("- id: \"hello\"\n translation: \"¡Hola, Mundo!\""),
},
args: nil,
lang: "es",
id: "hello",
expected: "¡Hola, Mundo!",
expectedFlag: "¡Hola, Mundo!",
},
// Translation missing in current language but present in default
{
data: map[string][]byte{
"en.yaml": []byte("- id: \"hello\"\n translation: \"Hello, World!\""),
"es.yaml": []byte("- id: \"goodbye\"\n translation: \"¡Adiós, Mundo!\""),
},
args: nil,
lang: "es",
id: "hello",
expected: "Hello, World!",
expectedFlag: "[i18n] hello",
},
// Translation missing in default language but present in current
{
data: map[string][]byte{
"en.yaml": []byte("- id: \"goodybe\"\n translation: \"Goodbye, World!\""),
"es.yaml": []byte("- id: \"hello\"\n translation: \"¡Hola, Mundo!\""),
},
args: nil,
lang: "es",
id: "hello",
expected: "¡Hola, Mundo!",
expectedFlag: "¡Hola, Mundo!",
},
// Translation missing in both default and current language
{
data: map[string][]byte{
"en.yaml": []byte("- id: \"goodbye\"\n translation: \"Goodbye, World!\""),
"es.yaml": []byte("- id: \"goodbye\"\n translation: \"¡Adiós, Mundo!\""),
},
args: nil,
lang: "es",
id: "hello",
expected: "",
expectedFlag: "[i18n] hello",
},
// Default translation file missing or empty
{
data: map[string][]byte{
"en.yaml": []byte(""),
},
args: nil,
lang: "es",
id: "hello",
expected: "",
expectedFlag: "[i18n] hello",
},
// Context provided
{
data: map[string][]byte{
"en.yaml": []byte("- id: \"wordCount\"\n translation: \"Hello, {{.WordCount}} people!\""),
"es.yaml": []byte("- id: \"wordCount\"\n translation: \"¡Hola, {{.WordCount}} gente!\""),
},
args: struct {
WordCount int
}{
50,
},
lang: "es",
id: "wordCount",
expected: "¡Hola, 50 gente!",
expectedFlag: "¡Hola, 50 gente!",
},
}
func doTestI18nTranslate(t *testing.T, data map[string][]byte, lang, id string, args interface{}) string {
i18nBundle := bundle.New()
for file, content := range data {
err := i18nBundle.ParseTranslationFileBytes(file, content)
if err != nil {
t.Errorf("Error parsing translation file: %s", err)
}
}
SetI18nTfuncs(i18nBundle)
SetTranslateLang(helpers.NewLanguage(lang))
translated, err := i18nTranslate(id, args)
if err != nil {
t.Errorf("Error translating '%s': %s", id, err)
}
return translated
}
func TestI18nTranslate(t *testing.T) {
var actual, expected string
viper.SetDefault("defaultContentLanguage", "en")
viper.Set("currentContentLanguage", helpers.NewLanguage("en"))
// Test without and with placeholders
for _, enablePlaceholders := range []bool{false, true} {
viper.Set("enableMissingTranslationPlaceholders", enablePlaceholders)
for _, test := range i18nTests {
if enablePlaceholders {
expected = test.expectedFlag
} else {
expected = test.expected
}
actual = doTestI18nTranslate(t, test.data, test.lang, test.id, test.args)
assert.Equal(t, expected, actual)
}
}
}

View File

@@ -27,9 +27,9 @@ import (
"time"
"github.com/spf13/afero"
"github.com/spf13/hugo/config"
"github.com/spf13/hugo/helpers"
jww "github.com/spf13/jwalterweatherman"
"github.com/spf13/viper"
)
var (
@@ -63,17 +63,17 @@ func (l *remoteLock) URLUnlock(url string) {
}
// getCacheFileID returns the cache ID for a string
func getCacheFileID(id string) string {
return viper.GetString("cacheDir") + url.QueryEscape(id)
func getCacheFileID(cfg config.Provider, id string) string {
return cfg.GetString("cacheDir") + url.QueryEscape(id)
}
// resGetCache returns the content for an ID from the file cache or an error
// if the file is not found returns nil,nil
func resGetCache(id string, fs afero.Fs, ignoreCache bool) ([]byte, error) {
func resGetCache(id string, fs afero.Fs, cfg config.Provider, ignoreCache bool) ([]byte, error) {
if ignoreCache {
return nil, nil
}
fID := getCacheFileID(id)
fID := getCacheFileID(cfg, id)
isExists, err := helpers.Exists(fID, fs)
if err != nil {
return nil, err
@@ -87,11 +87,11 @@ func resGetCache(id string, fs afero.Fs, ignoreCache bool) ([]byte, error) {
}
// resWriteCache writes bytes to an ID into the file cache
func resWriteCache(id string, c []byte, fs afero.Fs, ignoreCache bool) error {
func resWriteCache(id string, c []byte, fs afero.Fs, cfg config.Provider, ignoreCache bool) error {
if ignoreCache {
return nil
}
fID := getCacheFileID(id)
fID := getCacheFileID(cfg, id)
f, err := fs.Create(fID)
if err != nil {
return errors.New("Error: " + err.Error() + ". Failed to create file: " + fID)
@@ -107,13 +107,13 @@ func resWriteCache(id string, c []byte, fs afero.Fs, ignoreCache bool) error {
return nil
}
func resDeleteCache(id string, fs afero.Fs) error {
return fs.Remove(getCacheFileID(id))
func resDeleteCache(id string, fs afero.Fs, cfg config.Provider) error {
return fs.Remove(getCacheFileID(cfg, id))
}
// resGetRemote loads the content of a remote file. This method is thread safe.
func resGetRemote(url string, fs afero.Fs, hc *http.Client) ([]byte, error) {
c, err := resGetCache(url, fs, viper.GetBool("ignoreCache"))
func resGetRemote(url string, fs afero.Fs, cfg config.Provider, hc *http.Client) ([]byte, error) {
c, err := resGetCache(url, fs, cfg, cfg.GetBool("ignoreCache"))
if c != nil && err == nil {
return c, nil
}
@@ -126,7 +126,7 @@ func resGetRemote(url string, fs afero.Fs, hc *http.Client) ([]byte, error) {
defer func() { remoteURLLock.URLUnlock(url) }()
// avoid multiple locks due to calling resGetCache twice
c, err = resGetCache(url, fs, viper.GetBool("ignoreCache"))
c, err = resGetCache(url, fs, cfg, cfg.GetBool("ignoreCache"))
if c != nil && err == nil {
return c, nil
}
@@ -144,17 +144,17 @@ func resGetRemote(url string, fs afero.Fs, hc *http.Client) ([]byte, error) {
if err != nil {
return nil, err
}
err = resWriteCache(url, c, fs, viper.GetBool("ignoreCache"))
err = resWriteCache(url, c, fs, cfg, cfg.GetBool("ignoreCache"))
if err != nil {
return nil, err
}
jww.INFO.Printf("... and cached to: %s", getCacheFileID(url))
jww.INFO.Printf("... and cached to: %s", getCacheFileID(cfg, url))
return c, nil
}
// resGetLocal loads the content of a local file
func resGetLocal(url string, fs afero.Fs) ([]byte, error) {
filename := filepath.Join(viper.GetString("workingDir"), url)
func resGetLocal(url string, fs afero.Fs, cfg config.Provider) ([]byte, error) {
filename := filepath.Join(cfg.GetString("workingDir"), url)
if e, err := helpers.Exists(filename, fs); !e {
return nil, err
}
@@ -169,9 +169,9 @@ func (t *templateFuncster) resGetResource(url string) ([]byte, error) {
return nil, nil
}
if strings.Contains(url, "://") {
return resGetRemote(url, t.Fs.Source, http.DefaultClient)
return resGetRemote(url, t.Fs.Source, t.Cfg, http.DefaultClient)
}
return resGetLocal(url, t.Fs.Source)
return resGetLocal(url, t.Fs.Source, t.Cfg)
}
// getJSON expects one or n-parts of a URL to a resource which can either be a local or a remote one.
@@ -193,7 +193,7 @@ func (t *templateFuncster) getJSON(urlParts ...string) interface{} {
jww.ERROR.Printf("Cannot read json from resource %s with error message %s", url, err)
jww.ERROR.Printf("Retry #%d for %s and sleeping for %s", i, url, resSleep)
time.Sleep(resSleep)
resDeleteCache(url, t.Fs.Source)
resDeleteCache(url, t.Fs.Source, t.Cfg)
continue
}
break
@@ -226,7 +226,7 @@ func (t *templateFuncster) getCSV(sep string, urlParts ...string) [][]string {
var clearCacheSleep = func(i int, u string) {
jww.ERROR.Printf("Retry #%d for %s and sleeping for %s", i, url, resSleep)
time.Sleep(resSleep)
resDeleteCache(url, t.Fs.Source)
resDeleteCache(url, t.Fs.Source, t.Cfg)
}
for i := 0; i <= resRetries; i++ {

View File

@@ -19,10 +19,8 @@ import (
"net/http"
"net/http/httptest"
"net/url"
"os"
"strings"
"testing"
"time"
"github.com/spf13/afero"
"github.com/spf13/hugo/helpers"
@@ -32,6 +30,7 @@ import (
)
func TestScpCache(t *testing.T) {
t.Parallel()
tests := []struct {
path string
@@ -50,7 +49,8 @@ func TestScpCache(t *testing.T) {
fs := new(afero.MemMapFs)
for _, test := range tests {
c, err := resGetCache(test.path, fs, test.ignore)
cfg := viper.New()
c, err := resGetCache(test.path, fs, cfg, test.ignore)
if err != nil {
t.Errorf("Error getting cache: %s", err)
}
@@ -58,12 +58,12 @@ func TestScpCache(t *testing.T) {
t.Errorf("There is content where there should not be anything: %s", string(c))
}
err = resWriteCache(test.path, test.content, fs, test.ignore)
err = resWriteCache(test.path, test.content, fs, cfg, test.ignore)
if err != nil {
t.Errorf("Error writing cache: %s", err)
}
c, err = resGetCache(test.path, fs, test.ignore)
c, err = resGetCache(test.path, fs, cfg, test.ignore)
if err != nil {
t.Errorf("Error getting cache after writing: %s", err)
}
@@ -80,8 +80,9 @@ func TestScpCache(t *testing.T) {
}
func TestScpGetLocal(t *testing.T) {
testReset()
fs := hugofs.NewMem()
t.Parallel()
v := viper.New()
fs := hugofs.NewMem(v)
ps := helpers.FilePathSeparator
tests := []struct {
@@ -102,7 +103,7 @@ func TestScpGetLocal(t *testing.T) {
t.Error(err)
}
c, err := resGetLocal(test.path, fs.Source)
c, err := resGetLocal(test.path, fs.Source, v)
if err != nil {
t.Errorf("Error getting resource content: %s", err)
}
@@ -126,6 +127,7 @@ func getTestServer(handler func(w http.ResponseWriter, r *http.Request)) (*httpt
}
func TestScpGetRemote(t *testing.T) {
t.Parallel()
fs := new(afero.MemMapFs)
tests := []struct {
@@ -146,14 +148,16 @@ func TestScpGetRemote(t *testing.T) {
})
defer func() { srv.Close() }()
c, err := resGetRemote(test.path, fs, cl)
cfg := viper.New()
c, err := resGetRemote(test.path, fs, cfg, cl)
if err != nil {
t.Errorf("Error getting resource content: %s", err)
}
if !bytes.Equal(c, test.content) {
t.Errorf("\nNet Expected: %s\nNet Actual: %s\n", string(test.content), string(c))
}
cc, cErr := resGetCache(test.path, fs, test.ignore)
cc, cErr := resGetCache(test.path, fs, cfg, test.ignore)
if cErr != nil {
t.Error(cErr)
}
@@ -170,6 +174,7 @@ func TestScpGetRemote(t *testing.T) {
}
func TestParseCSV(t *testing.T) {
t.Parallel()
tests := []struct {
csv []byte
@@ -208,29 +213,11 @@ func TestParseCSV(t *testing.T) {
}
}
// https://twitter.com/francesc/status/603066617124126720
// for the construct: defer testRetryWhenDone().Reset()
type wd struct {
Reset func()
}
func testRetryWhenDone(f *templateFuncster) wd {
cd := viper.GetString("cacheDir")
viper.Set("cacheDir", helpers.GetTempDir("", f.Fs.Source))
var tmpSleep time.Duration
tmpSleep, resSleep = resSleep, time.Millisecond
return wd{func() {
viper.Set("cacheDir", cd)
resSleep = tmpSleep
}}
}
func TestGetJSONFailParse(t *testing.T) {
t.Parallel()
f := newTestFuncster()
defer testRetryWhenDone(f).Reset()
reqCount := 0
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if reqCount > 0 {
@@ -244,7 +231,6 @@ func TestGetJSONFailParse(t *testing.T) {
}))
defer ts.Close()
url := ts.URL + "/test.json"
defer os.Remove(getCacheFileID(url))
want := map[string]interface{}{"gomeetup": []interface{}{"Sydney", "San Francisco", "Stockholm"}}
have := f.getJSON(url)
@@ -255,10 +241,9 @@ func TestGetJSONFailParse(t *testing.T) {
}
func TestGetCSVFailParseSep(t *testing.T) {
t.Parallel()
f := newTestFuncster()
defer testRetryWhenDone(f).Reset()
reqCount := 0
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if reqCount > 0 {
@@ -275,7 +260,6 @@ func TestGetCSVFailParseSep(t *testing.T) {
}))
defer ts.Close()
url := ts.URL + "/test.csv"
defer os.Remove(getCacheFileID(url))
want := [][]string{{"gomeetup", "city"}, {"yes", "Sydney"}, {"yes", "San Francisco"}, {"yes", "Stockholm"}}
have := f.getCSV(",", url)
@@ -286,11 +270,10 @@ func TestGetCSVFailParseSep(t *testing.T) {
}
func TestGetCSVFailParse(t *testing.T) {
t.Parallel()
f := newTestFuncster()
defer testRetryWhenDone(f).Reset()
reqCount := 0
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-type", "application/json")
@@ -309,7 +292,6 @@ func TestGetCSVFailParse(t *testing.T) {
}))
defer ts.Close()
url := ts.URL + "/test.csv"
defer os.Remove(getCacheFileID(url))
want := [][]string{{"gomeetup", "city"}, {"yes", "Sydney"}, {"yes", "San Francisco"}, {"yes", "Stockholm"}}
have := f.getCSV(",", url)

View File

@@ -26,21 +26,15 @@ import (
"github.com/spf13/afero"
"github.com/spf13/hugo/deps"
"github.com/spf13/hugo/helpers"
"github.com/spf13/hugo/tplapi"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
)
func testReset() {
viper.Reset()
// TODO(bep) viper-globals
viper.Set("currentContentLanguage", helpers.NewLanguage("en"))
}
// Some tests for Issue #1178 -- Ace
func TestAceTemplates(t *testing.T) {
t.Parallel()
for i, this := range []struct {
basePath string
@@ -79,7 +73,7 @@ html lang=en
d := "DATA"
config := newDefaultDepsCfg()
config := newDepsConfig(viper.New())
config.WithTemplate = func(templ tplapi.Template) error {
return templ.AddAceTemplate("mytemplate.ace", basePath, innerPath,
[]byte(this.baseContent), []byte(this.innerContent))
@@ -87,7 +81,7 @@ html lang=en
a := deps.New(config)
if err := a.LoadTemplates(); err != nil {
if err := a.LoadResources(); err != nil {
t.Fatal(err)
}
@@ -124,6 +118,7 @@ func isAtLeastGo16() bool {
}
func TestAddTemplateFileWithMaster(t *testing.T) {
t.Parallel()
if !isAtLeastGo16() {
t.Skip("This test only runs on Go >= 1.6")
@@ -148,8 +143,8 @@ func TestAddTemplateFileWithMaster(t *testing.T) {
masterTplName := "mt"
finalTplName := "tp"
cfg := newDefaultDepsCfg()
cfg.WithTemplate = func(templ tplapi.Template) error {
config := newDepsConfig(viper.New())
config.WithTemplate = func(templ tplapi.Template) error {
err := templ.AddTemplateFileWithMaster(finalTplName, overlayTplName, masterTplName)
@@ -189,13 +184,13 @@ func TestAddTemplateFileWithMaster(t *testing.T) {
}
if this.writeSkipper != 1 {
afero.WriteFile(cfg.Fs.Source, masterTplName, []byte(this.masterTplContent), 0644)
afero.WriteFile(config.Fs.Source, masterTplName, []byte(this.masterTplContent), 0644)
}
if this.writeSkipper != 2 {
afero.WriteFile(cfg.Fs.Source, overlayTplName, []byte(this.overlayTplContent), 0644)
afero.WriteFile(config.Fs.Source, overlayTplName, []byte(this.overlayTplContent), 0644)
}
deps.New(cfg)
deps.New(config)
}
@@ -204,6 +199,7 @@ func TestAddTemplateFileWithMaster(t *testing.T) {
// A Go stdlib test for linux/arm. Will remove later.
// See #1771
func TestBigIntegerFunc(t *testing.T) {
t.Parallel()
var func1 = func(v int64) error {
return nil
}
@@ -234,6 +230,7 @@ func (b BI) A(v int64) error {
return nil
}
func TestBigIntegerMethod(t *testing.T) {
t.Parallel()
data := &BI{}
@@ -253,6 +250,7 @@ func TestBigIntegerMethod(t *testing.T) {
// Test for bugs discovered by https://github.com/dvyukov/go-fuzz
func TestTplGoFuzzReports(t *testing.T) {
t.Parallel()
// The following test case(s) also fail
// See https://github.com/golang/go/issues/10634
@@ -284,13 +282,14 @@ func TestTplGoFuzzReports(t *testing.T) {
H: "a,b,c,d,e,f",
}
cfg := newDefaultDepsCfg()
cfg.WithTemplate = func(templ tplapi.Template) error {
config := newDepsConfig(viper.New())
config.WithTemplate = func(templ tplapi.Template) error {
return templ.AddTemplate("fuzz", this.data)
}
de := deps.New(cfg)
require.NoError(t, de.LoadTemplates())
de := deps.New(config)
require.NoError(t, de.LoadResources())
templ := de.Tmpl.(*GoHTMLTemplate)