github.com/kovansky/hugo@v0.92.3-0.20220224232819-63076e4ff19f/hugolib/datafiles_test.go (about)

     1  // Copyright 2019 The Hugo Authors. All rights reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  // http://www.apache.org/licenses/LICENSE-2.0
     7  //
     8  // Unless required by applicable law or agreed to in writing, software
     9  // distributed under the License is distributed on an "AS IS" BASIS,
    10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package hugolib
    15  
    16  import (
    17  	"fmt"
    18  	"path/filepath"
    19  	"reflect"
    20  	"runtime"
    21  	"testing"
    22  
    23  	"github.com/gohugoio/hugo/common/loggers"
    24  
    25  	"github.com/gohugoio/hugo/deps"
    26  
    27  	qt "github.com/frankban/quicktest"
    28  )
    29  
    30  func TestDataDir(t *testing.T) {
    31  	t.Parallel()
    32  	equivDataDirs := make([]dataDir, 3)
    33  	equivDataDirs[0].addSource("data/test/a.json", `{ "b" : { "c1": "red" , "c2": "blue" } }`)
    34  	equivDataDirs[1].addSource("data/test/a.yaml", "b:\n  c1: red\n  c2: blue")
    35  	equivDataDirs[2].addSource("data/test/a.toml", "[b]\nc1 = \"red\"\nc2 = \"blue\"\n")
    36  	expected := map[string]interface{}{
    37  		"test": map[string]interface{}{
    38  			"a": map[string]interface{}{
    39  				"b": map[string]interface{}{
    40  					"c1": "red",
    41  					"c2": "blue",
    42  				},
    43  			},
    44  		},
    45  	}
    46  	doTestEquivalentDataDirs(t, equivDataDirs, expected)
    47  }
    48  
    49  // Unable to enforce equivalency for int values as
    50  // the JSON, YAML and TOML parsers return
    51  // float64, int, int64 respectively. They all return
    52  // float64 for float values though:
    53  func TestDataDirNumeric(t *testing.T) {
    54  	t.Parallel()
    55  	equivDataDirs := make([]dataDir, 3)
    56  	equivDataDirs[0].addSource("data/test/a.json", `{ "b" : { "c1": 1.7 , "c2": 2.9 } }`)
    57  	equivDataDirs[1].addSource("data/test/a.yaml", "b:\n  c1: 1.7\n  c2: 2.9")
    58  	equivDataDirs[2].addSource("data/test/a.toml", "[b]\nc1 = 1.7\nc2 = 2.9\n")
    59  	expected := map[string]interface{}{
    60  		"test": map[string]interface{}{
    61  			"a": map[string]interface{}{
    62  				"b": map[string]interface{}{
    63  					"c1": 1.7,
    64  					"c2": 2.9,
    65  				},
    66  			},
    67  		},
    68  	}
    69  	doTestEquivalentDataDirs(t, equivDataDirs, expected)
    70  }
    71  
    72  func TestDataDirBoolean(t *testing.T) {
    73  	t.Parallel()
    74  	equivDataDirs := make([]dataDir, 3)
    75  	equivDataDirs[0].addSource("data/test/a.json", `{ "b" : { "c1": true , "c2": false } }`)
    76  	equivDataDirs[1].addSource("data/test/a.yaml", "b:\n  c1: true\n  c2: false")
    77  	equivDataDirs[2].addSource("data/test/a.toml", "[b]\nc1 = true\nc2 = false\n")
    78  	expected := map[string]interface{}{
    79  		"test": map[string]interface{}{
    80  			"a": map[string]interface{}{
    81  				"b": map[string]interface{}{
    82  					"c1": true,
    83  					"c2": false,
    84  				},
    85  			},
    86  		},
    87  	}
    88  	doTestEquivalentDataDirs(t, equivDataDirs, expected)
    89  }
    90  
    91  func TestDataDirTwoFiles(t *testing.T) {
    92  	t.Parallel()
    93  	equivDataDirs := make([]dataDir, 3)
    94  
    95  	equivDataDirs[0].addSource("data/test/foo.json", `{ "bar": "foofoo"  }`)
    96  	equivDataDirs[0].addSource("data/test.json", `{ "hello": [ "world", "foo" ] }`)
    97  
    98  	equivDataDirs[1].addSource("data/test/foo.yaml", "bar: foofoo")
    99  	equivDataDirs[1].addSource("data/test.yaml", "hello:\n- world\n- foo")
   100  
   101  	equivDataDirs[2].addSource("data/test/foo.toml", "bar = \"foofoo\"")
   102  	equivDataDirs[2].addSource("data/test.toml", "hello = [\"world\", \"foo\"]")
   103  
   104  	expected :=
   105  		map[string]interface{}{
   106  			"test": map[string]interface{}{
   107  				"hello": []interface{}{
   108  					"world",
   109  					"foo",
   110  				},
   111  				"foo": map[string]interface{}{
   112  					"bar": "foofoo",
   113  				},
   114  			},
   115  		}
   116  
   117  	doTestEquivalentDataDirs(t, equivDataDirs, expected)
   118  }
   119  
   120  func TestDataDirOverriddenValue(t *testing.T) {
   121  	t.Parallel()
   122  	equivDataDirs := make([]dataDir, 3)
   123  
   124  	// filepath.Walk walks the files in lexical order, '/' comes before '.'. Simulate this:
   125  	equivDataDirs[0].addSource("data/a.json", `{"a": "1"}`)
   126  	equivDataDirs[0].addSource("data/test/v1.json", `{"v1-2": "2"}`)
   127  	equivDataDirs[0].addSource("data/test/v2.json", `{"v2": ["2", "3"]}`)
   128  	equivDataDirs[0].addSource("data/test.json", `{"v1": "1"}`)
   129  
   130  	equivDataDirs[1].addSource("data/a.yaml", "a: \"1\"")
   131  	equivDataDirs[1].addSource("data/test/v1.yaml", "v1-2: \"2\"")
   132  	equivDataDirs[1].addSource("data/test/v2.yaml", "v2:\n- \"2\"\n- \"3\"")
   133  	equivDataDirs[1].addSource("data/test.yaml", "v1: \"1\"")
   134  
   135  	equivDataDirs[2].addSource("data/a.toml", "a = \"1\"")
   136  	equivDataDirs[2].addSource("data/test/v1.toml", "v1-2 = \"2\"")
   137  	equivDataDirs[2].addSource("data/test/v2.toml", "v2 = [\"2\", \"3\"]")
   138  	equivDataDirs[2].addSource("data/test.toml", "v1 = \"1\"")
   139  
   140  	expected :=
   141  		map[string]interface{}{
   142  			"a": map[string]interface{}{"a": "1"},
   143  			"test": map[string]interface{}{
   144  				"v1": map[string]interface{}{"v1-2": "2"},
   145  				"v2": map[string]interface{}{"v2": []interface{}{"2", "3"}},
   146  			},
   147  		}
   148  
   149  	doTestEquivalentDataDirs(t, equivDataDirs, expected)
   150  }
   151  
   152  // Issue #4361, #3890
   153  func TestDataDirArrayAtTopLevelOfFile(t *testing.T) {
   154  	t.Parallel()
   155  	equivDataDirs := make([]dataDir, 2)
   156  
   157  	equivDataDirs[0].addSource("data/test.json", `[ { "hello": "world" }, { "what": "time" }, { "is": "lunch?" } ]`)
   158  	equivDataDirs[1].addSource("data/test.yaml", `
   159  - hello: world
   160  - what: time
   161  - is: lunch?
   162  `)
   163  
   164  	expected :=
   165  		map[string]interface{}{
   166  			"test": []interface{}{
   167  				map[string]interface{}{"hello": "world"},
   168  				map[string]interface{}{"what": "time"},
   169  				map[string]interface{}{"is": "lunch?"},
   170  			},
   171  		}
   172  
   173  	doTestEquivalentDataDirs(t, equivDataDirs, expected)
   174  }
   175  
   176  // Issue #892
   177  func TestDataDirMultipleSources(t *testing.T) {
   178  	t.Parallel()
   179  
   180  	var dd dataDir
   181  	dd.addSource("data/test/first.yaml", "bar: 1")
   182  	dd.addSource("themes/mytheme/data/test/first.yaml", "bar: 2")
   183  	dd.addSource("data/test/second.yaml", "tender: 2")
   184  
   185  	expected :=
   186  		map[string]interface{}{
   187  			"test": map[string]interface{}{
   188  				"first": map[string]interface{}{
   189  					"bar": 1,
   190  				},
   191  				"second": map[string]interface{}{
   192  					"tender": 2,
   193  				},
   194  			},
   195  		}
   196  
   197  	doTestDataDir(t, dd, expected,
   198  		"theme", "mytheme")
   199  }
   200  
   201  // test (and show) the way values from four different sources,
   202  // including theme data, commingle and override
   203  func TestDataDirMultipleSourcesCommingled(t *testing.T) {
   204  	t.Parallel()
   205  
   206  	var dd dataDir
   207  	dd.addSource("data/a.json", `{ "b1" : { "c1": "data/a" }, "b2": "data/a", "b3": ["x", "y", "z"] }`)
   208  	dd.addSource("themes/mytheme/data/a.json", `{ "b1": "mytheme/data/a",  "b2": "mytheme/data/a", "b3": "mytheme/data/a" }`)
   209  	dd.addSource("themes/mytheme/data/a/b1.json", `{ "c1": "mytheme/data/a/b1", "c2": "mytheme/data/a/b1" }`)
   210  	dd.addSource("data/a/b1.json", `{ "c1": "data/a/b1" }`)
   211  
   212  	// Per handleDataFile() comment:
   213  	// 1. A theme uses the same key; the main data folder wins
   214  	// 2. A sub folder uses the same key: the sub folder wins
   215  	expected :=
   216  		map[string]interface{}{
   217  			"a": map[string]interface{}{
   218  				"b1": map[string]interface{}{
   219  					"c1": "data/a/b1",
   220  					"c2": "mytheme/data/a/b1",
   221  				},
   222  				"b2": "data/a",
   223  				"b3": []interface{}{"x", "y", "z"},
   224  			},
   225  		}
   226  
   227  	doTestDataDir(t, dd, expected, "theme", "mytheme")
   228  }
   229  
   230  func TestDataDirCollidingChildArrays(t *testing.T) {
   231  	t.Parallel()
   232  
   233  	var dd dataDir
   234  	dd.addSource("themes/mytheme/data/a/b2.json", `["Q", "R", "S"]`)
   235  	dd.addSource("data/a.json", `{ "b1" : "data/a", "b2" : ["x", "y", "z"] }`)
   236  	dd.addSource("data/a/b2.json", `["1", "2", "3"]`)
   237  
   238  	// Per handleDataFile() comment:
   239  	// 1. A theme uses the same key; the main data folder wins
   240  	// 2. A sub folder uses the same key: the sub folder wins
   241  	expected :=
   242  		map[string]interface{}{
   243  			"a": map[string]interface{}{
   244  				"b1": "data/a",
   245  				"b2": []interface{}{"1", "2", "3"},
   246  			},
   247  		}
   248  
   249  	doTestDataDir(t, dd, expected, "theme", "mytheme")
   250  }
   251  
   252  func TestDataDirCollidingTopLevelArrays(t *testing.T) {
   253  	t.Parallel()
   254  
   255  	var dd dataDir
   256  	dd.addSource("themes/mytheme/data/a/b1.json", `["x", "y", "z"]`)
   257  	dd.addSource("data/a/b1.json", `["1", "2", "3"]`)
   258  
   259  	expected :=
   260  		map[string]interface{}{
   261  			"a": map[string]interface{}{
   262  				"b1": []interface{}{"1", "2", "3"},
   263  			},
   264  		}
   265  
   266  	doTestDataDir(t, dd, expected, "theme", "mytheme")
   267  }
   268  
   269  func TestDataDirCollidingMapsAndArrays(t *testing.T) {
   270  	t.Parallel()
   271  
   272  	var dd dataDir
   273  	// on
   274  	dd.addSource("themes/mytheme/data/a.json", `["1", "2", "3"]`)
   275  	dd.addSource("themes/mytheme/data/b.json", `{ "film" : "Logan Lucky" }`)
   276  	dd.addSource("data/a.json", `{ "music" : "Queen's Rebuke" }`)
   277  	dd.addSource("data/b.json", `["x", "y", "z"]`)
   278  
   279  	expected :=
   280  		map[string]interface{}{
   281  			"a": map[string]interface{}{
   282  				"music": "Queen's Rebuke",
   283  			},
   284  			"b": []interface{}{"x", "y", "z"},
   285  		}
   286  
   287  	doTestDataDir(t, dd, expected, "theme", "mytheme")
   288  }
   289  
   290  // https://discourse.gohugo.io/t/recursive-data-file-parsing/26192
   291  func TestDataDirNestedDirectories(t *testing.T) {
   292  	t.Parallel()
   293  
   294  	var dd dataDir
   295  	dd.addSource("themes/mytheme/data/a.json", `["1", "2", "3"]`)
   296  	dd.addSource("data/test1/20/06/a.json", `{ "artist" : "Michael Brecker" }`)
   297  	dd.addSource("data/test1/20/05/b.json", `{ "artist" : "Charlie Parker" }`)
   298  
   299  	expected :=
   300  		map[string]interface{}{
   301  			"a":     []interface{}{"1", "2", "3"},
   302  			"test1": map[string]interface{}{"20": map[string]interface{}{"05": map[string]interface{}{"b": map[string]interface{}{"artist": "Charlie Parker"}}, "06": map[string]interface{}{"a": map[string]interface{}{"artist": "Michael Brecker"}}}},
   303  		}
   304  
   305  	doTestDataDir(t, dd, expected, "theme", "mytheme")
   306  }
   307  
   308  type dataDir struct {
   309  	sources [][2]string
   310  }
   311  
   312  func (d *dataDir) addSource(path, content string) {
   313  	d.sources = append(d.sources, [2]string{path, content})
   314  }
   315  
   316  func doTestEquivalentDataDirs(t *testing.T, equivDataDirs []dataDir, expected interface{}, configKeyValues ...interface{}) {
   317  	for i, dd := range equivDataDirs {
   318  		err := doTestDataDirImpl(t, dd, expected, configKeyValues...)
   319  		if err != "" {
   320  			t.Errorf("equivDataDirs[%d]: %s", i, err)
   321  		}
   322  	}
   323  }
   324  
   325  func doTestDataDir(t *testing.T, dd dataDir, expected interface{}, configKeyValues ...interface{}) {
   326  	err := doTestDataDirImpl(t, dd, expected, configKeyValues...)
   327  	if err != "" {
   328  		t.Error(err)
   329  	}
   330  }
   331  
   332  func doTestDataDirImpl(t *testing.T, dd dataDir, expected interface{}, configKeyValues ...interface{}) (err string) {
   333  	cfg, fs := newTestCfg()
   334  
   335  	for i := 0; i < len(configKeyValues); i += 2 {
   336  		cfg.Set(configKeyValues[i].(string), configKeyValues[i+1])
   337  	}
   338  
   339  	var (
   340  		logger  = loggers.NewErrorLogger()
   341  		depsCfg = deps.DepsCfg{Fs: fs, Cfg: cfg, Logger: logger}
   342  	)
   343  
   344  	writeSource(t, fs, filepath.Join("content", "dummy.md"), "content")
   345  	writeSourcesToSource(t, "", fs, dd.sources...)
   346  
   347  	expectBuildError := false
   348  
   349  	if ok, shouldFail := expected.(bool); ok && shouldFail {
   350  		expectBuildError = true
   351  	}
   352  
   353  	// trap and report panics as unmarshaling errors so that test suit can complete
   354  	defer func() {
   355  		if r := recover(); r != nil {
   356  			// Capture the stack trace
   357  			buf := make([]byte, 10000)
   358  			runtime.Stack(buf, false)
   359  			t.Errorf("PANIC: %s\n\nStack Trace : %s", r, string(buf))
   360  		}
   361  	}()
   362  
   363  	s := buildSingleSiteExpected(t, false, expectBuildError, depsCfg, BuildCfg{SkipRender: true})
   364  
   365  	if !expectBuildError && !reflect.DeepEqual(expected, s.h.Data()) {
   366  		// This disabled code detects the situation described in the WARNING message below.
   367  		// The situation seems to only occur for TOML data with integer values.
   368  		// Perhaps the TOML parser returns ints in another type.
   369  		// Re-enable temporarily to debug fails that should be passing.
   370  		// Re-enable permanently if reflect.DeepEqual is simply too strict.
   371  		/*
   372  			exp := fmt.Sprintf("%#v", expected)
   373  			got := fmt.Sprintf("%#v", s.Data)
   374  			if exp == got {
   375  				t.Logf("WARNING: reflect.DeepEqual returned FALSE for values that appear equal.\n"+
   376  					"Treating as equal for the purpose of the test, but this maybe should be investigated.\n"+
   377  					"Expected data:\n%v got\n%v\n\nExpected type structure:\n%#[1]v got\n%#[2]v", expected, s.Data)
   378  				return
   379  			}
   380  		*/
   381  
   382  		return fmt.Sprintf("Expected data:\n%v got\n%v\n\nExpected type structure:\n%#[1]v got\n%#[2]v", expected, s.h.Data())
   383  	}
   384  
   385  	return
   386  }
   387  
   388  func TestDataFromShortcode(t *testing.T) {
   389  	t.Parallel()
   390  
   391  	var (
   392  		cfg, fs = newTestCfg()
   393  		c       = qt.New(t)
   394  	)
   395  
   396  	writeSource(t, fs, "data/hugo.toml", "slogan = \"Hugo Rocks!\"")
   397  	writeSource(t, fs, "layouts/_default/single.html", `
   398  * Slogan from template: {{  .Site.Data.hugo.slogan }}
   399  * {{ .Content }}`)
   400  	writeSource(t, fs, "layouts/shortcodes/d.html", `{{  .Page.Site.Data.hugo.slogan }}`)
   401  	writeSource(t, fs, "content/c.md", `---
   402  ---
   403  Slogan from shortcode: {{< d >}}
   404  `)
   405  
   406  	buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{})
   407  
   408  	content := readSource(t, fs, "public/c/index.html")
   409  
   410  	c.Assert(content, qt.Contains, "Slogan from template: Hugo Rocks!")
   411  	c.Assert(content, qt.Contains, "Slogan from shortcode: Hugo Rocks!")
   412  }