hugo/hugolib/page_bundler_capture_test.go
Bjørn Erik Pedersen 3cdf19e9b7
Implement Page bundling and image handling
This commit is not the smallest in Hugo's history.

Some hightlights include:

* Page bundles (for complete articles, keeping images and content together etc.).
* Bundled images can be processed in as many versions/sizes as you need with the three methods `Resize`, `Fill` and `Fit`.
* Processed images are cached inside `resources/_gen/images` (default) in your project.
* Symbolic links (both files and dirs) are now allowed anywhere inside /content
* A new table based build summary
* The "Total in nn ms" now reports the total including the handling of the files inside /static. So if it now reports more than you're used to, it is just **more real** and probably faster than before (see below).

A site building  benchmark run compared to `v0.31.1` shows that this should be slightly faster and use less memory:

```bash
▶ ./benchSite.sh "TOML,num_langs=.*,num_root_sections=5,num_pages=(500|1000),tags_per_page=5,shortcodes,render"

benchmark                                                                                                         old ns/op     new ns/op     delta
BenchmarkSiteBuilding/TOML,num_langs=1,num_root_sections=5,num_pages=500,tags_per_page=5,shortcodes,render-4      101785785     78067944      -23.30%
BenchmarkSiteBuilding/TOML,num_langs=1,num_root_sections=5,num_pages=1000,tags_per_page=5,shortcodes,render-4     185481057     149159919     -19.58%
BenchmarkSiteBuilding/TOML,num_langs=3,num_root_sections=5,num_pages=500,tags_per_page=5,shortcodes,render-4      103149918     85679409      -16.94%
BenchmarkSiteBuilding/TOML,num_langs=3,num_root_sections=5,num_pages=1000,tags_per_page=5,shortcodes,render-4     203515478     169208775     -16.86%

benchmark                                                                                                         old allocs     new allocs     delta
BenchmarkSiteBuilding/TOML,num_langs=1,num_root_sections=5,num_pages=500,tags_per_page=5,shortcodes,render-4      532464         391539         -26.47%
BenchmarkSiteBuilding/TOML,num_langs=1,num_root_sections=5,num_pages=1000,tags_per_page=5,shortcodes,render-4     1056549        772702         -26.87%
BenchmarkSiteBuilding/TOML,num_langs=3,num_root_sections=5,num_pages=500,tags_per_page=5,shortcodes,render-4      555974         406630         -26.86%
BenchmarkSiteBuilding/TOML,num_langs=3,num_root_sections=5,num_pages=1000,tags_per_page=5,shortcodes,render-4     1086545        789922         -27.30%

benchmark                                                                                                         old bytes     new bytes     delta
BenchmarkSiteBuilding/TOML,num_langs=1,num_root_sections=5,num_pages=500,tags_per_page=5,shortcodes,render-4      53243246      43598155      -18.12%
BenchmarkSiteBuilding/TOML,num_langs=1,num_root_sections=5,num_pages=1000,tags_per_page=5,shortcodes,render-4     105811617     86087116      -18.64%
BenchmarkSiteBuilding/TOML,num_langs=3,num_root_sections=5,num_pages=500,tags_per_page=5,shortcodes,render-4      54558852      44545097      -18.35%
BenchmarkSiteBuilding/TOML,num_langs=3,num_root_sections=5,num_pages=1000,tags_per_page=5,shortcodes,render-4     106903858     86978413      -18.64%
```

Fixes #3651
Closes #3158
Fixes #1014
Closes #2021
Fixes #1240
Updates #3757
2017-12-27 18:44:47 +01:00

255 lines
7.3 KiB
Go

