package hugolib import ( "bytes" "fmt" "strings" "testing" "os" "path/filepath" "text/template" "github.com/fortytw2/leaktest" "github.com/fsnotify/fsnotify" "github.com/spf13/afero" "github.com/spf13/hugo/helpers" "github.com/spf13/hugo/hugofs" "github.com/spf13/hugo/source" jww "github.com/spf13/jwalterweatherman" "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) type testSiteConfig struct { DefaultContentLanguage string } func init() { testCommonResetState() jww.SetStdoutThreshold(jww.LevelCritical) } func testCommonResetState() { hugofs.InitMemFs() viper.Reset() viper.SetFs(hugofs.Source()) loadDefaultSettings() // Default is false, but true is easier to use as default in tests viper.Set("DefaultContentLanguageInSubdir", true) if err := hugofs.Source().Mkdir("content", 0755); err != nil { panic("Content folder creation failed.") } } func TestMultiSitesMainLangInRoot(t *testing.T) { for _, b := range []bool{false, true} { doTestMultiSitesMainLangInRoot(t, b) } } func doTestMultiSitesMainLangInRoot(t *testing.T, defaultInSubDir bool) { testCommonResetState() viper.Set("DefaultContentLanguageInSubdir", defaultInSubDir) siteConfig := testSiteConfig{DefaultContentLanguage: "fr"} sites := createMultiTestSites(t, siteConfig, multiSiteTOMLConfigTemplate) err := sites.Build(BuildCfg{}) if err != nil { t.Fatalf("Failed to build sites: %s", err) } require.Len(t, sites.Sites, 4) enSite := sites.Sites[0] frSite := sites.Sites[1] require.Equal(t, "/en", enSite.Info.LanguagePrefix) if defaultInSubDir { require.Equal(t, "/fr", frSite.Info.LanguagePrefix) } else { require.Equal(t, "", frSite.Info.LanguagePrefix) } doc1en := enSite.Pages[0] doc1fr := frSite.Pages[0] enPerm, _ := doc1en.Permalink() enRelPerm, _ := doc1en.RelPermalink() require.Equal(t, "http://example.com/blog/en/sect/doc1-slug/", enPerm) require.Equal(t, "/blog/en/sect/doc1-slug/", enRelPerm) frPerm, _ := doc1fr.Permalink() frRelPerm, _ := doc1fr.RelPermalink() // Main language in root require.Equal(t, replaceDefaultContentLanguageValue("http://example.com/blog/fr/sect/doc1/", defaultInSubDir), frPerm) require.Equal(t, replaceDefaultContentLanguageValue("/blog/fr/sect/doc1/", defaultInSubDir), frRelPerm) assertFileContent(t, "public/fr/sect/doc1/index.html", defaultInSubDir, "Single", "Bonjour") assertFileContent(t, "public/en/sect/doc1-slug/index.html", defaultInSubDir, "Single", "Hello") // Check home if defaultInSubDir { // should have a redirect on top level. assertFileContent(t, "public/index.html", true, ``) } else { // should have redirect back to root assertFileContent(t, "public/fr/index.html", true, ``) } assertFileContent(t, "public/fr/index.html", defaultInSubDir, "Home", "Bonjour") assertFileContent(t, "public/en/index.html", defaultInSubDir, "Home", "Hello") // Check list pages assertFileContent(t, "public/fr/sect/index.html", defaultInSubDir, "List", "Bonjour") assertFileContent(t, "public/en/sect/index.html", defaultInSubDir, "List", "Hello") assertFileContent(t, "public/fr/plaques/frtag1/index.html", defaultInSubDir, "List", "Bonjour") assertFileContent(t, "public/en/tags/tag1/index.html", defaultInSubDir, "List", "Hello") // Check sitemaps // Sitemaps behaves different: In a multilanguage setup there will always be a index file and // one sitemap in each lang folder. assertFileContent(t, "public/sitemap.xml", true, "http://example.com/blog/en/sitemap.xml", "http://example.com/blog/fr/sitemap.xml") if defaultInSubDir { assertFileContent(t, "public/fr/sitemap.xml", true, "http://example.com/blog/fr/") } else { assertFileContent(t, "public/fr/sitemap.xml", true, "http://example.com/blog/") } assertFileContent(t, "public/en/sitemap.xml", true, "http://example.com/blog/en/") // Check rss assertFileContent(t, "public/fr/index.xml", defaultInSubDir, `http://example.com/blog/en/sitemap.xml"), sitemapIndex) require.True(t, strings.Contains(sitemapIndex, "http://example.com/blog/fr/sitemap.xml"), sitemapIndex) sitemapEn := readDestination(t, "public/en/sitemap.xml") sitemapFr := readDestination(t, "public/fr/sitemap.xml") require.True(t, strings.Contains(sitemapEn, "http://example.com/blog/en/sect/doc2/"), sitemapEn) require.True(t, strings.Contains(sitemapFr, "http://example.com/blog/fr/sect/doc1/"), sitemapFr) // Check taxonomies enTags := enSite.Taxonomies["tags"] frTags := frSite.Taxonomies["plaques"] require.Len(t, enTags, 2, fmt.Sprintf("Tags in en: %v", enTags)) require.Len(t, frTags, 2, fmt.Sprintf("Tags in fr: %v", frTags)) require.NotNil(t, enTags["tag1"]) require.NotNil(t, frTags["frtag1"]) readDestination(t, "public/fr/plaques/frtag1/index.html") readDestination(t, "public/en/tags/tag1/index.html") // Check Blackfriday config assert.True(t, strings.Contains(string(doc1fr.Content), "«"), string(doc1fr.Content)) assert.False(t, strings.Contains(string(doc1en.Content), "«"), string(doc1en.Content)) assert.True(t, strings.Contains(string(doc1en.Content), "“"), string(doc1en.Content)) // Check that the drafts etc. are not built/processed/rendered. assertShouldNotBuild(t, sites) // en and nn have custom site menus require.Len(t, frSite.Menus, 0, "fr: "+configSuffix) require.Len(t, enSite.Menus, 1, "en: "+configSuffix) require.Len(t, nnSite.Menus, 1, "nn: "+configSuffix) require.Equal(t, "Home", enSite.Menus["main"].ByName()[0].Name) require.Equal(t, "Heim", nnSite.Menus["main"].ByName()[0].Name) } func TestMultiSitesRebuild(t *testing.T) { defer leaktest.Check(t)() testCommonResetState() siteConfig := testSiteConfig{DefaultContentLanguage: "fr"} sites := createMultiTestSites(t, siteConfig, multiSiteTOMLConfigTemplate) cfg := BuildCfg{Watching: true} err := sites.Build(cfg) if err != nil { t.Fatalf("Failed to build sites: %s", err) } _, err = hugofs.Destination().Open("public/en/sect/doc2/index.html") if err != nil { t.Fatalf("Unable to locate file") } enSite := sites.Sites[0] frSite := sites.Sites[1] assert.Len(t, enSite.Pages, 3) assert.Len(t, frSite.Pages, 3) // Verify translations docEn := readDestination(t, "public/en/sect/doc1-slug/index.html") assert.True(t, strings.Contains(docEn, "Hello"), "No Hello") docFr := readDestination(t, "public/fr/sect/doc1/index.html") assert.True(t, strings.Contains(docFr, "Bonjour"), "No Bonjour") // check single page content assertFileContent(t, "public/fr/sect/doc1/index.html", true, "Single", "Shortcode: Bonjour") assertFileContent(t, "public/en/sect/doc1-slug/index.html", true, "Single", "Shortcode: Hello") for i, this := range []struct { preFunc func(t *testing.T) events []fsnotify.Event assertFunc func(t *testing.T) }{ // * Remove doc // * Add docs existing languages // (Add doc new language: TODO(bep) we should load config.toml as part of these so we can add languages). // * Rename file // * Change doc // * Change a template // * Change language file { nil, []fsnotify.Event{{Name: "content/sect/doc2.en.md", Op: fsnotify.Remove}}, func(t *testing.T) { assert.Len(t, enSite.Pages, 2, "1 en removed") // Check build stats assert.Equal(t, 1, enSite.draftCount, "Draft") assert.Equal(t, 1, enSite.futureCount, "Future") assert.Equal(t, 1, enSite.expiredCount, "Expired") assert.Equal(t, 0, frSite.draftCount, "Draft") assert.Equal(t, 1, frSite.futureCount, "Future") assert.Equal(t, 1, frSite.expiredCount, "Expired") }, }, { func(t *testing.T) { writeNewContentFile(t, "new_en_1", "2016-07-31", "content/new1.en.md", -5) writeNewContentFile(t, "new_en_2", "1989-07-30", "content/new2.en.md", -10) writeNewContentFile(t, "new_fr_1", "2016-07-30", "content/new1.fr.md", 10) }, []fsnotify.Event{ {Name: "content/new1.en.md", Op: fsnotify.Create}, {Name: "content/new2.en.md", Op: fsnotify.Create}, {Name: "content/new1.fr.md", Op: fsnotify.Create}, }, func(t *testing.T) { assert.Len(t, enSite.Pages, 4) assert.Len(t, enSite.AllPages, 10) assert.Len(t, frSite.Pages, 4) assert.Equal(t, "new_fr_1", frSite.Pages[3].Title) assert.Equal(t, "new_en_2", enSite.Pages[0].Title) assert.Equal(t, "new_en_1", enSite.Pages[1].Title) rendered := readDestination(t, "public/en/new1/index.html") assert.True(t, strings.Contains(rendered, "new_en_1"), rendered) }, }, { func(t *testing.T) { p := "content/sect/doc1.en.md" doc1 := readSource(t, p) doc1 += "CHANGED" writeSource(t, p, doc1) }, []fsnotify.Event{{Name: "content/sect/doc1.en.md", Op: fsnotify.Write}}, func(t *testing.T) { assert.Len(t, enSite.Pages, 4) doc1 := readDestination(t, "public/en/sect/doc1-slug/index.html") assert.True(t, strings.Contains(doc1, "CHANGED"), doc1) }, }, // Rename a file { func(t *testing.T) { if err := hugofs.Source().Rename("content/new1.en.md", "content/new1renamed.en.md"); err != nil { t.Fatalf("Rename failed: %s", err) } }, []fsnotify.Event{ {Name: "content/new1renamed.en.md", Op: fsnotify.Rename}, {Name: "content/new1.en.md", Op: fsnotify.Rename}, }, func(t *testing.T) { assert.Len(t, enSite.Pages, 4, "Rename") assert.Equal(t, "new_en_1", enSite.Pages[1].Title) rendered := readDestination(t, "public/en/new1renamed/index.html") assert.True(t, strings.Contains(rendered, "new_en_1"), rendered) }}, { // Change a template func(t *testing.T) { template := "layouts/_default/single.html" templateContent := readSource(t, template) templateContent += "{{ print \"Template Changed\"}}" writeSource(t, template, templateContent) }, []fsnotify.Event{{Name: "layouts/_default/single.html", Op: fsnotify.Write}}, func(t *testing.T) { assert.Len(t, enSite.Pages, 4) assert.Len(t, enSite.AllPages, 10) assert.Len(t, frSite.Pages, 4) doc1 := readDestination(t, "public/en/sect/doc1-slug/index.html") assert.True(t, strings.Contains(doc1, "Template Changed"), doc1) }, }, { // Change a language file func(t *testing.T) { languageFile := "i18n/fr.yaml" langContent := readSource(t, languageFile) langContent = strings.Replace(langContent, "Bonjour", "Salut", 1) writeSource(t, languageFile, langContent) }, []fsnotify.Event{{Name: "i18n/fr.yaml", Op: fsnotify.Write}}, func(t *testing.T) { assert.Len(t, enSite.Pages, 4) assert.Len(t, enSite.AllPages, 10) assert.Len(t, frSite.Pages, 4) docEn := readDestination(t, "public/en/sect/doc1-slug/index.html") assert.True(t, strings.Contains(docEn, "Hello"), "No Hello") docFr := readDestination(t, "public/fr/sect/doc1/index.html") assert.True(t, strings.Contains(docFr, "Salut"), "No Salut") homeEn := enSite.getNode("home-0") require.NotNil(t, homeEn) require.Len(t, homeEn.Translations(), 3) require.Equal(t, "fr", homeEn.Translations()[0].Lang()) }, }, // Change a shortcode { func(t *testing.T) { writeSource(t, "layouts/shortcodes/shortcode.html", "Modified Shortcode: {{ i18n \"hello\" }}") }, []fsnotify.Event{ {Name: "layouts/shortcodes/shortcode.html", Op: fsnotify.Write}, }, func(t *testing.T) { assert.Len(t, enSite.Pages, 4) assert.Len(t, enSite.AllPages, 10) assert.Len(t, frSite.Pages, 4) assertFileContent(t, "public/fr/sect/doc1/index.html", true, "Single", "Modified Shortcode: Salut") assertFileContent(t, "public/en/sect/doc1-slug/index.html", true, "Single", "Modified Shortcode: Hello") }, }, } { if this.preFunc != nil { this.preFunc(t) } err = sites.Rebuild(cfg, this.events...) if err != nil { t.Fatalf("[%d] Failed to rebuild sites: %s", i, err) } this.assertFunc(t) } // Check that the drafts etc. are not built/processed/rendered. assertShouldNotBuild(t, sites) } func assertShouldNotBuild(t *testing.T, sites *HugoSites) { s := sites.Sites[0] for _, p := range s.rawAllPages { // No HTML when not processed require.Equal(t, p.shouldBuild(), bytes.Contains(p.rawContent, []byte("}} NOTE: slug should be used as URL `)}, {Name: filepath.FromSlash("sect/doc1.fr.md"), Content: []byte(`--- title: doc1 plaques: - frtag1 - frtag2 publishdate: "2000-01-04" --- # doc1 *quelque "contenu"* {{< shortcode >}} NOTE: should be in the 'en' Page's 'Translations' field. NOTE: date is after "doc3" `)}, {Name: filepath.FromSlash("sect/doc2.en.md"), Content: []byte(`--- title: doc2 publishdate: "2000-01-02" --- # doc2 *some content* NOTE: without slug, "doc2" should be used, without ".en" as URL `)}, {Name: filepath.FromSlash("sect/doc3.en.md"), Content: []byte(`--- title: doc3 publishdate: "2000-01-03" tags: - tag2 - tag1 url: /superbob --- # doc3 *some content* NOTE: third 'en' doc, should trigger pagination on home page. `)}, {Name: filepath.FromSlash("sect/doc4.md"), Content: []byte(`--- title: doc4 plaques: - frtag1 publishdate: "2000-01-05" --- # doc4 *du contenu francophone* NOTE: should use the DefaultContentLanguage and mark this doc as 'fr'. NOTE: doesn't have any corresponding translation in 'en' `)}, {Name: filepath.FromSlash("other/doc5.fr.md"), Content: []byte(`--- title: doc5 publishdate: "2000-01-06" --- # doc5 *autre contenu francophone* NOTE: should use the "permalinks" configuration with :filename `)}, // Add some for the stats {Name: filepath.FromSlash("stats/expired.fr.md"), Content: []byte(`--- title: expired publishdate: "2000-01-06" expiryDate: "2001-01-06" --- # Expired `)}, {Name: filepath.FromSlash("stats/future.fr.md"), Content: []byte(`--- title: future publishdate: "2100-01-06" --- # Future `)}, {Name: filepath.FromSlash("stats/expired.en.md"), Content: []byte(`--- title: expired publishdate: "2000-01-06" expiryDate: "2001-01-06" --- # Expired `)}, {Name: filepath.FromSlash("stats/future.en.md"), Content: []byte(`--- title: future publishdate: "2100-01-06" --- # Future `)}, {Name: filepath.FromSlash("stats/draft.en.md"), Content: []byte(`--- title: expired publishdate: "2000-01-06" draft: true --- # Draft `)}, {Name: filepath.FromSlash("stats/tax.nn.md"), Content: []byte(`--- title: Tax NN publishdate: "2000-01-06" weight: 1001 lag: - Sogndal --- # Tax NN `)}, {Name: filepath.FromSlash("stats/tax.nb.md"), Content: []byte(`--- title: Tax NB publishdate: "2000-01-06" weight: 1002 lag: - Sogndal --- # Tax NB `)}, } configFile := "multilangconfig." + configSuffix writeSource(t, configFile, configContent) if err := LoadGlobalConfig("", configFile); err != nil { t.Fatalf("Failed to load config: %s", err) } // Hugo support using ByteSource's directly (for testing), // but to make it more real, we write them to the mem file system. for _, s := range sources { if err := afero.WriteFile(hugofs.Source(), filepath.Join("content", s.Name), s.Content, 0755); err != nil { t.Fatalf("Failed to write file: %s", err) } } // Add some data writeSource(t, "data/hugo.toml", "slogan = \"Hugo Rocks!\"") sites, err := NewHugoSitesFromConfiguration() if err != nil { t.Fatalf("Failed to create sites: %s", err) } if len(sites.Sites) != 4 { t.Fatalf("Got %d sites", len(sites.Sites)) } return sites } func writeSource(t *testing.T, filename, content string) { if err := afero.WriteFile(hugofs.Source(), filepath.FromSlash(filename), []byte(content), 0755); err != nil { t.Fatalf("Failed to write file: %s", err) } } func readDestination(t *testing.T, filename string) string { return readFileFromFs(t, hugofs.Destination(), filename) } func destinationExists(filename string) bool { b, err := helpers.Exists(filename, hugofs.Destination()) if err != nil { panic(err) } return b } func readSource(t *testing.T, filename string) string { return readFileFromFs(t, hugofs.Source(), filename) } func readFileFromFs(t *testing.T, fs afero.Fs, filename string) string { filename = filepath.FromSlash(filename) b, err := afero.ReadFile(fs, filename) if err != nil { // Print some debug info root := strings.Split(filename, helpers.FilePathSeparator)[0] afero.Walk(fs, root, func(path string, info os.FileInfo, err error) error { if info != nil && !info.IsDir() { fmt.Println(" ", path) } return nil }) t.Fatalf("Failed to read file: %s", err) } return string(b) } const testPageTemplate = `--- title: "%s" publishdate: "%s" weight: %d --- # Doc %s ` func newTestPage(title, date string, weight int) string { return fmt.Sprintf(testPageTemplate, title, date, weight, title) } func writeNewContentFile(t *testing.T, title, date, filename string, weight int) { content := newTestPage(title, date, weight) writeSource(t, filename, content) } func createConfig(t *testing.T, config testSiteConfig, configTemplate string) string { templ, err := template.New("test").Parse(configTemplate) if err != nil { t.Fatal("Template parse failed:", err) } var b bytes.Buffer templ.Execute(&b, config) return b.String() }