mirror of
				https://github.com/gohugoio/hugo.git
				synced 2024-05-11 05:54:58 +00:00 
			
		
		
		
	Note that this is backed by a LRU cache (which we soon shall see more usage of), so if you're a heavy user of cached partials it may be evicted and refreshed if needed. But in most cases every partial is only invoked once. This commit also adds a timeout (the global `timeout` config option) to make infinite recursion in partials easier to reason about. ``` name old time/op new time/op delta IncludeCached-10 8.92ms ± 0% 8.48ms ± 1% -4.87% (p=0.016 n=4+5) name old alloc/op new alloc/op delta IncludeCached-10 6.65MB ± 0% 5.17MB ± 0% -22.32% (p=0.002 n=6+6) name old allocs/op new allocs/op delta IncludeCached-10 117k ± 0% 71k ± 0% -39.44% (p=0.002 n=6+6) ``` Closes #4086 Updates #9588
		
			
				
	
	
		
			461 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			461 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright 2019 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 helpers
 | ||
| 
 | ||
| import (
 | ||
| 	"fmt"
 | ||
| 	"reflect"
 | ||
| 	"strings"
 | ||
| 	"testing"
 | ||
| 	"time"
 | ||
| 
 | ||
| 	"github.com/gohugoio/hugo/common/loggers"
 | ||
| 	"github.com/gohugoio/hugo/config"
 | ||
| 
 | ||
| 	qt "github.com/frankban/quicktest"
 | ||
| 	"github.com/spf13/afero"
 | ||
| )
 | ||
| 
 | ||
| func TestResolveMarkup(t *testing.T) {
 | ||
| 	c := qt.New(t)
 | ||
| 	cfg := config.NewWithTestDefaults()
 | ||
| 	spec, err := NewContentSpec(cfg, loggers.NewErrorLogger(), afero.NewMemMapFs(), nil)
 | ||
| 	c.Assert(err, qt.IsNil)
 | ||
| 
 | ||
| 	for i, this := range []struct {
 | ||
| 		in     string
 | ||
| 		expect string
 | ||
| 	}{
 | ||
| 		{"md", "markdown"},
 | ||
| 		{"markdown", "markdown"},
 | ||
| 		{"mdown", "markdown"},
 | ||
| 		{"asciidocext", "asciidocext"},
 | ||
| 		{"adoc", "asciidocext"},
 | ||
| 		{"ad", "asciidocext"},
 | ||
| 		{"rst", "rst"},
 | ||
| 		{"pandoc", "pandoc"},
 | ||
| 		{"pdc", "pandoc"},
 | ||
| 		{"html", "html"},
 | ||
| 		{"htm", "html"},
 | ||
| 		{"org", "org"},
 | ||
| 		{"excel", ""},
 | ||
| 	} {
 | ||
| 		result := spec.ResolveMarkup(this.in)
 | ||
| 		if result != this.expect {
 | ||
| 			t.Errorf("[%d] got %s but expected %s", i, result, this.expect)
 | ||
| 		}
 | ||
| 	}
 | ||
| }
 | ||
| 
 | ||
