package hugolib import ( "fmt" "strings" "testing" "path/filepath" "os" "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" ) func init() { testCommonResetState() jww.SetStdoutThreshold(jww.LevelCritical) } func testCommonResetState() { hugofs.InitMemFs() viper.Reset() viper.SetFs(hugofs.Source()) loadDefaultSettings() if err := hugofs.Source().Mkdir("content", 0755); err != nil { panic("Content folder creation failed.") } } func TestMultiSitesBuild(t *testing.T) { testCommonResetState() sites := createMultiTestSites(t, multiSiteTomlConfig) err := sites.Build(BuildCfg{}) if err != nil { t.Fatalf("Failed to build sites: %s", err) } enSite := sites.Sites[0] assert.Equal(t, "en", enSite.Language.Lang) if len(enSite.Pages) != 3 { t.Fatal("Expected 3 english pages") } assert.Len(t, enSite.Source.Files(), 11, "should have 11 source files") assert.Len(t, enSite.AllPages, 6, "should have 6 total pages (including translations)") doc1en := enSite.Pages[0] permalink, err := doc1en.Permalink() assert.NoError(t, err, "permalink call failed") assert.Equal(t, "http://example.com/blog/en/sect/doc1-slug/", permalink, "invalid doc1.en permalink") assert.Len(t, doc1en.Translations(), 1, "doc1-en should have one translation, excluding itself") doc2 := enSite.Pages[1] permalink, err = doc2.Permalink() assert.NoError(t, err, "permalink call failed") assert.Equal(t, "http://example.com/blog/en/sect/doc2/", permalink, "invalid doc2 permalink") doc3 := enSite.Pages[2] permalink, err = doc3.Permalink() assert.NoError(t, err, "permalink call failed") assert.Equal(t, "http://example.com/blog/superbob", permalink, "invalid doc3 permalink") // TODO(bep) multilingo. Check this case. This has url set in frontmatter, but we must split into lang folders // The assertion below was missing the /en prefix. assert.Equal(t, "/en/superbob", doc3.URL(), "invalid url, was specified on doc3 TODO(bep)") assert.Equal(t, doc2.Next, doc3, "doc3 should follow doc2, in .Next") doc1fr := doc1en.Translations()[0] permalink, err = doc1fr.Permalink() assert.NoError(t, err, "permalink call failed") assert.Equal(t, "http://example.com/blog/fr/sect/doc1/", permalink, "invalid doc1fr permalink") assert.Equal(t, doc1en.Translations()[0], doc1fr, "doc1-en should have doc1-fr as translation") assert.Equal(t, doc1fr.Translations()[0], doc1en, "doc1-fr should have doc1-en as translation") assert.Equal(t, "fr", doc1fr.Language().Lang) doc4 := enSite.AllPages[4] permalink, err = doc4.Permalink() assert.NoError(t, err, "permalink call failed") assert.Equal(t, "http://example.com/blog/fr/sect/doc4/", permalink, "invalid doc4 permalink") assert.Len(t, doc4.Translations(), 0, "found translations for doc4") doc5 := enSite.AllPages[5] permalink, err = doc5.Permalink() assert.NoError(t, err, "permalink call failed") assert.Equal(t, "http://example.com/blog/fr/somewhere/else/doc5", permalink, "invalid doc5 permalink") // Taxonomies and their URLs assert.Len(t, enSite.Taxonomies, 1, "should have 1 taxonomy") tags := enSite.Taxonomies["tags"] assert.Len(t, tags, 2, "should have 2 different tags") assert.Equal(t, tags["tag1"][0].Page, doc1en, "first tag1 page should be doc1") frSite := sites.Sites[1] assert.Equal(t, "fr", frSite.Language.Lang) assert.Len(t, frSite.Pages, 3, "should have 3 pages") assert.Len(t, frSite.AllPages, 6, "should have 6 total pages (including translations)") for _, frenchPage := range frSite.Pages { assert.Equal(t, "fr", frenchPage.Lang()) } // Check redirect to main language, French languageRedirect := readDestination(t, "public/index.html") require.True(t, strings.Contains(languageRedirect, "0; url=http://example.com/blog/fr"), languageRedirect) // Check sitemap(s) sitemapIndex := readDestination(t, "public/sitemap.xml") require.True(t, strings.Contains(sitemapIndex, "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") } func TestMultiSitesRebuild(t *testing.T) { testCommonResetState() sites := createMultiTestSites(t, multiSiteTomlConfig) cfg := BuildCfg{} 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") 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, 8) 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, 8) 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, 8) 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") }, }, } { 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) } } func TestAddNewLanguage(t *testing.T) { testCommonResetState() sites := createMultiTestSites(t, multiSiteTomlConfig) cfg := BuildCfg{} err := sites.Build(cfg) if err != nil { t.Fatalf("Failed to build sites: %s", err) } newConfig := multiSiteTomlConfig + ` [Languages.no] weight = 15 title = "Norsk" ` writeNewContentFile(t, "Norwegian Contentfile", "2016-01-01", "content/sect/doc1.no.md", 10) // replace the config writeSource(t, "multilangconfig.toml", newConfig) // Watching does not work with in-memory fs, so we trigger a reload manually require.NoError(t, viper.ReadInConfig()) err = sites.Build(BuildCfg{CreateSitesFromConfig: true}) if err != nil { t.Fatalf("Failed to rebuild sites: %s", err) } require.Len(t, sites.Sites, 3, fmt.Sprintf("Len %d", len(sites.Sites))) // The Norwegian site should be put in the middle (language weight=15) enSite := sites.Sites[0] noSite := sites.Sites[1] frSite := sites.Sites[2] require.True(t, enSite.Language.Lang == "en", enSite.Language.Lang) require.True(t, noSite.Language.Lang == "no", noSite.Language.Lang) require.True(t, frSite.Language.Lang == "fr", frSite.Language.Lang) require.Len(t, enSite.Pages, 3) require.Len(t, frSite.Pages, 3) // Veriy Norwegian site require.Len(t, noSite.Pages, 1) noPage := noSite.Pages[0] require.Equal(t, "Norwegian Contentfile", noPage.Title) require.Equal(t, "no", noPage.Lang()) require.Len(t, noPage.Translations(), 2) require.Len(t, noPage.AllTranslations(), 3) require.Equal(t, "en", noPage.Translations()[0].Lang()) //noFile := readDestination(t, "/public/no/doc1/index.html") //require.True(t, strings.Contains("foo", noFile), noFile) } var multiSiteTomlConfig = ` DefaultExtension = "html" baseurl = "http://example.com/blog" DisableSitemap = false DisableRSS = false RSSUri = "index.xml" paginate = 2 DefaultContentLanguage = "fr" [permalinks] other = "/somewhere/else/:filename" [Taxonomies] tag = "tags" [Languages] [Languages.en] weight = 10 title = "English" [Languages.fr] weight = 20 title = "Français" [Languages.fr.Taxonomies] plaque = "plaques" ` func createMultiTestSites(t *testing.T, tomlConfig string) *HugoSites { // Add some layouts if err := afero.WriteFile(hugofs.Source(), filepath.Join("layouts", "_default/single.html"), []byte("Single: {{ .Title }}|{{ i18n \"hello\" }} {{ .Content }}"), 0755); err != nil { t.Fatalf("Failed to write layout file: %s", err) } if err := afero.WriteFile(hugofs.Source(), filepath.Join("layouts", "_default/list.html"), []byte("List: {{ .Title }}"), 0755); err != nil { t.Fatalf("Failed to write layout file: %s", err) } if err := afero.WriteFile(hugofs.Source(), filepath.Join("layouts", "index.html"), []byte("Home: {{ .Title }}|{{ .IsHome }}"), 0755); err != nil { t.Fatalf("Failed to write layout file: %s", err) } // Add some language files if err := afero.WriteFile(hugofs.Source(), filepath.Join("i18n", "en.yaml"), []byte(` - id: hello translation: "Hello" `), 0755); err != nil { t.Fatalf("Failed to write language file: %s", err) } if err := afero.WriteFile(hugofs.Source(), filepath.Join("i18n", "fr.yaml"), []byte(` - id: hello translation: "Bonjour" `), 0755); err != nil { t.Fatalf("Failed to write language file: %s", err) } // Sources sources := []source.ByteSource{ {filepath.FromSlash("sect/doc1.en.md"), []byte(`--- title: doc1 slug: doc1-slug tags: - tag1 publishdate: "2000-01-01" --- # doc1 *some content* NOTE: slug should be used as URL `)}, {filepath.FromSlash("sect/doc1.fr.md"), []byte(`--- title: doc1 plaques: - frtag1 - frtag2 publishdate: "2000-01-04" --- # doc1 *quelque contenu* NOTE: should be in the 'en' Page's 'Translations' field. NOTE: date is after "doc3" `)}, {filepath.FromSlash("sect/doc2.en.md"), []byte(`--- title: doc2 publishdate: "2000-01-02" --- # doc2 *some content* NOTE: without slug, "doc2" should be used, without ".en" as URL `)}, {filepath.FromSlash("sect/doc3.en.md"), []byte(`--- title: doc3 publishdate: "2000-01-03" tags: - tag2 url: /superbob --- # doc3 *some content* NOTE: third 'en' doc, should trigger pagination on home page. `)}, {filepath.FromSlash("sect/doc4.md"), []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' `)}, {filepath.FromSlash("other/doc5.fr.md"), []byte(`--- title: doc5 publishdate: "2000-01-06" --- # doc5 *autre contenu francophone* NOTE: should use the "permalinks" configuration with :filename `)}, // Add some for the stats {filepath.FromSlash("stats/expired.fr.md"), []byte(`--- title: expired publishdate: "2000-01-06" expiryDate: "2001-01-06" --- # Expired `)}, {filepath.FromSlash("stats/future.fr.md"), []byte(`--- title: future publishdate: "2100-01-06" --- # Future `)}, {filepath.FromSlash("stats/expired.en.md"), []byte(`--- title: expired publishdate: "2000-01-06" expiryDate: "2001-01-06" --- # Expired `)}, {filepath.FromSlash("stats/future.en.md"), []byte(`--- title: future publishdate: "2100-01-06" --- # Future `)}, {filepath.FromSlash("stats/draft.en.md"), []byte(`--- title: expired publishdate: "2000-01-06" draft: true --- # Draft `)}, } writeSource(t, "multilangconfig.toml", tomlConfig) if err := LoadGlobalConfig("", "multilangconfig.toml"); 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) } } _, err := hugofs.Source().Open("content/other/doc5.fr.md") if err != nil { t.Fatalf("Unable to locate file") } sites, err := NewHugoSitesFromConfiguration() if err != nil { t.Fatalf("Failed to create sites: %s", err) } if len(sites.Sites) != 2 { 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 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) }