// Copyright 2017-present 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 hugolib
import (
"fmt"
"path"
"path/filepath"
"sort"
jww "github.com/spf13/jwalterweatherman"
"strings"
"sync"
"testing"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/source"
"github.com/stretchr/testify/require"
)
type storeFilenames struct {
sync.Mutex
filenames []string
copyNames []string
dirKeys []string
}
func (s *storeFilenames) handleSingles(fis ...*fileInfo) {
s.Lock()
defer s.Unlock()
for _, fi := range fis {
s.filenames = append(s.filenames, filepath.ToSlash(fi.Filename()))
}
}
func (s *storeFilenames) handleBundles(d *bundleDirs) {
s.Lock()
defer s.Unlock()
var keys []string
for _, b := range d.bundles {
res := make([]string, len(b.resources))
i := 0
for _, r := range b.resources {
res[i] = path.Join(r.Lang(), filepath.ToSlash(r.Filename()))
i++
}
sort.Strings(res)
keys = append(keys, path.Join("__bundle", b.fi.Lang(), filepath.ToSlash(b.fi.Filename()), "resources", strings.Join(res, "|")))
}
s.dirKeys = append(s.dirKeys, keys...)
}
func (s *storeFilenames) handleCopyFiles(names ...string) {
s.Lock()
defer s.Unlock()
for _, name := range names {
s.copyNames = append(s.copyNames, filepath.ToSlash(name))
}
}
func (s *storeFilenames) sortedStr() string {
s.Lock()
defer s.Unlock()
sort.Strings(s.filenames)
sort.Strings(s.dirKeys)
sort.Strings(s.copyNames)
return "\nF:\n" + strings.Join(s.filenames, "\n") + "\nD:\n" + strings.Join(s.dirKeys, "\n") +
"\nC:\n" + strings.Join(s.copyNames, "\n") + "\n"
}
func TestPageBundlerCaptureSymlinks(t *testing.T) {
assert := require.New(t)
cfg, fs, workDir := newTestBundleSymbolicSources(t)
contentDir := "base"
sourceSpec := source.NewSourceSpec(cfg, fs)
fileStore := &storeFilenames{}
logger := newWarningLogger()
c := newCapturer(logger, sourceSpec, fileStore, nil, filepath.Join(workDir, contentDir))
assert.NoError(c.capture())
// Symlik back to content skipped to prevent infinite recursion.
assert.Equal(uint64(3), logger.LogCountForLevelsGreaterThanorEqualTo(jww.LevelWarn))
expected := `
F:
/base/a/page_s.md
/base/a/regular.md
/base/symbolic1/s1.md
/base/symbolic1/s2.md
/base/symbolic3/circus/a/page_s.md
/base/symbolic3/circus/a/regular.md
D:
__bundle/en/base/symbolic2/a1/index.md/resources/en/base/symbolic2/a1/logo.png|en/base/symbolic2/a1/page.md
C:
/base/symbolic3/s1.png
/base/symbolic3/s2.png
`
got := strings.Replace(fileStore.sortedStr(), filepath.ToSlash(workDir), "", -1)
got = strings.Replace(got, "//", "/", -1)
if expected != got {
diff := helpers.DiffStringSlices(strings.Fields(expected), strings.Fields(got))
t.Log(got)
t.Fatalf("Failed:\n%s", diff)
}
}
func TestPageBundlerCapture(t *testing.T) {
t.Parallel()
assert := require.New(t)
cfg, fs := newTestBundleSources(t)
sourceSpec := source.NewSourceSpec(cfg, fs)
fileStore := &storeFilenames{}
c := newCapturer(newErrorLogger(), sourceSpec, fileStore, nil, filepath.FromSlash("/work/base"))
assert.NoError(c.capture())
expected := `
F:
/work/base/_1.md
/work/base/a/1.md
/work/base/a/2.md
/work/base/assets/pages/mypage.md
D:
__bundle/en/work/base/_index.md/resources/en/work/base/_1.png
__bundle/en/work/base/a/b/index.md/resources/en/work/base/a/b/ab1.md
__bundle/en/work/base/b/index.md/resources/en/work/base/b/1.md|en/work/base/b/2.md|en/work/base/b/c/logo.png|en/work/base/b/custom-mime.bep
C:
/work/base/assets/pic1.png
/work/base/assets/pic2.png
/work/base/images/hugo-logo.png
`
got := fileStore.sortedStr()
if expected != got {
diff := helpers.DiffStringSlices(strings.Fields(expected), strings.Fields(got))
t.Log(got)
t.Fatalf("Failed:\n%s", diff)
}
}
func TestPageBundlerCaptureMultilingual(t *testing.T) {
t.Parallel()
assert := require.New(t)
cfg, fs := newTestBundleSourcesMultilingual(t)
sourceSpec := source.NewSourceSpec(cfg, fs)
fileStore := &storeFilenames{}
c := newCapturer(newErrorLogger(), sourceSpec, fileStore, nil, filepath.FromSlash("/work/base"))
assert.NoError(c.capture())
expected := `
F:
/work/base/1s/mypage.md
/work/base/bb/_1.md
/work/base/bb/_1.nn.md
/work/base/bb/en.md
/work/base/bc/page.md
/work/base/bc/page.nn.md
/work/base/be/_index.md
/work/base/be/page.md
/work/base/be/page.nn.md
D:
__bundle/en/work/base/bb/_index.md/resources/en/work/base/bb/a.png|en/work/base/bb/b.png|nn/work/base/bb/c.nn.png
__bundle/en/work/base/bc/_index.md/resources/en/work/base/bc/logo-bc.png
__bundle/en/work/base/bd/index.md/resources/en/work/base/bd/page.md
__bundle/en/work/base/lb/index.md/resources/en/work/base/lb/1.md|en/work/base/lb/2.md|en/work/base/lb/c/d/deep.png|en/work/base/lb/c/logo.png|en/work/base/lb/c/one.png
__bundle/nn/work/base/bb/_index.nn.md/resources/en/work/base/bb/a.png|nn/work/base/bb/b.nn.png|nn/work/base/bb/c.nn.png
__bundle/nn/work/base/bd/index.md/resources/nn/work/base/bd/page.nn.md
__bundle/nn/work/base/lb/index.nn.md/resources/en/work/base/lb/c/d/deep.png|en/work/base/lb/c/one.png|nn/work/base/lb/2.nn.md|nn/work/base/lb/c/logo.nn.png
C:
/work/base/1s/mylogo.png
/work/base/bb/b/d.nn.png
`
got := fileStore.sortedStr()
if expected != got {
diff := helpers.DiffStringSlices(strings.Fields(expected), strings.Fields(got))
t.Log(got)
t.Fatalf("Failed:\n%s", diff)
}
}
type noOpFileStore int
func (noOpFileStore) handleSingles(fis ...*fileInfo) {}
func (noOpFileStore) handleBundles(b *bundleDirs) {}
func (noOpFileStore) handleCopyFiles(names ...string) {}
func BenchmarkPageBundlerCapture(b *testing.B) {
capturers := make([]*capturer, b.N)
for i := 0; i < b.N; i++ {
cfg, fs := newTestCfg()
sourceSpec := source.NewSourceSpec(cfg, fs)
base := fmt.Sprintf("base%d", i)
for j := 1; j <= 5; j++ {
js := fmt.Sprintf("j%d", j)
writeSource(b, fs, filepath.Join(base, js, "index.md"), "content")
writeSource(b, fs, filepath.Join(base, js, "logo1.png"), "content")
writeSource(b, fs, filepath.Join(base, js, "sub", "logo2.png"), "content")
writeSource(b, fs, filepath.Join(base, js, "section", "_index.md"), "content")
writeSource(b, fs, filepath.Join(base, js, "section", "logo.png"), "content")
writeSource(b, fs, filepath.Join(base, js, "section", "sub", "logo.png"), "content")
for k := 1; k <= 5; k++ {
ks := fmt.Sprintf("k%d", k)
writeSource(b, fs, filepath.Join(base, js, ks, "logo1.png"), "content")
writeSource(b, fs, filepath.Join(base, js, "section", ks, "logo.png"), "content")
}
}
for i := 1; i <= 5; i++ {
writeSource(b, fs, filepath.Join(base, "assetsonly", fmt.Sprintf("image%d.png", i)), "image")
}
for i := 1; i <= 5; i++ {
writeSource(b, fs, filepath.Join(base, "contentonly", fmt.Sprintf("c%d.md", i)), "content")
}
capturers[i] = newCapturer(newErrorLogger(), sourceSpec, new(noOpFileStore), nil, base)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
err := capturers[i].capture()
if err != nil {
b.Fatal(err)
}
}
}