| func TestDistinctLoggerDoesNotLockOnWarningPanic(t *testing.T) {
 | ||
| 	// Testing to make sure logger mutex doesn't lock if warnings cause panics.
 | ||
| 	// func Warnf() of DistinctLogger is defined in general.go
 | ||
| 	l := NewDistinctLogger(loggers.NewWarningLogger())
 | ||
| 
 | ||
| 	// Set PanicOnWarning to true to reproduce issue 9380
 | ||
| 	// Ensure global variable loggers.PanicOnWarning is reset to old value after test
 | ||
| 	if loggers.PanicOnWarning == false {
 | ||
| 		loggers.PanicOnWarning = true
 | ||
| 		defer func() {
 | ||
| 			loggers.PanicOnWarning = false
 | ||
| 		}()
 | ||
| 	}
 | ||
| 
 | ||
| 	// Establish timeout in case a lock occurs:
 | ||
| 	timeIsUp := make(chan bool)
 | ||
| 	timeOutSeconds := 1
 | ||
| 	go func() {
 | ||
| 		time.Sleep(time.Second * time.Duration(timeOutSeconds))
 | ||
| 		timeIsUp <- true
 | ||
| 	}()
 | ||
| 
 | ||
| 	// Attempt to run multiple logging threads in parallel
 | ||
| 	counterC := make(chan int)
 | ||
| 	goroutines := 5
 | ||
| 
 | ||
| 	for i := 0; i < goroutines; i++ {
 | ||
| 		go func() {
 | ||
| 			defer func() {
 | ||
| 				// Intentional panic successfully recovered - notify counter channel
 | ||
| 				recover()
 | ||
| 				counterC <- 1
 | ||
| 			}()
 | ||
| 
 | ||
| 			l.Warnf("Placeholder template message: %v", "In this test, logging a warning causes a panic.")
 | ||
| 		}()
 | ||
| 	}
 | ||
| 
 | ||
| 	// All goroutines should complete before timeout
 | ||
| 	var counter int
 | ||
| 	for {
 | ||
| 		select {
 | ||
| 		case <-counterC:
 | ||
| 			counter++
 | ||
| 			if counter == goroutines {
 | ||
| 				return
 | ||
| 			}
 | ||
| 		case <-timeIsUp:
 | ||
| 			t.Errorf("Unable to log warnings with --panicOnWarning within alloted time of: %v seconds. Investigate possible mutex locking on panic in distinct warning logger.", timeOutSeconds)
 | ||
| 			return
 | ||
| 		}
 | ||
| 	}
 | ||
| }
 | ||
| 
 | ||
| func TestFirstUpper(t *testing.T) {
 | ||
| 	for i, this := range []struct {
 | ||
| 		in     string
 | ||
| 		expect string
 | ||
| 	}{
 | ||
| 		{"foo", "Foo"},
 | ||
| 		{"foo bar", "Foo bar"},
 | ||
| 		{"Foo Bar", "Foo Bar"},
 | ||
| 		{"", ""},
 | ||
| 		{"å", "Å"},
 | ||
| 	} {
 | ||
| 		result := FirstUpper(this.in)
 | ||
| 		if result != this.expect {
 | ||
| 			t.Errorf("[%d] got %s but expected %s", i, result, this.expect)
 | ||
| 		}
 | ||
| 	}
 | ||
| }
 | ||
| 
 | ||
| func TestHasStringsPrefix(t *testing.T) {
 | ||
| 	for i, this := range []struct {
 | ||
| 		s      []string
 | ||
| 		prefix []string
 | ||
| 		expect bool
 | ||
| 	}{
 | ||
| 		{[]string{"a"}, []string{"a"}, true},
 | ||
| 		{[]string{}, []string{}, true},
 | ||
| 		{[]string{"a", "b", "c"}, []string{"a", "b"}, true},
 | ||
| 		{[]string{"d", "a", "b", "c"}, []string{"a", "b"}, false},
 | ||
| 		{[]string{"abra", "ca", "dabra"}, []string{"abra", "ca"}, true},
 | ||
| 		{[]string{"abra", "ca"}, []string{"abra", "ca", "dabra"}, false},
 | ||
| 	} {
 | ||
| 		result := HasStringsPrefix(this.s, this.prefix)
 | ||
| 		if result != this.expect {
 | ||
| 			t.Fatalf("[%d] got %t but expected %t", i, result, this.expect)
 | ||
| 		}
 | ||
| 	}
 | ||
| }
 | ||
| 
 | ||
