github.com/jbramsden/hugo@v0.47.1/hugolib/datafiles_test.go (about)

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