hugo/hugolib/rebuild_test.go
2024-02-20 18:42:14 +01:00

1451 lines
41 KiB
Go

package hugolib
import (
"fmt"
"path/filepath"
"strings"
"testing"
"time"
"github.com/fortytw2/leaktest"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/htesting"
"github.com/gohugoio/hugo/resources/resource_transformers/tocss/dartsass"
"github.com/gohugoio/hugo/resources/resource_transformers/tocss/scss"
)
const rebuildFilesSimple = `
-- hugo.toml --
baseURL = "https://example.com"
disableKinds = ["term", "taxonomy", "sitemap", "robotstxt", "404"]
disableLiveReload = true
[outputs]
home = ["html"]
section = ["html"]
page = ["html"]
-- content/mysection/_index.md --
---
title: "My Section"
---
-- content/mysection/mysectionbundle/index.md --
---
title: "My Section Bundle"
---
My Section Bundle Content.
-- content/mysection/mysectionbundle/mysectionbundletext.txt --
My Section Bundle Text 2 Content.
-- content/mysection/mysectionbundle/mysectionbundlecontent.md --
---
title: "My Section Bundle Content"
---
My Section Bundle Content Content.
-- content/mysection/_index.md --
---
title: "My Section"
---
-- content/mysection/mysectiontext.txt --
-- content/_index.md --
---
title: "Home"
---
Home Content.
-- content/hometext.txt --
Home Text Content.
-- layouts/_default/single.html --
Single: {{ .Title }}|{{ .Content }}$
Resources: {{ range $i, $e := .Resources }}{{ $i }}:{{ .RelPermalink }}|{{ .Content }}|{{ end }}$
Len Resources: {{ len .Resources }}|
-- layouts/_default/list.html --
List: {{ .Title }}|{{ .Content }}$
Len Resources: {{ len .Resources }}|
Resources: {{ range $i, $e := .Resources }}{{ $i }}:{{ .RelPermalink }}|{{ .Content }}|{{ end }}$
-- layouts/shortcodes/foo.html --
Foo.
`
func TestRebuildEditTextFileInLeafBundle(t *testing.T) {
b := TestRunning(t, rebuildFilesSimple)
b.AssertFileContent("public/mysection/mysectionbundle/index.html",
"Resources: 0:/mysection/mysectionbundle/mysectionbundletext.txt|My Section Bundle Text 2 Content.|1:|<p>My Section Bundle Content Content.</p>\n|$")
b.EditFileReplaceAll("content/mysection/mysectionbundle/mysectionbundletext.txt", "Content.", "Content Edited.").Build()
b.AssertFileContent("public/mysection/mysectionbundle/index.html",
"Text 2 Content Edited")
b.AssertRenderCountPage(1)
b.AssertRenderCountContent(1)
}
func TestRebuiEditUnmarshaledYamlFileInLeafBundle(t *testing.T) {
files := `
-- hugo.toml --
baseURL = "https://example.com"
disableLiveReload = true
disableKinds = ["taxonomy", "term", "sitemap", "robotsTXT", "404", "rss"]
-- content/mybundle/index.md --
-- content/mybundle/mydata.yml --
foo: bar
-- layouts/_default/single.html --
MyData: {{ .Resources.Get "mydata.yml" | transform.Unmarshal }}|
`
b := TestRunning(t, files)
b.AssertFileContent("public/mybundle/index.html", "MyData: map[foo:bar]")
b.EditFileReplaceAll("content/mybundle/mydata.yml", "bar", "bar edited").Build()
b.AssertFileContent("public/mybundle/index.html", "MyData: map[foo:bar edited]")
}
func TestRebuildEditTextFileInHomeBundle(t *testing.T) {
b := TestRunning(t, rebuildFilesSimple)
b.AssertFileContent("public/index.html", "Home Content.")
b.AssertFileContent("public/index.html", "Home Text Content.")
b.EditFileReplaceAll("content/hometext.txt", "Content.", "Content Edited.").Build()
b.AssertFileContent("public/index.html", "Home Content.")
b.AssertFileContent("public/index.html", "Home Text Content Edited.")
b.AssertRenderCountPage(1)
b.AssertRenderCountContent(1)
}
func TestRebuildEditTextFileInBranchBundle(t *testing.T) {
b := TestRunning(t, rebuildFilesSimple)
b.AssertFileContent("public/mysection/index.html", "My Section")
b.EditFileReplaceAll("content/mysection/mysectiontext.txt", "Content.", "Content Edited.").Build()
b.AssertFileContent("public/mysection/index.html", "My Section")
b.AssertRenderCountPage(1)
b.AssertRenderCountContent(1)
}
func TestRebuildRenameTextFileInLeafBundle(t *testing.T) {
b := TestRunning(t, rebuildFilesSimple)
b.AssertFileContent("public/mysection/mysectionbundle/index.html", "My Section Bundle Text 2 Content.", "Len Resources: 2|")
b.RenameFile("content/mysection/mysectionbundle/mysectionbundletext.txt", "content/mysection/mysectionbundle/mysectionbundletext2.txt").Build()
b.AssertFileContent("public/mysection/mysectionbundle/index.html", "mysectionbundletext2", "My Section Bundle Text 2 Content.", "Len Resources: 2|")
b.AssertRenderCountPage(3)
b.AssertRenderCountContent(3)
}
func TestRebuilEditContentFileInLeafBundle(t *testing.T) {
b := TestRunning(t, rebuildFilesSimple)
b.AssertFileContent("public/mysection/mysectionbundle/index.html", "My Section Bundle Content Content.")
b.EditFileReplaceAll("content/mysection/mysectionbundle/mysectionbundlecontent.md", "Content Content.", "Content Content Edited.").Build()
b.AssertFileContent("public/mysection/mysectionbundle/index.html", "My Section Bundle Content Content Edited.")
}
func TestRebuildRenameTextFileInBranchBundle(t *testing.T) {
b := TestRunning(t, rebuildFilesSimple)
b.AssertFileContent("public/mysection/index.html", "My Section")
b.RenameFile("content/mysection/mysectiontext.txt", "content/mysection/mysectiontext2.txt").Build()
b.AssertFileContent("public/mysection/index.html", "mysectiontext2", "My Section")
b.AssertRenderCountPage(2)
b.AssertRenderCountContent(2)
}
func TestRebuildRenameTextFileInHomeBundle(t *testing.T) {
b := TestRunning(t, rebuildFilesSimple)
b.AssertFileContent("public/index.html", "Home Text Content.")
b.RenameFile("content/hometext.txt", "content/hometext2.txt").Build()
b.AssertFileContent("public/index.html", "hometext2", "Home Text Content.")
b.AssertRenderCountPage(2)
}
func TestRebuildRenameDirectoryWithLeafBundle(t *testing.T) {
b := TestRunning(t, rebuildFilesSimple)
b.RenameDir("content/mysection/mysectionbundle", "content/mysection/mysectionbundlerenamed").Build()
b.AssertFileContent("public/mysection/mysectionbundlerenamed/index.html", "My Section Bundle")
b.AssertRenderCountPage(1)
}
func TestRebuildRenameDirectoryWithBranchBundle(t *testing.T) {
b := TestRunning(t, rebuildFilesSimple)
b.RenameDir("content/mysection", "content/mysectionrenamed").Build()
b.AssertFileContent("public/mysectionrenamed/index.html", "My Section")
b.AssertFileContent("public/mysectionrenamed/mysectionbundle/index.html", "My Section Bundle")
b.AssertFileContent("public/mysectionrenamed/mysectionbundle/mysectionbundletext.txt", "My Section Bundle Text 2 Content.")
b.AssertRenderCountPage(2)
}
func TestRebuildRenameDirectoryWithRegularPageUsedInHome(t *testing.T) {
files := `
-- hugo.toml --
baseURL = "https://example.com"
disableLiveReload = true
-- content/foo/p1.md --
---
title: "P1"
---
-- layouts/index.html --
Pages: {{ range .Site.RegularPages }}{{ .RelPermalink }}|{{ end }}$
`
b := TestRunning(t, files)
b.AssertFileContent("public/index.html", "Pages: /foo/p1/|$")
b.RenameDir("content/foo", "content/bar").Build()
b.AssertFileContent("public/index.html", "Pages: /bar/p1/|$")
}
func TestRebuildAddRegularFileRegularPageUsedInHomeMultilingual(t *testing.T) {
files := `
-- hugo.toml --
baseURL = "https://example.com"
disableLiveReload = true
[languages]
[languages.en]
weight = 1
[languages.nn]
weight = 2
[languages.fr]
weight = 3
[languages.a]
weight = 4
[languages.b]
weight = 5
[languages.c]
weight = 6
[languages.d]
weight = 7
[languages.e]
weight = 8
[languages.f]
weight = 9
[languages.g]
weight = 10
[languages.h]
weight = 11
[languages.i]
weight = 12
[languages.j]
weight = 13
-- content/foo/_index.md --
-- content/foo/data.txt --
-- content/foo/p1.md --
-- content/foo/p1.nn.md --
-- content/foo/p1.fr.md --
-- content/foo/p1.a.md --
-- content/foo/p1.b.md --
-- content/foo/p1.c.md --
-- content/foo/p1.d.md --
-- content/foo/p1.e.md --
-- content/foo/p1.f.md --
-- content/foo/p1.g.md --
-- content/foo/p1.h.md --
-- content/foo/p1.i.md --
-- content/foo/p1.j.md --
-- layouts/index.html --
RegularPages: {{ range .Site.RegularPages }}{{ .RelPermalink }}|{{ end }}$
`
b := TestRunning(t, files)
b.AssertFileContent("public/index.html", "RegularPages: /foo/p1/|$")
b.AssertFileContent("public/nn/index.html", "RegularPages: /nn/foo/p1/|$")
b.AssertFileContent("public/i/index.html", "RegularPages: /i/foo/p1/|$")
b.AddFiles("content/foo/p2.md", ``).Build()
b.AssertFileContent("public/index.html", "RegularPages: /foo/p1/|/foo/p2/|$")
b.AssertFileContent("public/fr/index.html", "RegularPages: /fr/foo/p1/|$")
b.AddFiles("content/foo/p2.fr.md", ``).Build()
b.AssertFileContent("public/fr/index.html", "RegularPages: /fr/foo/p1/|/fr/foo/p2/|$")
b.AddFiles("content/foo/p2.i.md", ``).Build()
b.AssertFileContent("public/i/index.html", "RegularPages: /i/foo/p1/|/i/foo/p2/|$")
}
func TestRebuildRenameDirectoryWithBranchBundleFastRender(t *testing.T) {
recentlyVisited := types.NewEvictingStringQueue(10).Add("/a/b/c/")
b := TestRunning(t, rebuildFilesSimple, func(cfg *IntegrationTestConfig) { cfg.BuildCfg = BuildCfg{RecentlyVisited: recentlyVisited} })
b.RenameDir("content/mysection", "content/mysectionrenamed").Build()
b.AssertFileContent("public/mysectionrenamed/index.html", "My Section")
b.AssertFileContent("public/mysectionrenamed/mysectionbundle/index.html", "My Section Bundle")
b.AssertFileContent("public/mysectionrenamed/mysectionbundle/mysectionbundletext.txt", "My Section Bundle Text 2 Content.")
b.AssertRenderCountPage(2)
}
func TestRebuilErrorRecovery(t *testing.T) {
b := TestRunning(t, rebuildFilesSimple)
_, err := b.EditFileReplaceAll("content/mysection/mysectionbundle/index.md", "My Section Bundle Content.", "My Section Bundle Content\n\n\n\n{{< foo }}.").BuildE()
b.Assert(err, qt.Not(qt.IsNil))
b.Assert(err.Error(), qt.Contains, filepath.FromSlash(`"/content/mysection/mysectionbundle/index.md:8:9": unrecognized character`))
// Fix the error
b.EditFileReplaceAll("content/mysection/mysectionbundle/index.md", "{{< foo }}", "{{< foo >}}").Build()
}
func TestRebuildAddPageListPagesInHome(t *testing.T) {
files := `
-- hugo.toml --
baseURL = "https://example.com"
disableLiveReload = true
-- content/asection/s1.md --
-- content/p1.md --
---
title: "P1"
weight: 1
---
-- layouts/_default/single.html --
Single: {{ .Title }}|{{ .Content }}|
-- layouts/index.html --
Pages: {{ range .RegularPages }}{{ .RelPermalink }}|{{ end }}$
`
b := TestRunning(t, files)
b.AssertFileContent("public/index.html", "Pages: /p1/|$")
b.AddFiles("content/p2.md", ``).Build()
b.AssertFileContent("public/index.html", "Pages: /p1/|/p2/|$")
}
func TestRebuildScopedToOutputFormat(t *testing.T) {
files := `
-- hugo.toml --
baseURL = "https://example.com"
disableKinds = ["term", "taxonomy", "sitemap", "robotstxt", "404"]
disableLiveReload = true
-- content/p1.md --
---
title: "P1"
outputs: ["html", "json"]
---
P1 Content.
{{< myshort >}}
-- layouts/_default/single.html --
Single HTML: {{ .Title }}|{{ .Content }}|
-- layouts/_default/single.json --
Single JSON: {{ .Title }}|{{ .Content }}|
-- layouts/shortcodes/myshort.html --
My short.
`
b := Test(t, files, TestOptRunning())
b.AssertRenderCountPage(3)
b.AssertRenderCountContent(1)
b.AssertFileContent("public/p1/index.html", "Single HTML: P1|<p>P1 Content.</p>\n")
b.AssertFileContent("public/p1/index.json", "Single JSON: P1|<p>P1 Content.</p>\n")
b.EditFileReplaceAll("layouts/_default/single.html", "Single HTML", "Single HTML Edited").Build()
b.AssertFileContent("public/p1/index.html", "Single HTML Edited: P1|<p>P1 Content.</p>\n")
b.AssertRenderCountPage(1)
// Edit shortcode. Note that this is reused across all output formats.
b.EditFileReplaceAll("layouts/shortcodes/myshort.html", "My short", "My short edited").Build()
b.AssertFileContent("public/p1/index.html", "My short edited")
b.AssertFileContent("public/p1/index.json", "My short edited")
b.AssertRenderCountPage(3) // rss (uses .Content) + 2 single pages.
}
func TestRebuildBaseof(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
title = "Hugo Site"
baseURL = "https://example.com"
disableKinds = ["term", "taxonomy"]
disableLiveReload = true
-- layouts/_default/baseof.html --
Baseof: {{ .Title }}|
{{ block "main" . }}default{{ end }}
-- layouts/index.html --
{{ define "main" }}
Home: {{ .Title }}|{{ .Content }}|
{{ end }}
`
b := Test(t, files, TestOptRunning())
b.AssertFileContent("public/index.html", "Baseof: Hugo Site|", "Home: Hugo Site||")
b.EditFileReplaceFunc("layouts/_default/baseof.html", func(s string) string {
return strings.Replace(s, "Baseof", "Baseof Edited", 1)
}).Build()
b.AssertFileContent("public/index.html", "Baseof Edited: Hugo Site|", "Home: Hugo Site||")
}
func TestRebuildSingleWithBaseof(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
title = "Hugo Site"
baseURL = "https://example.com"
disableKinds = ["term", "taxonomy"]
disableLiveReload = true
-- content/p1.md --
---
title: "P1"
---
P1 Content.
-- layouts/_default/baseof.html --
Baseof: {{ .Title }}|
{{ block "main" . }}default{{ end }}
-- layouts/index.html --
Home.
-- layouts/_default/single.html --
{{ define "main" }}
Single: {{ .Title }}|{{ .Content }}|
{{ end }}
`
b := Test(t, files, TestOptRunning())
b.AssertFileContent("public/p1/index.html", "Baseof: P1|\n\nSingle: P1|<p>P1 Content.</p>\n|")
b.EditFileReplaceFunc("layouts/_default/single.html", func(s string) string {
return strings.Replace(s, "Single", "Single Edited", 1)
}).Build()
b.AssertFileContent("public/p1/index.html", "Baseof: P1|\n\nSingle Edited: P1|<p>P1 Content.</p>\n|")
}
func TestRebuildFromString(t *testing.T) {
files := `
-- hugo.toml --
baseURL = "https://example.com"
disableKinds = ["term", "taxonomy", "sitemap", "robotstxt", "404"]
disableLiveReload = true
-- content/p1.md --
---
title: "P1"
layout: "l1"
---
P1 Content.
-- content/p2.md --
---
title: "P2"
layout: "l2"
---
P2 Content.
-- assets/mytext.txt --
My Text
-- layouts/_default/l1.html --
{{ $r := partial "get-resource.html" . }}
L1: {{ .Title }}|{{ .Content }}|R: {{ $r.Content }}|
-- layouts/_default/l2.html --
L2.
-- layouts/partials/get-resource.html --
{{ $mytext := resources.Get "mytext.txt" }}
{{ $txt := printf "Text: %s" $mytext.Content }}
{{ $r := resources.FromString "r.txt" $txt }}
{{ return $r }}
`
b := TestRunning(t, files)
b.AssertFileContent("public/p1/index.html", "L1: P1|<p>P1 Content.</p>\n|R: Text: My Text|")
b.EditFileReplaceAll("assets/mytext.txt", "My Text", "My Text Edited").Build()
b.AssertFileContent("public/p1/index.html", "L1: P1|<p>P1 Content.</p>\n|R: Text: My Text Edited|")
b.AssertRenderCountPage(1)
}
func TestRebuildDeeplyNestedLink(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
baseURL = "https://example.com/"
disableKinds = ["term", "taxonomy", "sitemap", "robotstxt", "404"]
disableLiveReload = true
-- content/s/p1.md --
---
title: "P1"
---
-- content/s/p2.md --
---
title: "P2"
---
-- content/s/p3.md --
---
title: "P3"
---
-- content/s/p4.md --
---
title: "P4"
---
-- content/s/p5.md --
---
title: "P5"
---
-- content/s/p6.md --
---
title: "P6"
---
-- content/s/p7.md --
---
title: "P7"
---
-- layouts/_default/list.html --
List.
-- layouts/_default/single.html --
Single.
-- layouts/_default/single.html --
Next: {{ with .PrevInSection }}{{ .Title }}{{ end }}|
Prev: {{ with .NextInSection }}{{ .Title }}{{ end }}|
`
b := TestRunning(t, files)
b.AssertFileContent("public/s/p1/index.html", "Next: P2|")
b.EditFileReplaceAll("content/s/p7.md", "P7", "P7 Edited").Build()
b.AssertFileContent("public/s/p6/index.html", "Next: P7 Edited|")
}
func TestRebuildVariations(t *testing.T) {
// t.Parallel() not supported, see https://github.com/fortytw2/leaktest/issues/4
// This leaktest seems to be a little bit shaky on Travis.
if !htesting.IsCI() {
defer leaktest.CheckTimeout(t, 10*time.Second)()
}
files := `
-- hugo.toml --
baseURL = "https://example.com"
disableKinds = ["term", "taxonomy"]
disableLiveReload = true
defaultContentLanguage = "nn"
paginate = 20
[security]
enableInlineShortcodes = true
[languages]
[languages.en]
weight = 1
[languages.nn]
weight = 2
-- content/mysect/p1/index.md --
---
title: "P1"
---
P1 Content.
{{< include "mysect/p2" >}}
§§§go { page="mysect/p3" }
hello
§§§
{{< foo.inline >}}Foo{{< /foo.inline >}}
-- content/mysect/p2/index.md --
---
title: "P2"
---
P2 Content.
-- content/mysect/p3/index.md --
---
title: "P3"
---
P3 Content.
-- content/mysect/sub/_index.md --
-- content/mysect/sub/p4/index.md --
---
title: "P4"
---
P4 Content.
-- content/mysect/sub/p5/index.md --
---
title: "P5"
lastMod: 2019-03-02
---
P5 Content.
-- content/myothersect/_index.md --
---
cascade:
- _target:
cascadeparam: "cascadevalue"
---
-- content/myothersect/sub/_index.md --
-- content/myothersect/sub/p6/index.md --
---
title: "P6"
---
P6 Content.
-- content/translations/p7.en.md --
---
title: "P7 EN"
---
P7 EN Content.
-- content/translations/p7.nn.md --
---
title: "P7 NN"
---
P7 NN Content.
-- layouts/index.html --
Home: {{ .Title }}|{{ .Content }}|
RegularPages: {{ range .RegularPages }}{{ .RelPermalink }}|{{ end }}$
Len RegularPagesRecursive: {{ len .RegularPagesRecursive }}
Site.Lastmod: {{ .Site.Lastmod.Format "2006-01-02" }}|
Paginate: {{ range (.Paginate .Site.RegularPages).Pages }}{{ .RelPermalink }}|{{ .Title }}|{{ end }}$
-- layouts/_default/single.html --
Single: {{ .Title }}|{{ .Content }}|
Single Partial Cached: {{ partialCached "pcached" . }}|
Page.Lastmod: {{ .Lastmod.Format "2006-01-02" }}|
Cascade param: {{ .Params.cascadeparam }}|
-- layouts/_default/list.html --
List: {{ .Title }}|{{ .Content }}|
RegularPages: {{ range .RegularPages }}{{ .Title }}|{{ end }}$
Len RegularPagesRecursive: {{ len .RegularPagesRecursive }}
RegularPagesRecursive: {{ range .RegularPagesRecursive }}{{ .RelPermalink }}|{{ end }}$
List Partial P1: {{ partial "p1" . }}|
Page.Lastmod: {{ .Lastmod.Format "2006-01-02" }}|
Cascade param: {{ .Params.cascadeparam }}|
-- layouts/partials/p1.html --
Partial P1.
-- layouts/partials/pcached.html --
Partial Pcached.
-- layouts/shortcodes/include.html --
{{ $p := site.GetPage (.Get 0)}}
{{ with $p }}
Shortcode Include: {{ .Title }}|
{{ end }}
Shortcode .Page.Title: {{ .Page.Title }}|
Shortcode Partial P1: {{ partial "p1" . }}|
-- layouts/_default/_markup/render-codeblock.html --
{{ $p := site.GetPage (.Attributes.page)}}
{{ with $p }}
Codeblock Include: {{ .Title }}|
{{ end }}
`
b := NewIntegrationTestBuilder(
IntegrationTestConfig{
T: t,
TxtarString: files,
Running: true,
BuildCfg: BuildCfg{
testCounters: &buildCounters{},
},
// Verbose: true,
// LogLevel: logg.LevelTrace,
},
).Build()
// When running the server, this is done on shutdown.
// Do this here to satisfy the leak detector above.
defer func() {
b.Assert(b.H.Close(), qt.IsNil)
}()
contentRenderCount := b.counters.contentRenderCounter.Load()
pageRenderCount := b.counters.pageRenderCounter.Load()
b.Assert(contentRenderCount > 0, qt.IsTrue)
b.Assert(pageRenderCount > 0, qt.IsTrue)
// Test cases:
// - Edit content file direct
// - Edit content file transitive shortcode
// - Edit content file transitive render hook
// - Rename one language version of a content file
// - Delete content file, check site.RegularPages and section.RegularPagesRecursive (length)
// - Add content file (see above).
// - Edit shortcode
// - Edit inline shortcode
// - Edit render hook
// - Edit partial used in template
// - Edit partial used in shortcode
// - Edit partial cached.
// - Edit lastMod date in content file, check site.Lastmod.
editFile := func(filename string, replacementFunc func(s string) string) {
b.EditFileReplaceFunc(filename, replacementFunc).Build()
b.Assert(b.counters.contentRenderCounter.Load() < contentRenderCount, qt.IsTrue, qt.Commentf("count %d < %d", b.counters.contentRenderCounter.Load(), contentRenderCount))
b.Assert(b.counters.pageRenderCounter.Load() < pageRenderCount, qt.IsTrue, qt.Commentf("count %d < %d", b.counters.pageRenderCounter.Load(), pageRenderCount))
}
b.AssertFileContent("public/index.html", "RegularPages: $", "Len RegularPagesRecursive: 7", "Site.Lastmod: 2019-03-02")
b.AssertFileContent("public/mysect/p1/index.html",
"Single: P1|<p>P1 Content.",
"Shortcode Include: P2|",
"Codeblock Include: P3|")
editFile("content/mysect/p1/index.md", func(s string) string {
return strings.ReplaceAll(s, "P1", "P1 Edited")
})
b.AssertFileContent("public/mysect/p1/index.html", "Single: P1 Edited|<p>P1 Edited Content.")
b.AssertFileContent("public/index.html", "RegularPages: $", "Len RegularPagesRecursive: 7", "Paginate: /mysect/sub/p5/|P5|/mysect/p1/|P1 Edited")
b.AssertFileContent("public/mysect/index.html", "RegularPages: P1 Edited|P2|P3|$", "Len RegularPagesRecursive: 5")
// p2 is included in p1 via shortcode.
editFile("content/mysect/p2/index.md", func(s string) string {
return strings.ReplaceAll(s, "P2", "P2 Edited")
})
b.AssertFileContent("public/mysect/p1/index.html", "Shortcode Include: P2 Edited|")
// p3 is included in p1 via codeblock hook.
editFile("content/mysect/p3/index.md", func(s string) string {
return strings.ReplaceAll(s, "P3", "P3 Edited")
})
b.AssertFileContent("public/mysect/p1/index.html", "Codeblock Include: P3 Edited|")
// Remove a content file in a nested section.
b.RemoveFiles("content/mysect/sub/p4/index.md").Build()
b.AssertFileContent("public/mysect/index.html", "RegularPages: P1 Edited|P2 Edited|P3 Edited", "Len RegularPagesRecursive: 4")
b.AssertFileContent("public/mysect/sub/index.html", "RegularPages: P5|$", "RegularPagesRecursive: 1")
// Rename one of the translations.
b.AssertFileContent("public/translations/index.html", "RegularPagesRecursive: /translations/p7/")
b.AssertFileContent("public/en/translations/index.html", "RegularPagesRecursive: /en/translations/p7/")
b.RenameFile("content/translations/p7.nn.md", "content/translations/p7rename.nn.md").Build()
b.AssertFileContent("public/translations/index.html", "RegularPagesRecursive: /translations/p7rename/")
b.AssertFileContent("public/en/translations/index.html", "RegularPagesRecursive: /en/translations/p7/")
// Edit shortcode
editFile("layouts/shortcodes/include.html", func(s string) string {
return s + "\nShortcode Include Edited."
})
b.AssertFileContent("public/mysect/p1/index.html", "Shortcode Include Edited.")
// Edit render hook
editFile("layouts/_default/_markup/render-codeblock.html", func(s string) string {
return s + "\nCodeblock Include Edited."
})
b.AssertFileContent("public/mysect/p1/index.html", "Codeblock Include Edited.")
// Edit partial p1
editFile("layouts/partials/p1.html", func(s string) string {
return strings.Replace(s, "Partial P1", "Partial P1 Edited", 1)
})
b.AssertFileContent("public/mysect/index.html", "List Partial P1: Partial P1 Edited.")
b.AssertFileContent("public/mysect/p1/index.html", "Shortcode Partial P1: Partial P1 Edited.")
// Edit partial cached.
editFile("layouts/partials/pcached.html", func(s string) string {
return strings.Replace(s, "Partial Pcached", "Partial Pcached Edited", 1)
})
b.AssertFileContent("public/mysect/p1/index.html", "Pcached Edited.")
// Edit lastMod date in content file, check site.Lastmod.
editFile("content/mysect/sub/p5/index.md", func(s string) string {
return strings.Replace(s, "2019-03-02", "2020-03-10", 1)
})
b.AssertFileContent("public/index.html", "Site.Lastmod: 2020-03-10|")
b.AssertFileContent("public/mysect/index.html", "Page.Lastmod: 2020-03-10|")
// Adjust the date back a few days.
editFile("content/mysect/sub/p5/index.md", func(s string) string {
return strings.Replace(s, "2020-03-10", "2019-03-08", 1)
})
b.AssertFileContent("public/mysect/index.html", "Page.Lastmod: 2019-03-08|")
b.AssertFileContent("public/index.html", "Site.Lastmod: 2019-03-08|")
// Check cascade mods.
b.AssertFileContent("public/myothersect/index.html", "Cascade param: cascadevalue|")
b.AssertFileContent("public/myothersect/sub/index.html", "Cascade param: cascadevalue|")
b.AssertFileContent("public/myothersect/sub/p6/index.html", "Cascade param: cascadevalue|")
editFile("content/myothersect/_index.md", func(s string) string {
return strings.Replace(s, "cascadevalue", "cascadevalue edited", 1)
})
b.AssertFileContent("public/myothersect/index.html", "Cascade param: cascadevalue edited|")
b.AssertFileContent("public/myothersect/sub/p6/index.html", "Cascade param: cascadevalue edited|")
// Repurpose the cascadeparam to set the title.
editFile("content/myothersect/_index.md", func(s string) string {
return strings.Replace(s, "cascadeparam:", "title:", 1)
})
b.AssertFileContent("public/myothersect/sub/index.html", "Cascade param: |", "List: cascadevalue edited|")
// Revert it.
editFile("content/myothersect/_index.md", func(s string) string {
return strings.Replace(s, "title:", "cascadeparam:", 1)
})
b.AssertFileContent("public/myothersect/sub/index.html", "Cascade param: cascadevalue edited|", "List: |")
}
func TestRebuildVariationsJSNoneFingerprinted(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
baseURL = "https://example.com/"
disableKinds = ["term", "taxonomy", "sitemap", "robotsTXT", "404", "rss"]
disableLiveReload = true
-- content/p1/index.md --
---
title: "P1"
---
P1.
-- content/p2/index.md --
---
title: "P2"
---
P2.
-- content/p3/index.md --
---
title: "P3"
---
P3.
-- content/p4/index.md --
---
title: "P4"
---
P4.
-- assets/main.css --
body {
background: red;
}
-- layouts/default/list.html --
List.
-- layouts/_default/single.html --
Single.
{{ $css := resources.Get "main.css" | minify }}
RelPermalink: {{ $css.RelPermalink }}|
`
b := TestRunning(t, files)
b.AssertFileContent("public/p1/index.html", "RelPermalink: /main.min.css|")
b.AssertFileContent("public/main.min.css", "body{background:red}")
b.EditFileReplaceAll("assets/main.css", "red", "blue")
b.RemoveFiles("content/p2/index.md")
b.RemoveFiles("content/p3/index.md")
b.Build()
b.AssertFileContent("public/main.min.css", "body{background:blue}")
}
func TestRebuildVariationsJSInNestedCachedPartialFingerprinted(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
baseURL = "https://example.com/"
disableKinds = ["term", "taxonomy", "sitemap", "robotsTXT", "404", "rss"]
disableLiveReload = true
-- content/p1/index.md --
---
title: "P1"
---
P1.
-- content/p2/index.md --
---
title: "P2"
---
P2.
-- content/p3/index.md --
---
title: "P3"
---
P3.
-- content/p4/index.md --
---
title: "P4"
---
P4.
-- assets/js/main.js --
console.log("Hello");
-- layouts/_default/list.html --
List. {{ partial "head.html" . }}$
-- layouts/_default/single.html --
Single. {{ partial "head.html" . }}$
-- layouts/partials/head.html --
{{ partialCached "js.html" . }}$
-- layouts/partials/js.html --
{{ $js := resources.Get "js/main.js" | js.Build | fingerprint }}
RelPermalink: {{ $js.RelPermalink }}|
`
b := TestRunning(t, files)
b.AssertFileContent("public/p1/index.html", "/js/main.712a50b59d0f0dedb4e3606eaa3860b1f1a5305f6c42da30a2985e473ba314eb.js")
b.AssertFileContent("public/index.html", "/js/main.712a50b59d0f0dedb4e3606eaa3860b1f1a5305f6c42da30a2985e473ba314eb.js")
b.EditFileReplaceAll("assets/js/main.js", "Hello", "Hello is Edited").Build()
for i := 1; i < 5; i++ {
b.AssertFileContent(fmt.Sprintf("public/p%d/index.html", i), "/js/main.6535698cec9a21875f40ae03e96f30c4bee41a01e979224761e270b9034b2424.js")
}
b.AssertFileContent("public/index.html", "/js/main.6535698cec9a21875f40ae03e96f30c4bee41a01e979224761e270b9034b2424.js")
}
func TestRebuildVariationsJSInNestedPartialFingerprintedInBase(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
baseURL = "https://example.com/"
disableKinds = ["term", "taxonomy", "sitemap", "robotsTXT", "404", "rss"]
disableLiveReload = true
-- assets/js/main.js --
console.log("Hello");
-- layouts/_default/baseof.html --
Base. {{ partial "common/head.html" . }}$
{{ block "main" . }}default{{ end }}
-- layouts/_default/list.html --
{{ define "main" }}main{{ end }}
-- layouts/partials/common/head.html --
{{ partial "myfiles/js.html" . }}$
-- layouts/partials/myfiles/js.html --
{{ $js := resources.Get "js/main.js" | js.Build | fingerprint }}
RelPermalink: {{ $js.RelPermalink }}|
`
b := TestRunning(t, files)
b.AssertFileContent("public/index.html", "/js/main.712a50b59d0f0dedb4e3606eaa3860b1f1a5305f6c42da30a2985e473ba314eb.js")
b.EditFileReplaceAll("assets/js/main.js", "Hello", "Hello is Edited").Build()
b.AssertFileContent("public/index.html", "/js/main.6535698cec9a21875f40ae03e96f30c4bee41a01e979224761e270b9034b2424.js")
}
func TestRebuildVariationsJSBundled(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
baseURL = "https://example.com"
disableKinds = ["term", "taxonomy", "sitemap", "robotsTXT", "404", "rss"]
disableLiveReload = true
-- content/_index.md --
---
title: "Home"
---
-- content/p1.md --
---
title: "P1"
layout: "main"
---
-- content/p2.md --
---
title: "P2"
---
{{< jsfingerprinted >}}
-- content/p3.md --
---
title: "P3"
layout: "plain"
---
{{< jsfingerprinted >}}
-- content/main.js --
console.log("Hello");
-- content/foo.js --
console.log("Foo");
-- layouts/index.html --
Home.
{{ $js := site.Home.Resources.Get "main.js" }}
{{ with $js }}
<script src="{{ .RelPermalink }}"></script>
{{ end }}
-- layouts/_default/single.html --
Single. Deliberately no .Content in here.
-- layouts/_default/plain.html --
Content: {{ .Content }}|
-- layouts/_default/main.html --
{{ $js := site.Home.Resources.Get "main.js" }}
{{ with $js }}
<script>
{{ .Content }}
</script>
{{ end }}
-- layouts/shortcodes/jsfingerprinted.html --
{{ $js := site.Home.Resources.Get "foo.js" | fingerprint }}
<script src="{{ $js.RelPermalink }}"></script>
`
testCounters := &buildCounters{}
b := NewIntegrationTestBuilder(
IntegrationTestConfig{
T: t,
TxtarString: files,
Running: true,
// LogLevel: logg.LevelTrace,
// Verbose: true,
BuildCfg: BuildCfg{
testCounters: testCounters,
},
},
).Build()
b.AssertFileContent("public/index.html", `<script src="/main.js"></script>`)
b.AssertFileContent("public/p1/index.html", "<script>\n\"console.log(\\\"Hello\\\");\"\n</script>")
b.AssertFileContent("public/p2/index.html", "Single. Deliberately no .Content in here.")
b.AssertFileContent("public/p3/index.html", "foo.57b4465b908531b43d4e4680ab1063d856b475cb1ae81ad43e0064ecf607bec1.js")
b.AssertRenderCountPage(4)
// Edit JS file.
b.EditFileReplaceFunc("content/main.js", func(s string) string {
return strings.Replace(s, "Hello", "Hello is Edited", 1)
}).Build()
b.AssertFileContent("public/p1/index.html", "<script>\n\"console.log(\\\"Hello is Edited\\\");\"\n</script>")
// The p1 (the one inlining the JS) should be rebuilt.
b.AssertRenderCountPage(2)
// But not the content file.
b.AssertRenderCountContent(0)
// This is included with RelPermalink in a shortcode used in p3, but it's fingerprinted
// so we need to rebuild on change.
b.EditFileReplaceFunc("content/foo.js", func(s string) string {
return strings.Replace(s, "Foo", "Foo Edited", 1)
}).Build()
// Verify that the hash has changed.
b.AssertFileContent("public/p3/index.html", "foo.3a332a088521231e5fc9bd22f15e0ccf507faa7b373fbff22959005b9a80481c.js")
b.AssertRenderCountPage(1)
b.AssertRenderCountContent(1)
}
func TestRebuildEditData(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableLiveReload = true
[security]
enableInlineShortcodes=true
-- data/mydata.yaml --
foo: bar
-- content/_index.md --
---
title: "Home"
---
{{< data "mydata.foo" >}}}
-- content/p1.md --
---
title: "P1"
---
Foo inline: {{< foo.inline >}}{{ site.Data.mydata.foo }}|{{< /foo.inline >}}
-- layouts/shortcodes/data.html --
{{ $path := split (.Get 0) "." }}
{{ $data := index site.Data $path }}
Foo: {{ $data }}|
-- layouts/index.html --
Content: {{ .Content }}|
-- layouts/_default/single.html --
Single: {{ .Content }}|
`
b := TestRunning(t, files)
b.AssertFileContent("public/index.html", "Foo: bar|")
b.AssertFileContent("public/p1/index.html", "Foo inline: bar|")
b.EditFileReplaceFunc("data/mydata.yaml", func(s string) string {
return strings.Replace(s, "bar", "bar edited", 1)
}).Build()
b.AssertFileContent("public/index.html", "Foo: bar edited|")
b.AssertFileContent("public/p1/index.html", "Foo inline: bar edited|")
}
func TestRebuildEditHomeContent(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
baseURL = "https://example.com"
disableLiveReload = true
-- content/_index.md --
---
title: "Home"
---
Home.
-- layouts/index.html --
Content: {{ .Content }}
`
b := TestRunning(t, files)
b.AssertFileContent("public/index.html", "Content: <p>Home.</p>")
b.EditFileReplaceAll("content/_index.md", "Home.", "Home").Build()
b.AssertFileContent("public/index.html", "Content: <p>Home</p>")
}
func TestRebuildVariationsAssetsJSImport(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
baseURL = "https://example.com"
disableKinds = ["term", "taxonomy"]
disableLiveReload = true
-- layouts/index.html --
Home. {{ now }}
{{ with (resources.Get "js/main.js" | js.Build | fingerprint) }}
<script>{{ .Content | safeJS }}</script>
{{ end }}
-- assets/js/lib/foo.js --
export function foo() {
console.log("Foo");
}
-- assets/js/main.js --
import { foo } from "./lib/foo.js";
console.log("Hello");
foo();
`
b := NewIntegrationTestBuilder(
IntegrationTestConfig{
T: t,
TxtarString: files,
Running: true,
// LogLevel: logg.LevelTrace,
NeedsOsFS: true,
},
).Build()
b.AssertFileContent("public/index.html", "Home.", "Hello", "Foo")
// Edit the imported file.
b.EditFileReplaceAll("assets/js/lib/foo.js", "Foo", "Foo Edited").Build()
b.AssertFileContent("public/index.html", "Home.", "Hello", "Foo Edited")
}
func TestRebuildVariationsAssetsPostCSSImport(t *testing.T) {
if !htesting.IsCI() {
t.Skip("skip CI only")
}
files := `
-- hugo.toml --
baseURL = "https://example.com"
disableKinds = ["term", "taxonomy", "sitemap", "rss"]
disableLiveReload = true
-- assets/css/lib/foo.css --
body {
background: red;
}
-- assets/css/main.css --
@import "lib/foo.css";
-- package.json --
{
"devDependencies": {
"postcss-cli": "^9.0.1"
}
}
-- content/p1.md --
---
title: "P1"
---
-- content/p2.md --
---
title: "P2"
layout: "foo"
---
{{< fingerprinted >}}
-- content/p3.md --
---
title: "P3"
layout: "foo"
---
{{< notfingerprinted >}}
-- layouts/shortcodes/fingerprinted.html --
Fingerprinted.
{{ $opts := dict "inlineImports" true "noMap" true }}
{{ with (resources.Get "css/main.css" | postCSS $opts | fingerprint) }}
<style src="{{ .RelPermalink }}"></style>
{{ end }}
-- layouts/shortcodes/notfingerprinted.html --
Fingerprinted.
{{ $opts := dict "inlineImports" true "noMap" true }}
{{ with (resources.Get "css/main.css" | postCSS $opts) }}
<style src="{{ .RelPermalink }}"></style>
{{ end }}
-- layouts/index.html --
Home.
{{ $opts := dict "inlineImports" true "noMap" true }}
{{ with (resources.Get "css/main.css" | postCSS $opts) }}
<style>{{ .Content | safeCSS }}</style>
{{ end }}
-- layouts/_default/foo.html --
Foo.
{{ .Title }}|{{ .Content }}|
-- layouts/_default/single.html --
Single.
{{ $opts := dict "inlineImports" true "noMap" true }}
{{ with (resources.Get "css/main.css" | postCSS $opts) }}
<style src="{{ .RelPermalink }}"></style>
{{ end }}
`
b := NewIntegrationTestBuilder(
IntegrationTestConfig{
T: t,
TxtarString: files,
Running: true,
NeedsOsFS: true,
NeedsNpmInstall: true,
// LogLevel: logg.LevelDebug,
},
).Build()
b.AssertFileContent("public/index.html", "Home.", "<style>body {\n\tbackground: red;\n}</style>")
b.AssertFileContent("public/p1/index.html", "Single.", "/css/main.css")
b.AssertRenderCountPage(4)
// Edit the imported file.
b.EditFileReplaceFunc("assets/css/lib/foo.css", func(s string) string {
return strings.Replace(s, "red", "blue", 1)
}).Build()
b.AssertRenderCountPage(3)
b.AssertFileContent("public/index.html", "Home.", "<style>body {\n\tbackground: blue;\n}</style>")
}
func TestRebuildI18n(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
baseURL = "https://example.com"
disableLiveReload = true
-- i18n/en.toml --
hello = "Hello"
-- layouts/index.html --
Hello: {{ i18n "hello" }}
`
b := TestRunning(t, files)
b.AssertFileContent("public/index.html", "Hello: Hello")
b.EditFileReplaceAll("i18n/en.toml", "Hello", "Hugo").Build()
b.AssertFileContent("public/index.html", "Hello: Hugo")
}
func TestRebuildEditContentNonDefaultLanguage(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
baseURL = "https://example.com"
disableLiveReload = true
defaultContentLanguage = "en"
defaultContentLanguageInSubdir = true
[languages]
[languages.en]
weight = 1
[languages.nn]
weight = 2
-- content/p1/index.en.md --
---
title: "P1 en"
---
P1 en.
-- content/p1/b.en.md --
---
title: "B en"
---
B en.
-- content/p1/f1.en.txt --
F1 en
-- content/p1/index.nn.md --
---
title: "P1 nn"
---
P1 nn.
-- content/p1/b.nn.md --
---
title: "B nn"
---
B nn.
-- content/p1/f1.nn.txt --
F1 nn
-- layouts/_default/single.html --
Single: {{ .Title }}|{{ .Content }}|Bundled File: {{ with .Resources.GetMatch "f1.*" }}{{ .Content }}{{ end }}|Bundled Page: {{ with .Resources.GetMatch "b.*" }}{{ .Content }}{{ end }}|
`
b := TestRunning(t, files)
b.AssertFileContent("public/nn/p1/index.html", "Single: P1 nn|<p>P1 nn.</p>", "F1 nn|")
b.EditFileReplaceAll("content/p1/index.nn.md", "P1 nn.", "P1 nn edit.").Build()
b.AssertFileContent("public/nn/p1/index.html", "Single: P1 nn|<p>P1 nn edit.</p>\n|")
b.EditFileReplaceAll("content/p1/f1.nn.txt", "F1 nn", "F1 nn edit.").Build()
b.AssertFileContent("public/nn/p1/index.html", "Bundled File: F1 nn edit.")
b.EditFileReplaceAll("content/p1/b.nn.md", "B nn.", "B nn edit.").Build()
b.AssertFileContent("public/nn/p1/index.html", "B nn edit.")
}
func TestRebuildEditContentNonDefaultLanguageDifferentBundles(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
baseURL = "https://example.com"
disableLiveReload = true
defaultContentLanguage = "en"
defaultContentLanguageInSubdir = true
[languages]
[languages.en]
weight = 1
contentDir = "content/en"
[languages.nn]
weight = 2
contentDir = "content/nn"
-- content/en/p1en/index.md --
---
title: "P1 en"
---
-- content/nn/p1nn/index.md --
---
title: "P1 nn"
---
P1 nn.
-- layouts/_default/single.html --
Single: {{ .Title }}|{{ .Content }}|
`
b := TestRunning(t, files)
b.AssertFileContent("public/nn/p1nn/index.html", "Single: P1 nn|<p>P1 nn.</p>")
b.EditFileReplaceAll("content/nn/p1nn/index.md", "P1 nn.", "P1 nn edit.").Build()
b.AssertFileContent("public/nn/p1nn/index.html", "Single: P1 nn|<p>P1 nn edit.</p>\n|")
b.AssertFileContent("public/nn/p1nn/index.html", "P1 nn edit.")
}
func TestRebuildVariationsAssetsSassImport(t *testing.T) {
if !htesting.IsCI() {
t.Skip("skip CI only")
}
filesTemplate := `
-- hugo.toml --
baseURL = "https://example.com"
disableKinds = ["term", "taxonomy"]
disableLiveReload = true
-- assets/css/lib/foo.scss --
body {
background: red;
}
-- assets/css/main.scss --
@import "lib/foo";
-- layouts/index.html --
Home.
{{ $opts := dict "transpiler" "TRANSPILER" }}
{{ with (resources.Get "css/main.scss" | toCSS $opts) }}
<style>{{ .Content | safeCSS }}</style>
{{ end }}
`
runTest := func(transpiler string) {
t.Run(transpiler, func(t *testing.T) {
files := strings.Replace(filesTemplate, "TRANSPILER", transpiler, 1)
b := NewIntegrationTestBuilder(
IntegrationTestConfig{
T: t,
TxtarString: files,
Running: true,
NeedsOsFS: true,
},
).Build()
b.AssertFileContent("public/index.html", "Home.", "background: red")
// Edit the imported file.
b.EditFileReplaceFunc("assets/css/lib/foo.scss", func(s string) string {
return strings.Replace(s, "red", "blue", 1)
}).Build()
b.AssertFileContent("public/index.html", "Home.", "background: blue")
})
}
if scss.Supports() {
runTest("libsass")
}
if dartsass.Supports() {
runTest("dartsass")
}
}
func benchmarkFilesEdit(count int) string {
files := `
-- hugo.toml --
baseURL = "https://example.com"
disableKinds = ["term", "taxonomy"]
disableLiveReload = true
-- layouts/_default/single.html --
Single: {{ .Title }}|{{ .Content }}|
-- layouts/_default/list.html --
List: {{ .Title }}|{{ .Content }}|
-- content/mysect/_index.md --
---
title: "My Sect"
---
`
contentTemplate := `
---
title: "P%d"
---
P%d Content.
`
for i := 0; i < count; i++ {
files += fmt.Sprintf("-- content/mysect/p%d/index.md --\n%s", i, fmt.Sprintf(contentTemplate, i, i))
}
return files
}
func BenchmarkRebuildContentFileChange(b *testing.B) {
files := benchmarkFilesEdit(500)
cfg := IntegrationTestConfig{
T: b,
TxtarString: files,
Running: true,
// Verbose: true,
// LogLevel: logg.LevelInfo,
}
builders := make([]*IntegrationTestBuilder, b.N)
for i := range builders {
builders[i] = NewIntegrationTestBuilder(cfg)
builders[i].Build()
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
bb := builders[i]
bb.EditFileReplaceFunc("content/mysect/p123/index.md", func(s string) string {
return s + "... Edited"
}).Build()
// fmt.Println(bb.LogString())
}
}
func TestRebuildConcat(t *testing.T) {
files := `
-- hugo.toml --
baseURL = "https://example.com"
disableLiveReload = true
disableKinds = ["taxonomy", "term", "sitemap", "robotsTXT", "404", "rss"]
-- assets/a.css --
a
-- assets/b.css --
b
-- assets/c.css --
c
-- assets/common/c1.css --
c1
-- assets/common/c2.css --
c2
-- layouts/index.html --
{{ $a := resources.Get "a.css" }}
{{ $b := resources.Get "b.css" }}
{{ $common := resources.Match "common/*.css" | resources.Concat "common.css" | minify }}
{{ $ab := slice $a $b $common | resources.Concat "ab.css" }}
all: {{ $ab.RelPermalink }}
`
b := TestRunning(t, files)
b.AssertFileContent("public/ab.css", "abc1c2")
b.EditFileReplaceAll("assets/common/c2.css", "c2", "c2 edited").Build()
b.AssertFileContent("public/ab.css", "abc1c2 edited")
b.AddFiles("assets/common/c3.css", "c3").Build()
b.AssertFileContent("public/ab.css", "abc1c2 editedc3")
}