| func TestHasStringsSuffix(t *testing.T) {
 | ||
| 	for i, this := range []struct {
 | ||
| 		s      []string
 | ||
| 		suffix []string
 | ||
| 		expect bool
 | ||
| 	}{
 | ||
| 		{[]string{"a"}, []string{"a"}, true},
 | ||
| 		{[]string{}, []string{}, true},
 | ||
| 		{[]string{"a", "b", "c"}, []string{"b", "c"}, true},
 | ||
| 		{[]string{"abra", "ca", "dabra"}, []string{"abra", "ca"}, false},
 | ||
| 		{[]string{"abra", "ca", "dabra"}, []string{"ca", "dabra"}, true},
 | ||
| 	} {
 | ||
| 		result := HasStringsSuffix(this.s, this.suffix)
 | ||
| 		if result != this.expect {
 | ||
| 			t.Fatalf("[%d] got %t but expected %t", i, result, this.expect)
 | ||
| 		}
 | ||
| 	}
 | ||
| }
 | ||
| 
 | ||
| var containsTestText = (`На берегу пустынных волн
 | ||
| Стоял он, дум великих полн,
 | ||
| И вдаль глядел. Пред ним широко
 | ||
| Река неслася; бедный чёлн
 | ||
| По ней стремился одиноко.
 | ||
| По мшистым, топким берегам
 | ||
| Чернели избы здесь и там,
 | ||
| Приют убогого чухонца;
 | ||
| И лес, неведомый лучам
 | ||
| В тумане спрятанного солнца,
 | ||
| Кругом шумел.
 | ||
| 
 | ||
| Τη γλώσσα μου έδωσαν ελληνική
 | ||
| το σπίτι φτωχικό στις αμμουδιές του Ομήρου.
 | ||
| Μονάχη έγνοια η γλώσσα μου στις αμμουδιές του Ομήρου.
 | ||
| 
 | ||
| από το Άξιον Εστί
 | ||
| του Οδυσσέα Ελύτη
 | ||
| 
 | ||
| Sîne klâwen durh die wolken sint geslagen,
 | ||
| er stîget ûf mit grôzer kraft,
 | ||
| ich sih in grâwen tägelîch als er wil tagen,
 | ||
| den tac, der im geselleschaft
 | ||
| erwenden wil, dem werden man,
 | ||
| den ich mit sorgen în verliez.
 | ||
| ich bringe in hinnen, ob ich kan.
 | ||
| sîn vil manegiu tugent michz leisten hiez.
 | ||
| `)
 | ||
| 
 | ||
| var containsBenchTestData = []struct {
 | ||
| 	v1     string
 | ||
| 	v2     []byte
 | ||
| 	expect bool
 | ||
| }{
 | ||
| 	{"abc", []byte("a"), true},
 | ||
| 	{"abc", []byte("b"), true},
 | ||
| 	{"abcdefg", []byte("efg"), true},
 | ||
| 	{"abc", []byte("d"), false},
 | ||
| 	{containsTestText, []byte("стремился"), true},
 | ||
| 	{containsTestText, []byte(containsTestText[10:80]), true},
 | ||
| 	{containsTestText, []byte(containsTestText[100:111]), true},
 | ||
| 	{containsTestText, []byte(containsTestText[len(containsTestText)-100 : len(containsTestText)-10]), true},
 | ||
| 	{containsTestText, []byte(containsTestText[len(containsTestText)-20:]), true},
 | ||
| 	{containsTestText, []byte("notfound"), false},
 | ||
| }
 | ||
| 
 | ||
| // some corner cases
 | ||
| var containsAdditionalTestData = []struct {
 | ||
| 	v1     string
 | ||
| 	v2     []byte
 | ||
| 	expect bool
 | ||
| }{
 | ||
| 	{"", nil, false},
 | ||
| 	{"", []byte("a"), false},
 | ||
| 	{"a", []byte(""), false},
 | ||
| 	{"", []byte(""), false},
 | ||
| }
 | ||
| 
 | ||
