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:|

My Section Bundle Content Content.

\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 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 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|

P1 Content.

\n") b.AssertFileContent("public/p1/index.json", "Single JSON: P1|

P1 Content.

\n") b.EditFileReplaceAll("layouts/_default/single.html", "Single HTML", "Single HTML Edited").Build() b.AssertFileContent("public/p1/index.html", "Single HTML Edited: P1|

P1 Content.

\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|

P1 Content.

\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|

P1 Content.

\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|

P1 Content.

\n|R: Text: My Text|") b.EditFileReplaceAll("assets/mytext.txt", "My Text", "My Text Edited").Build() b.AssertFileContent("public/p1/index.html", "L1: P1|

P1 Content.

\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" disdableKinds = ["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 languge 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|

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|

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 }} {{ 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 }} {{ end }} -- layouts/shortcodes/jsfingerprinted.html -- {{ $js := site.Home.Resources.Get "foo.js" | fingerprint }} ` 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", ``) b.AssertFileContent("public/p1/index.html", "") 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", "") // 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:

Home.

") b.EditFileReplaceAll("content/_index.md", "Home.", "Home").Build() b.AssertFileContent("public/index.html", "Content:

Home

") } 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) }} {{ 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) }} {{ end }} -- layouts/shortcodes/notfingerprinted.html -- Fingerprinted. {{ $opts := dict "inlineImports" true "noMap" true }} {{ with (resources.Get "css/main.css" | postCSS $opts) }} {{ end }} -- layouts/index.html -- Home. {{ $opts := dict "inlineImports" true "noMap" true }} {{ with (resources.Get "css/main.css" | postCSS $opts) }} {{ 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) }} {{ end }} ` b := NewIntegrationTestBuilder( IntegrationTestConfig{ T: t, TxtarString: files, Running: true, NeedsOsFS: true, NeedsNpmInstall: true, // LogLevel: logg.LevelDebug, }, ).Build() b.AssertFileContent("public/index.html", "Home.", "") 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.", "") } 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) }} {{ 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" disdableKinds = ["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") }