| func TestSliceToLower(t *testing.T) {
 | ||
| 	t.Parallel()
 | ||
| 	tests := []struct {
 | ||
| 		value    []string
 | ||
| 		expected []string
 | ||
| 	}{
 | ||
| 		{[]string{"a", "b", "c"}, []string{"a", "b", "c"}},
 | ||
| 		{[]string{"a", "B", "c"}, []string{"a", "b", "c"}},
 | ||
| 		{[]string{"A", "B", "C"}, []string{"a", "b", "c"}},
 | ||
| 	}
 | ||
| 
 | ||
| 	for _, test := range tests {
 | ||
| 		res := SliceToLower(test.value)
 | ||
| 		for i, val := range res {
 | ||
| 			if val != test.expected[i] {
 | ||
| 				t.Errorf("Case mismatch. Expected %s, got %s", test.expected[i], res[i])
 | ||
| 			}
 | ||
| 		}
 | ||
| 	}
 | ||
| }
 | ||
| 
 | ||
| func TestReaderContains(t *testing.T) {
 | ||
| 	c := qt.New(t)
 | ||
| 	for i, this := range append(containsBenchTestData, containsAdditionalTestData...) {
 | ||
| 		result := ReaderContains(strings.NewReader(this.v1), this.v2)
 | ||
| 		if result != this.expect {
 | ||
| 			t.Errorf("[%d] got %t but expected %t", i, result, this.expect)
 | ||
| 		}
 | ||
| 	}
 | ||
| 
 | ||
| 	c.Assert(ReaderContains(nil, []byte("a")), qt.Equals, false)
 | ||
| 	c.Assert(ReaderContains(nil, nil), qt.Equals, false)
 | ||
| }
 | ||
| 
 | ||
| func TestGetTitleFunc(t *testing.T) {
 | ||
| 	title := "somewhere over the rainbow"
 | ||
| 	c := qt.New(t)
 | ||
| 
 | ||
| 	c.Assert(GetTitleFunc("go")(title), qt.Equals, "Somewhere Over The Rainbow")
 | ||
| 	c.Assert(GetTitleFunc("chicago")(title), qt.Equals, "Somewhere over the Rainbow")
 | ||
| 	c.Assert(GetTitleFunc("Chicago")(title), qt.Equals, "Somewhere over the Rainbow")
 | ||
| 	c.Assert(GetTitleFunc("ap")(title), qt.Equals, "Somewhere Over the Rainbow")
 | ||
| 	c.Assert(GetTitleFunc("ap")(title), qt.Equals, "Somewhere Over the Rainbow")
 | ||
| 	c.Assert(GetTitleFunc("")(title), qt.Equals, "Somewhere Over the Rainbow")
 | ||
| 	c.Assert(GetTitleFunc("unknown")(title), qt.Equals, "Somewhere Over the Rainbow")
 | ||
| }
 | ||
| 
 | ||
| func BenchmarkReaderContains(b *testing.B) {
 | ||
| 	b.ResetTimer()
 | ||
| 	for i := 0; i < b.N; i++ {
 | ||
| 		for i, this := range containsBenchTestData {
 | ||
| 			result := ReaderContains(strings.NewReader(this.v1), this.v2)
 | ||
| 			if result != this.expect {
 | ||
| 				b.Errorf("[%d] got %t but expected %t", i, result, this.expect)
 | ||
| 			}
 | ||
| 		}
 | ||
| 	}
 | ||
| }
 | ||
| 
 | ||
| func TestUniqueStrings(t *testing.T) {
 | ||
| 	in := []string{"a", "b", "a", "b", "c", "", "a", "", "d"}
 | ||
| 	output := UniqueStrings(in)
 | ||
| 	expected := []string{"a", "b", "c", "", "d"}
 | ||
| 	if !reflect.DeepEqual(output, expected) {
 | ||
| 		t.Errorf("Expected %#v, got %#v\n", expected, output)
 | ||
| 	}
 | ||
| }
 | ||
| 
 | ||
| func TestUniqueStringsReuse(t *testing.T) {
 | ||
| 	in := []string{"a", "b", "a", "b", "c", "", "a", "", "d"}
 | ||
| 	output := UniqueStringsReuse(in)
 | ||
| 	expected := []string{"a", "b", "c", "", "d"}
 | ||
| 	if !reflect.DeepEqual(output, expected) {
 | ||
| 		t.Errorf("Expected %#v, got %#v\n", expected, output)
 | ||
| 	}
 | ||
| }
 | ||
| 
 | ||
| func TestUniqueStringsSorted(t *testing.T) {
 | ||
| 	c := qt.New(t)
 | ||
| 	in := []string{"a", "a", "b", "c", "b", "", "a", "", "d"}
 | ||
| 	output := UniqueStringsSorted(in)
 | ||
| 	expected := []string{"", "a", "b", "c", "d"}
 | ||
| 	c.Assert(output, qt.DeepEquals, expected)
 | ||
| 	c.Assert(UniqueStringsSorted(nil), qt.IsNil)
 | ||
| }
 | ||
| 
 | ||
| func TestFindAvailablePort(t *testing.T) {
 | ||
| 	c := qt.New(t)
 | ||
| 	addr, err := FindAvailablePort()
 | ||
| 	c.Assert(err, qt.IsNil)
 | ||
| 	c.Assert(addr, qt.Not(qt.IsNil))
 | ||
| 	c.Assert(addr.Port > 0, qt.Equals, true)
 | ||
| }
 | ||
| 
 | ||
| func TestFastMD5FromFile(t *testing.T) {
 | ||
| 	fs := afero.NewMemMapFs()
 | ||
| 
 | ||
| 	if err := afero.WriteFile(fs, "small.txt", []byte("abc"), 0777); err != nil {
 | ||
| 		t.Fatal(err)
 | ||
| 	}
 | ||
| 
 | ||
| 	if err := afero.WriteFile(fs, "small2.txt", []byte("abd"), 0777); err != nil {
 | ||
| 		t.Fatal(err)
 | ||
| 	}
 | ||
| 
 | ||
| 	if err := afero.WriteFile(fs, "bigger.txt", []byte(strings.Repeat("a bc d e", 100)), 0777); err != nil {
 | ||
| 		t.Fatal(err)
 | ||
| 	}
 | ||
| 
 | ||
| 	if err := afero.WriteFile(fs, "bigger2.txt", []byte(strings.Repeat("c d e f g", 100)), 0777); err != nil {
 | ||
| 		t.Fatal(err)
 | ||
| 	}
 | ||
| 
 | ||
| 	c := qt.New(t)
 | ||
| 
 | ||
| 	sf1, err := fs.Open("small.txt")
 | ||
| 	c.Assert(err, qt.IsNil)
 | ||
| 	sf2, err := fs.Open("small2.txt")
 | ||
| 	c.Assert(err, qt.IsNil)
 | ||
| 
 | ||
| 	bf1, err := fs.Open("bigger.txt")
 | ||
| 	c.Assert(err, qt.IsNil)
 | ||
| 	bf2, err := fs.Open("bigger2.txt")
 | ||
| 	c.Assert(err, qt.IsNil)
 | ||
| 
 | ||
| 	defer sf1.Close()
 | ||
| 	defer sf2.Close()
 | ||
| 	defer bf1.Close()
 | ||
| 	defer bf2.Close()
 | ||
| 
 | ||
| 	m1, err := MD5FromFileFast(sf1)
 | ||
| 	c.Assert(err, qt.IsNil)
 | ||
| 	c.Assert(m1, qt.Equals, "e9c8989b64b71a88b4efb66ad05eea96")
 | ||
| 
 | ||
| 	m2, err := MD5FromFileFast(sf2)
 | ||
| 	c.Assert(err, qt.IsNil)
 | ||
| 	c.Assert(m2, qt.Not(qt.Equals), m1)
 | ||
| 
 | ||
| 	m3, err := MD5FromFileFast(bf1)
 | ||
| 	c.Assert(err, qt.IsNil)
 | ||
| 	c.Assert(m3, qt.Not(qt.Equals), m2)
 | ||
| 
 | ||
| 	m4, err := MD5FromFileFast(bf2)
 | ||
| 	c.Assert(err, qt.IsNil)
 | ||
| 	c.Assert(m4, qt.Not(qt.Equals), m3)
 | ||
| 
 | ||
| 	m5, err := MD5FromReader(bf2)
 | ||
| 	c.Assert(err, qt.IsNil)
 | ||
| 	c.Assert(m5, qt.Not(qt.Equals), m4)
 | ||
| }
 | ||
| 
 | ||
| func BenchmarkMD5FromFileFast(b *testing.B) {
 | ||
| 	fs := afero.NewMemMapFs()
 | ||
| 
 | ||
| 	for _, full := range []bool{false, true} {
 | ||
| 		b.Run(fmt.Sprintf("full=%t", full), func(b *testing.B) {
 | ||
| 			for i := 0; i < b.N; i++ {
 | ||
| 				b.StopTimer()
 | ||
| 				if err := afero.WriteFile(fs, "file.txt", []byte(strings.Repeat("1234567890", 2000)), 0777); err != nil {
 | ||
| 					b.Fatal(err)
 | ||
| 				}
 | ||
| 				f, err := fs.Open("file.txt")
 | ||
| 				if err != nil {
 | ||
| 					b.Fatal(err)
 | ||
| 				}
 | ||
| 				b.StartTimer()
 | ||
| 				if full {
 | ||
| 					if _, err := MD5FromReader(f); err != nil {
 | ||
| 						b.Fatal(err)
 | ||
| 					}
 | ||
| 				} else {
 | ||
| 					if _, err := MD5FromFileFast(f); err != nil {
 | ||
| 						b.Fatal(err)
 | ||
| 					}
 | ||
| 				}
 | ||
| 				f.Close()
 | ||
| 			}
 | ||
| 		})
 | ||
| 	}
 | ||
| }
 | ||
| 
 | ||
| func BenchmarkUniqueStrings(b *testing.B) {
 | ||
| 	input := []string{"a", "b", "d", "e", "d", "h", "a", "i"}
 | ||
| 
 | ||
| 	b.Run("Safe", func(b *testing.B) {
 | ||
| 		for i := 0; i < b.N; i++ {
 | ||
| 			result := UniqueStrings(input)
 | ||
| 			if len(result) != 6 {
 | ||
| 				b.Fatal(fmt.Sprintf("invalid count: %d", len(result)))
 | ||
| 			}
 | ||
| 		}
 | ||
| 	})
 | ||
| 
 | ||
| 	b.Run("Reuse slice", func(b *testing.B) {
 | ||
| 		b.StopTimer()
 | ||
| 		inputs := make([][]string, b.N)
 | ||
| 		for i := 0; i < b.N; i++ {
 | ||
| 			inputc := make([]string, len(input))
 | ||
| 			copy(inputc, input)
 | ||
| 			inputs[i] = inputc
 | ||
| 		}
 | ||
| 		b.StartTimer()
 | ||
| 		for i := 0; i < b.N; i++ {
 | ||
| 			inputc := inputs[i]
 | ||
| 
 | ||
| 			result := UniqueStringsReuse(inputc)
 | ||
| 			if len(result) != 6 {
 | ||
| 				b.Fatal(fmt.Sprintf("invalid count: %d", len(result)))
 | ||
| 			}
 | ||
| 		}
 | ||
| 	})
 | ||
| 
 | ||
| 	b.Run("Reuse slice sorted", func(b *testing.B) {
 | ||
| 		b.StopTimer()
 | ||
| 		inputs := make([][]string, b.N)
 | ||
| 		for i := 0; i < b.N; i++ {
 | ||
| 			inputc := make([]string, len(input))
 | ||
| 			copy(inputc, input)
 | ||
| 			inputs[i] = inputc
 | ||
| 		}
 | ||
| 		b.StartTimer()
 | ||
| 		for i := 0; i < b.N; i++ {
 | ||
| 			inputc := inputs[i]
 | ||
| 
 | ||
| 			result := UniqueStringsSorted(inputc)
 | ||
| 			if len(result) != 6 {
 | ||
| 				b.Fatal(fmt.Sprintf("invalid count: %d", len(result)))
 | ||
| 			}
 | ||
| 		}
 | ||
| 	})
 | ||
| }
 |