github.com/opentofu/opentofu@v1.7.1/internal/initwd/module_install_test.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package initwd
     7  
     8  import (
     9  	"bytes"
    10  	"context"
    11  	"flag"
    12  	"fmt"
    13  	"os"
    14  	"path/filepath"
    15  	"strings"
    16  	"testing"
    17  
    18  	"github.com/davecgh/go-spew/spew"
    19  	"github.com/go-test/deep"
    20  	"github.com/google/go-cmp/cmp"
    21  	version "github.com/hashicorp/go-version"
    22  	svchost "github.com/hashicorp/terraform-svchost"
    23  
    24  	"github.com/opentofu/opentofu/internal/addrs"
    25  	"github.com/opentofu/opentofu/internal/configs"
    26  	"github.com/opentofu/opentofu/internal/configs/configload"
    27  	"github.com/opentofu/opentofu/internal/copy"
    28  	"github.com/opentofu/opentofu/internal/registry"
    29  	"github.com/opentofu/opentofu/internal/tfdiags"
    30  
    31  	_ "github.com/opentofu/opentofu/internal/logging"
    32  )
    33  
    34  func TestMain(m *testing.M) {
    35  	flag.Parse()
    36  	os.Exit(m.Run())
    37  }
    38  
    39  func TestModuleInstaller(t *testing.T) {
    40  	fixtureDir := filepath.Clean("testdata/local-modules")
    41  	dir, done := tempChdir(t, fixtureDir)
    42  	defer done()
    43  
    44  	hooks := &testInstallHooks{}
    45  
    46  	modulesDir := filepath.Join(dir, ".terraform/modules")
    47  	loader, close := configload.NewLoaderForTests(t)
    48  	defer close()
    49  	inst := NewModuleInstaller(modulesDir, loader, nil)
    50  	_, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks)
    51  	assertNoDiagnostics(t, diags)
    52  
    53  	wantCalls := []testInstallHookCall{
    54  		{
    55  			Name:        "Install",
    56  			ModuleAddr:  "child_a",
    57  			PackageAddr: "",
    58  			LocalPath:   "child_a",
    59  		},
    60  		{
    61  			Name:        "Install",
    62  			ModuleAddr:  "child_a.child_b",
    63  			PackageAddr: "",
    64  			LocalPath:   "child_a/child_b",
    65  		},
    66  	}
    67  
    68  	if assertResultDeepEqual(t, hooks.Calls, wantCalls) {
    69  		return
    70  	}
    71  
    72  	loader, err := configload.NewLoader(&configload.Config{
    73  		ModulesDir: modulesDir,
    74  	})
    75  	if err != nil {
    76  		t.Fatal(err)
    77  	}
    78  
    79  	// Make sure the configuration is loadable now.
    80  	// (This ensures that correct information is recorded in the manifest.)
    81  	config, loadDiags := loader.LoadConfig(".")
    82  	assertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags))
    83  
    84  	wantTraces := map[string]string{
    85  		"":                "in root module",
    86  		"child_a":         "in child_a module",
    87  		"child_a.child_b": "in child_b module",
    88  	}
    89  	gotTraces := map[string]string{}
    90  	config.DeepEach(func(c *configs.Config) {
    91  		path := strings.Join(c.Path, ".")
    92  		if c.Module.Variables["v"] == nil {
    93  			gotTraces[path] = "<missing>"
    94  			return
    95  		}
    96  		varDesc := c.Module.Variables["v"].Description
    97  		gotTraces[path] = varDesc
    98  	})
    99  	assertResultDeepEqual(t, gotTraces, wantTraces)
   100  }
   101  
   102  func TestModuleInstaller_error(t *testing.T) {
   103  	fixtureDir := filepath.Clean("testdata/local-module-error")
   104  	dir, done := tempChdir(t, fixtureDir)
   105  	defer done()
   106  
   107  	hooks := &testInstallHooks{}
   108  
   109  	modulesDir := filepath.Join(dir, ".terraform/modules")
   110  
   111  	loader, close := configload.NewLoaderForTests(t)
   112  	defer close()
   113  	inst := NewModuleInstaller(modulesDir, loader, nil)
   114  	_, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks)
   115  
   116  	if !diags.HasErrors() {
   117  		t.Fatal("expected error")
   118  	} else {
   119  		assertDiagnosticSummary(t, diags, "Invalid module source address")
   120  	}
   121  }
   122  
   123  func TestModuleInstaller_emptyModuleName(t *testing.T) {
   124  	fixtureDir := filepath.Clean("testdata/empty-module-name")
   125  	dir, done := tempChdir(t, fixtureDir)
   126  	defer done()
   127  
   128  	hooks := &testInstallHooks{}
   129  
   130  	modulesDir := filepath.Join(dir, ".terraform/modules")
   131  
   132  	loader, close := configload.NewLoaderForTests(t)
   133  	defer close()
   134  	inst := NewModuleInstaller(modulesDir, loader, nil)
   135  	_, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks)
   136  
   137  	if !diags.HasErrors() {
   138  		t.Fatal("expected error")
   139  	} else {
   140  		assertDiagnosticSummary(t, diags, "Invalid module instance name")
   141  	}
   142  }
   143  
   144  func TestModuleInstaller_invalidModuleName(t *testing.T) {
   145  	fixtureDir := filepath.Clean("testdata/invalid-module-name")
   146  	dir, done := tempChdir(t, fixtureDir)
   147  	defer done()
   148  
   149  	hooks := &testInstallHooks{}
   150  
   151  	modulesDir := filepath.Join(dir, ".terraform/modules")
   152  
   153  	loader, close := configload.NewLoaderForTests(t)
   154  	defer close()
   155  	inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(nil, nil))
   156  	_, diags := inst.InstallModules(context.Background(), dir, "tests", false, false, hooks)
   157  	if !diags.HasErrors() {
   158  		t.Fatal("expected error")
   159  	} else {
   160  		assertDiagnosticSummary(t, diags, "Invalid module instance name")
   161  	}
   162  }
   163  
   164  func TestModuleInstaller_packageEscapeError(t *testing.T) {
   165  	fixtureDir := filepath.Clean("testdata/load-module-package-escape")
   166  	dir, done := tempChdir(t, fixtureDir)
   167  	defer done()
   168  
   169  	// For this particular test we need an absolute path in the root module
   170  	// that must actually resolve to our temporary directory in "dir", so
   171  	// we need to do a little rewriting. We replace the arbitrary placeholder
   172  	// %%BASE%% with the temporary directory path.
   173  	{
   174  		rootFilename := filepath.Join(dir, "package-escape.tf")
   175  		template, err := os.ReadFile(rootFilename)
   176  		if err != nil {
   177  			t.Fatal(err)
   178  		}
   179  		final := bytes.ReplaceAll(template, []byte("%%BASE%%"), []byte(filepath.ToSlash(dir)))
   180  		err = os.WriteFile(rootFilename, final, 0644)
   181  		if err != nil {
   182  			t.Fatal(err)
   183  		}
   184  	}
   185  
   186  	hooks := &testInstallHooks{}
   187  
   188  	modulesDir := filepath.Join(dir, ".terraform/modules")
   189  
   190  	loader, close := configload.NewLoaderForTests(t)
   191  	defer close()
   192  	inst := NewModuleInstaller(modulesDir, loader, nil)
   193  	_, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks)
   194  
   195  	if !diags.HasErrors() {
   196  		t.Fatal("expected error")
   197  	} else {
   198  		assertDiagnosticSummary(t, diags, "Local module path escapes module package")
   199  	}
   200  }
   201  
   202  func TestModuleInstaller_explicitPackageBoundary(t *testing.T) {
   203  	fixtureDir := filepath.Clean("testdata/load-module-package-prefix")
   204  	dir, done := tempChdir(t, fixtureDir)
   205  	defer done()
   206  
   207  	// For this particular test we need an absolute path in the root module
   208  	// that must actually resolve to our temporary directory in "dir", so
   209  	// we need to do a little rewriting. We replace the arbitrary placeholder
   210  	// %%BASE%% with the temporary directory path.
   211  	{
   212  		rootFilename := filepath.Join(dir, "package-prefix.tf")
   213  		template, err := os.ReadFile(rootFilename)
   214  		if err != nil {
   215  			t.Fatal(err)
   216  		}
   217  		final := bytes.ReplaceAll(template, []byte("%%BASE%%"), []byte(filepath.ToSlash(dir)))
   218  		err = os.WriteFile(rootFilename, final, 0644)
   219  		if err != nil {
   220  			t.Fatal(err)
   221  		}
   222  	}
   223  
   224  	hooks := &testInstallHooks{}
   225  
   226  	modulesDir := filepath.Join(dir, ".terraform/modules")
   227  
   228  	loader, close := configload.NewLoaderForTests(t)
   229  	defer close()
   230  	inst := NewModuleInstaller(modulesDir, loader, nil)
   231  	_, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks)
   232  
   233  	if diags.HasErrors() {
   234  		t.Fatalf("unexpected errors\n%s", diags.Err().Error())
   235  	}
   236  }
   237  
   238  func TestModuleInstaller_ExactMatchPrerelease(t *testing.T) {
   239  	if os.Getenv("TF_ACC") == "" {
   240  		t.Skip("this test accesses registry.opentofu.org and github.com; set TF_ACC=1 to run it")
   241  	}
   242  
   243  	fixtureDir := filepath.Clean("testdata/prerelease-version-constraint-match")
   244  	dir, done := tempChdir(t, fixtureDir)
   245  	defer done()
   246  
   247  	hooks := &testInstallHooks{}
   248  
   249  	modulesDir := filepath.Join(dir, ".terraform/modules")
   250  
   251  	loader, close := configload.NewLoaderForTests(t)
   252  	defer close()
   253  	inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(nil, nil))
   254  	cfg, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks)
   255  
   256  	if diags.HasErrors() {
   257  		t.Fatalf("found unexpected errors: %s", diags.Err())
   258  	}
   259  
   260  	if !cfg.Children["acctest_exact"].Version.Equal(version.Must(version.NewVersion("v0.0.3-alpha.1"))) {
   261  		t.Fatalf("expected version %s but found version %s", "v0.0.3-alpha.1", cfg.Version.String())
   262  	}
   263  }
   264  
   265  func TestModuleInstaller_PartialMatchPrerelease(t *testing.T) {
   266  	if os.Getenv("TF_ACC") == "" {
   267  		t.Skip("this test accesses registry.opentofu.org and github.com; set TF_ACC=1 to run it")
   268  	}
   269  
   270  	fixtureDir := filepath.Clean("testdata/prerelease-version-constraint")
   271  	dir, done := tempChdir(t, fixtureDir)
   272  	defer done()
   273  
   274  	hooks := &testInstallHooks{}
   275  
   276  	modulesDir := filepath.Join(dir, ".terraform/modules")
   277  
   278  	loader, close := configload.NewLoaderForTests(t)
   279  	defer close()
   280  	inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(nil, nil))
   281  	cfg, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks)
   282  
   283  	if diags.HasErrors() {
   284  		t.Fatalf("found unexpected errors: %s", diags.Err())
   285  	}
   286  
   287  	if !cfg.Children["acctest_partial"].Version.Equal(version.Must(version.NewVersion("v0.0.2"))) {
   288  		t.Fatalf("expected version %s but found version %s", "v0.0.2", cfg.Version.String())
   289  	}
   290  }
   291  
   292  func TestModuleInstaller_invalid_version_constraint_error(t *testing.T) {
   293  	fixtureDir := filepath.Clean("testdata/invalid-version-constraint")
   294  	dir, done := tempChdir(t, fixtureDir)
   295  	defer done()
   296  
   297  	hooks := &testInstallHooks{}
   298  
   299  	modulesDir := filepath.Join(dir, ".terraform/modules")
   300  
   301  	loader, close := configload.NewLoaderForTests(t)
   302  	defer close()
   303  	inst := NewModuleInstaller(modulesDir, loader, nil)
   304  	_, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks)
   305  
   306  	if !diags.HasErrors() {
   307  		t.Fatal("expected error")
   308  	} else {
   309  		// We use the presence of the "version" argument as a heuristic for
   310  		// user intent to use a registry module, and so we intentionally catch
   311  		// this as an invalid registry module address rather than an invalid
   312  		// version constraint, so we can surface the specific address parsing
   313  		// error instead of a generic version constraint error.
   314  		assertDiagnosticSummary(t, diags, "Invalid registry module source address")
   315  	}
   316  }
   317  
   318  func TestModuleInstaller_invalidVersionConstraintGetter(t *testing.T) {
   319  	fixtureDir := filepath.Clean("testdata/invalid-version-constraint")
   320  	dir, done := tempChdir(t, fixtureDir)
   321  	defer done()
   322  
   323  	hooks := &testInstallHooks{}
   324  
   325  	modulesDir := filepath.Join(dir, ".terraform/modules")
   326  
   327  	loader, close := configload.NewLoaderForTests(t)
   328  	defer close()
   329  	inst := NewModuleInstaller(modulesDir, loader, nil)
   330  	_, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks)
   331  
   332  	if !diags.HasErrors() {
   333  		t.Fatal("expected error")
   334  	} else {
   335  		// We use the presence of the "version" argument as a heuristic for
   336  		// user intent to use a registry module, and so we intentionally catch
   337  		// this as an invalid registry module address rather than an invalid
   338  		// version constraint, so we can surface the specific address parsing
   339  		// error instead of a generic version constraint error.
   340  		assertDiagnosticSummary(t, diags, "Invalid registry module source address")
   341  	}
   342  }
   343  
   344  func TestModuleInstaller_invalidVersionConstraintLocal(t *testing.T) {
   345  	fixtureDir := filepath.Clean("testdata/invalid-version-constraint-local")
   346  	dir, done := tempChdir(t, fixtureDir)
   347  	defer done()
   348  
   349  	hooks := &testInstallHooks{}
   350  
   351  	modulesDir := filepath.Join(dir, ".terraform/modules")
   352  
   353  	loader, close := configload.NewLoaderForTests(t)
   354  	defer close()
   355  	inst := NewModuleInstaller(modulesDir, loader, nil)
   356  	_, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks)
   357  
   358  	if !diags.HasErrors() {
   359  		t.Fatal("expected error")
   360  	} else {
   361  		// We use the presence of the "version" argument as a heuristic for
   362  		// user intent to use a registry module, and so we intentionally catch
   363  		// this as an invalid registry module address rather than an invalid
   364  		// version constraint, so we can surface the specific address parsing
   365  		// error instead of a generic version constraint error.
   366  		assertDiagnosticSummary(t, diags, "Invalid registry module source address")
   367  	}
   368  }
   369  
   370  func TestModuleInstaller_symlink(t *testing.T) {
   371  	fixtureDir := filepath.Clean("testdata/local-module-symlink")
   372  	dir, done := tempChdir(t, fixtureDir)
   373  	defer done()
   374  
   375  	hooks := &testInstallHooks{}
   376  
   377  	modulesDir := filepath.Join(dir, ".terraform/modules")
   378  
   379  	loader, close := configload.NewLoaderForTests(t)
   380  	defer close()
   381  	inst := NewModuleInstaller(modulesDir, loader, nil)
   382  	_, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks)
   383  	assertNoDiagnostics(t, diags)
   384  
   385  	wantCalls := []testInstallHookCall{
   386  		{
   387  			Name:        "Install",
   388  			ModuleAddr:  "child_a",
   389  			PackageAddr: "",
   390  			LocalPath:   "child_a",
   391  		},
   392  		{
   393  			Name:        "Install",
   394  			ModuleAddr:  "child_a.child_b",
   395  			PackageAddr: "",
   396  			LocalPath:   "child_a/child_b",
   397  		},
   398  	}
   399  
   400  	if assertResultDeepEqual(t, hooks.Calls, wantCalls) {
   401  		return
   402  	}
   403  
   404  	loader, err := configload.NewLoader(&configload.Config{
   405  		ModulesDir: modulesDir,
   406  	})
   407  	if err != nil {
   408  		t.Fatal(err)
   409  	}
   410  
   411  	// Make sure the configuration is loadable now.
   412  	// (This ensures that correct information is recorded in the manifest.)
   413  	config, loadDiags := loader.LoadConfig(".")
   414  	assertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags))
   415  
   416  	wantTraces := map[string]string{
   417  		"":                "in root module",
   418  		"child_a":         "in child_a module",
   419  		"child_a.child_b": "in child_b module",
   420  	}
   421  	gotTraces := map[string]string{}
   422  	config.DeepEach(func(c *configs.Config) {
   423  		path := strings.Join(c.Path, ".")
   424  		if c.Module.Variables["v"] == nil {
   425  			gotTraces[path] = "<missing>"
   426  			return
   427  		}
   428  		varDesc := c.Module.Variables["v"].Description
   429  		gotTraces[path] = varDesc
   430  	})
   431  	assertResultDeepEqual(t, gotTraces, wantTraces)
   432  }
   433  
   434  func TestLoaderInstallModules_registry(t *testing.T) {
   435  	if os.Getenv("TF_ACC") == "" {
   436  		t.Skip("this test accesses registry.opentofu.org and github.com; set TF_ACC=1 to run it")
   437  	}
   438  
   439  	fixtureDir := filepath.Clean("testdata/registry-modules")
   440  	tmpDir, done := tempChdir(t, fixtureDir)
   441  	// the module installer runs filepath.EvalSymlinks() on the destination
   442  	// directory before copying files, and the resultant directory is what is
   443  	// returned by the install hooks. Without this, tests could fail on machines
   444  	// where the default temp dir was a symlink.
   445  	dir, err := filepath.EvalSymlinks(tmpDir)
   446  	if err != nil {
   447  		t.Error(err)
   448  	}
   449  
   450  	defer done()
   451  
   452  	hooks := &testInstallHooks{}
   453  	modulesDir := filepath.Join(dir, ".terraform/modules")
   454  
   455  	loader, close := configload.NewLoaderForTests(t)
   456  	defer close()
   457  	inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(nil, nil))
   458  	_, diags := inst.InstallModules(context.Background(), dir, "tests", false, false, hooks)
   459  	assertNoDiagnostics(t, diags)
   460  
   461  	v := version.Must(version.NewVersion("0.0.1"))
   462  
   463  	wantCalls := []testInstallHookCall{
   464  		// the configuration builder visits each level of calls in lexicographical
   465  		// order by name, so the following list is kept in the same order.
   466  
   467  		// acctest_child_a accesses //modules/child_a directly
   468  		{
   469  			Name:        "Download",
   470  			ModuleAddr:  "acctest_child_a",
   471  			PackageAddr: "registry.opentofu.org/hashicorp/module-installer-acctest/aws", // intentionally excludes the subdir because we're downloading the whole package here
   472  			Version:     v,
   473  		},
   474  		{
   475  			Name:       "Install",
   476  			ModuleAddr: "acctest_child_a",
   477  			Version:    v,
   478  			// NOTE: This local path and the other paths derived from it below
   479  			// can vary depending on how the registry is implemented. At the
   480  			// time of writing this test, registry.opentofu.org returns
   481  			// git repository source addresses and so this path refers to the
   482  			// root of the git clone, but historically the registry referred
   483  			// to GitHub-provided tar archives which meant that there was an
   484  			// extra level of subdirectory here for the typical directory
   485  			// nesting in tar archives, which would've been reflected as
   486  			// an extra segment on this path. If this test fails due to an
   487  			// additional path segment in future, then a change to the upstream
   488  			// registry might be the root cause.
   489  			LocalPath: filepath.Join(dir, ".terraform/modules/acctest_child_a/modules/child_a"),
   490  		},
   491  
   492  		// acctest_child_a.child_b
   493  		// (no download because it's a relative path inside acctest_child_a)
   494  		{
   495  			Name:       "Install",
   496  			ModuleAddr: "acctest_child_a.child_b",
   497  			LocalPath:  filepath.Join(dir, ".terraform/modules/acctest_child_a/modules/child_b"),
   498  		},
   499  
   500  		// acctest_child_b accesses //modules/child_b directly
   501  		{
   502  			Name:        "Download",
   503  			ModuleAddr:  "acctest_child_b",
   504  			PackageAddr: "registry.opentofu.org/hashicorp/module-installer-acctest/aws", // intentionally excludes the subdir because we're downloading the whole package here
   505  			Version:     v,
   506  		},
   507  		{
   508  			Name:       "Install",
   509  			ModuleAddr: "acctest_child_b",
   510  			Version:    v,
   511  			LocalPath:  filepath.Join(dir, ".terraform/modules/acctest_child_b/modules/child_b"),
   512  		},
   513  
   514  		// acctest_root
   515  		{
   516  			Name:        "Download",
   517  			ModuleAddr:  "acctest_root",
   518  			PackageAddr: "registry.opentofu.org/hashicorp/module-installer-acctest/aws",
   519  			Version:     v,
   520  		},
   521  		{
   522  			Name:       "Install",
   523  			ModuleAddr: "acctest_root",
   524  			Version:    v,
   525  			LocalPath:  filepath.Join(dir, ".terraform/modules/acctest_root"),
   526  		},
   527  
   528  		// acctest_root.child_a
   529  		// (no download because it's a relative path inside acctest_root)
   530  		{
   531  			Name:       "Install",
   532  			ModuleAddr: "acctest_root.child_a",
   533  			LocalPath:  filepath.Join(dir, ".terraform/modules/acctest_root/modules/child_a"),
   534  		},
   535  
   536  		// acctest_root.child_a.child_b
   537  		// (no download because it's a relative path inside acctest_root, via acctest_root.child_a)
   538  		{
   539  			Name:       "Install",
   540  			ModuleAddr: "acctest_root.child_a.child_b",
   541  			LocalPath:  filepath.Join(dir, ".terraform/modules/acctest_root/modules/child_b"),
   542  		},
   543  	}
   544  
   545  	if diff := cmp.Diff(wantCalls, hooks.Calls); diff != "" {
   546  		t.Fatalf("wrong installer calls\n%s", diff)
   547  	}
   548  
   549  	//check that the registry reponses were cached
   550  	packageAddr := addrs.ModuleRegistryPackage{
   551  		Host:         svchost.Hostname("registry.opentofu.org"),
   552  		Namespace:    "hashicorp",
   553  		Name:         "module-installer-acctest",
   554  		TargetSystem: "aws",
   555  	}
   556  	if _, ok := inst.registryPackageVersions[packageAddr]; !ok {
   557  		t.Errorf("module versions cache was not populated\ngot: %s\nwant: key hashicorp/module-installer-acctest/aws", spew.Sdump(inst.registryPackageVersions))
   558  	}
   559  	if _, ok := inst.registryPackageSources[moduleVersion{module: packageAddr, version: "0.0.1"}]; !ok {
   560  		t.Errorf("module download url cache was not populated\ngot: %s", spew.Sdump(inst.registryPackageSources))
   561  	}
   562  
   563  	loader, err = configload.NewLoader(&configload.Config{
   564  		ModulesDir: modulesDir,
   565  	})
   566  	if err != nil {
   567  		t.Fatal(err)
   568  	}
   569  
   570  	// Make sure the configuration is loadable now.
   571  	// (This ensures that correct information is recorded in the manifest.)
   572  	config, loadDiags := loader.LoadConfig(".")
   573  	assertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags))
   574  
   575  	wantTraces := map[string]string{
   576  		"":                             "in local caller for registry-modules",
   577  		"acctest_root":                 "in root module",
   578  		"acctest_root.child_a":         "in child_a module",
   579  		"acctest_root.child_a.child_b": "in child_b module",
   580  		"acctest_child_a":              "in child_a module",
   581  		"acctest_child_a.child_b":      "in child_b module",
   582  		"acctest_child_b":              "in child_b module",
   583  	}
   584  	gotTraces := map[string]string{}
   585  	config.DeepEach(func(c *configs.Config) {
   586  		path := strings.Join(c.Path, ".")
   587  		if c.Module.Variables["v"] == nil {
   588  			gotTraces[path] = "<missing>"
   589  			return
   590  		}
   591  		varDesc := c.Module.Variables["v"].Description
   592  		gotTraces[path] = varDesc
   593  	})
   594  	assertResultDeepEqual(t, gotTraces, wantTraces)
   595  
   596  }
   597  
   598  func TestLoaderInstallModules_goGetter(t *testing.T) {
   599  	if os.Getenv("TF_ACC") == "" {
   600  		t.Skip("this test accesses github.com; set TF_ACC=1 to run it")
   601  	}
   602  
   603  	fixtureDir := filepath.Clean("testdata/go-getter-modules")
   604  	tmpDir, done := tempChdir(t, fixtureDir)
   605  	// the module installer runs filepath.EvalSymlinks() on the destination
   606  	// directory before copying files, and the resultant directory is what is
   607  	// returned by the install hooks. Without this, tests could fail on machines
   608  	// where the default temp dir was a symlink.
   609  	dir, err := filepath.EvalSymlinks(tmpDir)
   610  	if err != nil {
   611  		t.Error(err)
   612  	}
   613  	defer done()
   614  
   615  	hooks := &testInstallHooks{}
   616  	modulesDir := filepath.Join(dir, ".terraform/modules")
   617  
   618  	loader, close := configload.NewLoaderForTests(t)
   619  	defer close()
   620  	inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(nil, nil))
   621  	_, diags := inst.InstallModules(context.Background(), dir, "tests", false, false, hooks)
   622  	assertNoDiagnostics(t, diags)
   623  
   624  	wantCalls := []testInstallHookCall{
   625  		// the configuration builder visits each level of calls in lexicographical
   626  		// order by name, so the following list is kept in the same order.
   627  
   628  		// acctest_child_a accesses //modules/child_a directly
   629  		{
   630  			Name:        "Download",
   631  			ModuleAddr:  "acctest_child_a",
   632  			PackageAddr: "git::https://github.com/hashicorp/terraform-aws-module-installer-acctest.git?ref=v0.0.1", // intentionally excludes the subdir because we're downloading the whole repo here
   633  		},
   634  		{
   635  			Name:       "Install",
   636  			ModuleAddr: "acctest_child_a",
   637  			LocalPath:  filepath.Join(dir, ".terraform/modules/acctest_child_a/modules/child_a"),
   638  		},
   639  
   640  		// acctest_child_a.child_b
   641  		// (no download because it's a relative path inside acctest_child_a)
   642  		{
   643  			Name:       "Install",
   644  			ModuleAddr: "acctest_child_a.child_b",
   645  			LocalPath:  filepath.Join(dir, ".terraform/modules/acctest_child_a/modules/child_b"),
   646  		},
   647  
   648  		// acctest_child_b accesses //modules/child_b directly
   649  		{
   650  			Name:        "Download",
   651  			ModuleAddr:  "acctest_child_b",
   652  			PackageAddr: "git::https://github.com/hashicorp/terraform-aws-module-installer-acctest.git?ref=v0.0.1", // intentionally excludes the subdir because we're downloading the whole package here
   653  		},
   654  		{
   655  			Name:       "Install",
   656  			ModuleAddr: "acctest_child_b",
   657  			LocalPath:  filepath.Join(dir, ".terraform/modules/acctest_child_b/modules/child_b"),
   658  		},
   659  
   660  		// acctest_root
   661  		{
   662  			Name:        "Download",
   663  			ModuleAddr:  "acctest_root",
   664  			PackageAddr: "git::https://github.com/hashicorp/terraform-aws-module-installer-acctest.git?ref=v0.0.1",
   665  		},
   666  		{
   667  			Name:       "Install",
   668  			ModuleAddr: "acctest_root",
   669  			LocalPath:  filepath.Join(dir, ".terraform/modules/acctest_root"),
   670  		},
   671  
   672  		// acctest_root.child_a
   673  		// (no download because it's a relative path inside acctest_root)
   674  		{
   675  			Name:       "Install",
   676  			ModuleAddr: "acctest_root.child_a",
   677  			LocalPath:  filepath.Join(dir, ".terraform/modules/acctest_root/modules/child_a"),
   678  		},
   679  
   680  		// acctest_root.child_a.child_b
   681  		// (no download because it's a relative path inside acctest_root, via acctest_root.child_a)
   682  		{
   683  			Name:       "Install",
   684  			ModuleAddr: "acctest_root.child_a.child_b",
   685  			LocalPath:  filepath.Join(dir, ".terraform/modules/acctest_root/modules/child_b"),
   686  		},
   687  	}
   688  
   689  	if diff := cmp.Diff(wantCalls, hooks.Calls); diff != "" {
   690  		t.Fatalf("wrong installer calls\n%s", diff)
   691  	}
   692  
   693  	loader, err = configload.NewLoader(&configload.Config{
   694  		ModulesDir: modulesDir,
   695  	})
   696  	if err != nil {
   697  		t.Fatal(err)
   698  	}
   699  
   700  	// Make sure the configuration is loadable now.
   701  	// (This ensures that correct information is recorded in the manifest.)
   702  	config, loadDiags := loader.LoadConfig(".")
   703  	assertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags))
   704  
   705  	wantTraces := map[string]string{
   706  		"":                             "in local caller for go-getter-modules",
   707  		"acctest_root":                 "in root module",
   708  		"acctest_root.child_a":         "in child_a module",
   709  		"acctest_root.child_a.child_b": "in child_b module",
   710  		"acctest_child_a":              "in child_a module",
   711  		"acctest_child_a.child_b":      "in child_b module",
   712  		"acctest_child_b":              "in child_b module",
   713  	}
   714  	gotTraces := map[string]string{}
   715  	config.DeepEach(func(c *configs.Config) {
   716  		path := strings.Join(c.Path, ".")
   717  		if c.Module.Variables["v"] == nil {
   718  			gotTraces[path] = "<missing>"
   719  			return
   720  		}
   721  		varDesc := c.Module.Variables["v"].Description
   722  		gotTraces[path] = varDesc
   723  	})
   724  	assertResultDeepEqual(t, gotTraces, wantTraces)
   725  
   726  }
   727  
   728  func TestModuleInstaller_fromTests(t *testing.T) {
   729  	fixtureDir := filepath.Clean("testdata/local-module-from-test")
   730  	dir, done := tempChdir(t, fixtureDir)
   731  	defer done()
   732  
   733  	hooks := &testInstallHooks{}
   734  
   735  	modulesDir := filepath.Join(dir, ".terraform/modules")
   736  	loader, close := configload.NewLoaderForTests(t)
   737  	defer close()
   738  	inst := NewModuleInstaller(modulesDir, loader, nil)
   739  	_, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks)
   740  	assertNoDiagnostics(t, diags)
   741  
   742  	wantCalls := []testInstallHookCall{
   743  		{
   744  			Name:        "Install",
   745  			ModuleAddr:  "test.tests.main.setup",
   746  			PackageAddr: "",
   747  			LocalPath:   "setup",
   748  		},
   749  	}
   750  
   751  	if assertResultDeepEqual(t, hooks.Calls, wantCalls) {
   752  		return
   753  	}
   754  
   755  	loader, err := configload.NewLoader(&configload.Config{
   756  		ModulesDir: modulesDir,
   757  	})
   758  	if err != nil {
   759  		t.Fatal(err)
   760  	}
   761  
   762  	// Make sure the configuration is loadable now.
   763  	// (This ensures that correct information is recorded in the manifest.)
   764  	config, loadDiags := loader.LoadConfigWithTests(".", "tests")
   765  	assertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags))
   766  
   767  	if config.Module.Tests["tests/main.tftest.hcl"].Runs[0].ConfigUnderTest == nil {
   768  		t.Fatalf("should have loaded config into the relevant run block but did not")
   769  	}
   770  }
   771  
   772  func TestLoadInstallModules_registryFromTest(t *testing.T) {
   773  	if os.Getenv("TF_ACC") == "" {
   774  		t.Skip("this test accesses registry.opentofu.org and github.com; set TF_ACC=1 to run it")
   775  	}
   776  
   777  	fixtureDir := filepath.Clean("testdata/registry-module-from-test")
   778  	tmpDir, done := tempChdir(t, fixtureDir)
   779  	// the module installer runs filepath.EvalSymlinks() on the destination
   780  	// directory before copying files, and the resultant directory is what is
   781  	// returned by the install hooks. Without this, tests could fail on machines
   782  	// where the default temp dir was a symlink.
   783  	dir, err := filepath.EvalSymlinks(tmpDir)
   784  	if err != nil {
   785  		t.Error(err)
   786  	}
   787  
   788  	defer done()
   789  
   790  	hooks := &testInstallHooks{}
   791  	modulesDir := filepath.Join(dir, ".terraform/modules")
   792  
   793  	loader, close := configload.NewLoaderForTests(t)
   794  	defer close()
   795  	inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(nil, nil))
   796  	_, diags := inst.InstallModules(context.Background(), dir, "tests", false, false, hooks)
   797  	assertNoDiagnostics(t, diags)
   798  
   799  	v := version.Must(version.NewVersion("0.0.1"))
   800  	wantCalls := []testInstallHookCall{
   801  		// the configuration builder visits each level of calls in lexicographical
   802  		// order by name, so the following list is kept in the same order.
   803  
   804  		// setup access acctest directly.
   805  		{
   806  			Name:        "Download",
   807  			ModuleAddr:  "test.main.setup",
   808  			PackageAddr: "registry.opentofu.org/hashicorp/module-installer-acctest/aws", // intentionally excludes the subdir because we're downloading the whole package here
   809  			Version:     v,
   810  		},
   811  		{
   812  			Name:       "Install",
   813  			ModuleAddr: "test.main.setup",
   814  			Version:    v,
   815  			// NOTE: This local path and the other paths derived from it below
   816  			// can vary depending on how the registry is implemented. At the
   817  			// time of writing this test, registry.opentofu.org returns
   818  			// git repository source addresses and so this path refers to the
   819  			// root of the git clone, but historically the registry referred
   820  			// to GitHub-provided tar archives which meant that there was an
   821  			// extra level of subdirectory here for the typical directory
   822  			// nesting in tar archives, which would've been reflected as
   823  			// an extra segment on this path. If this test fails due to an
   824  			// additional path segment in future, then a change to the upstream
   825  			// registry might be the root cause.
   826  			LocalPath: filepath.Join(dir, ".terraform/modules/test.main.setup"),
   827  		},
   828  
   829  		// main.tftest.hcl.setup.child_a
   830  		// (no download because it's a relative path inside acctest_child_a)
   831  		{
   832  			Name:       "Install",
   833  			ModuleAddr: "test.main.setup.child_a",
   834  			LocalPath:  filepath.Join(dir, ".terraform/modules/test.main.setup/modules/child_a"),
   835  		},
   836  
   837  		// main.tftest.hcl.setup.child_a.child_b
   838  		// (no download because it's a relative path inside main.tftest.hcl.setup.child_a)
   839  		{
   840  			Name:       "Install",
   841  			ModuleAddr: "test.main.setup.child_a.child_b",
   842  			LocalPath:  filepath.Join(dir, ".terraform/modules/test.main.setup/modules/child_b"),
   843  		},
   844  	}
   845  
   846  	if diff := cmp.Diff(wantCalls, hooks.Calls); diff != "" {
   847  		t.Fatalf("wrong installer calls\n%s", diff)
   848  	}
   849  
   850  	//check that the registry reponses were cached
   851  	packageAddr := addrs.ModuleRegistryPackage{
   852  		Host:         svchost.Hostname("registry.opentofu.org"),
   853  		Namespace:    "hashicorp",
   854  		Name:         "module-installer-acctest",
   855  		TargetSystem: "aws",
   856  	}
   857  	if _, ok := inst.registryPackageVersions[packageAddr]; !ok {
   858  		t.Errorf("module versions cache was not populated\ngot: %s\nwant: key hashicorp/module-installer-acctest/aws", spew.Sdump(inst.registryPackageVersions))
   859  	}
   860  	if _, ok := inst.registryPackageSources[moduleVersion{module: packageAddr, version: "0.0.1"}]; !ok {
   861  		t.Errorf("module download url cache was not populated\ngot: %s", spew.Sdump(inst.registryPackageSources))
   862  	}
   863  
   864  	loader, err = configload.NewLoader(&configload.Config{
   865  		ModulesDir: modulesDir,
   866  	})
   867  	if err != nil {
   868  		t.Fatal(err)
   869  	}
   870  
   871  	// Make sure the configuration is loadable now.
   872  	// (This ensures that correct information is recorded in the manifest.)
   873  	config, loadDiags := loader.LoadConfigWithTests(".", "tests")
   874  	assertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags))
   875  
   876  	if config.Module.Tests["main.tftest.hcl"].Runs[0].ConfigUnderTest == nil {
   877  		t.Fatalf("should have loaded config into the relevant run block but did not")
   878  	}
   879  }
   880  
   881  type testInstallHooks struct {
   882  	Calls []testInstallHookCall
   883  }
   884  
   885  type testInstallHookCall struct {
   886  	Name        string
   887  	ModuleAddr  string
   888  	PackageAddr string
   889  	Version     *version.Version
   890  	LocalPath   string
   891  }
   892  
   893  func (h *testInstallHooks) Download(moduleAddr, packageAddr string, version *version.Version) {
   894  	h.Calls = append(h.Calls, testInstallHookCall{
   895  		Name:        "Download",
   896  		ModuleAddr:  moduleAddr,
   897  		PackageAddr: packageAddr,
   898  		Version:     version,
   899  	})
   900  }
   901  
   902  func (h *testInstallHooks) Install(moduleAddr string, version *version.Version, localPath string) {
   903  	h.Calls = append(h.Calls, testInstallHookCall{
   904  		Name:       "Install",
   905  		ModuleAddr: moduleAddr,
   906  		Version:    version,
   907  		LocalPath:  localPath,
   908  	})
   909  }
   910  
   911  // tempChdir copies the contents of the given directory to a temporary
   912  // directory and changes the test process's current working directory to
   913  // point to that directory. Also returned is a function that should be
   914  // called at the end of the test (e.g. via "defer") to restore the previous
   915  // working directory.
   916  //
   917  // Tests using this helper cannot safely be run in parallel with other tests.
   918  func tempChdir(t *testing.T, sourceDir string) (string, func()) {
   919  	t.Helper()
   920  
   921  	tmpDir, err := os.MkdirTemp("", "terraform-configload")
   922  	if err != nil {
   923  		t.Fatalf("failed to create temporary directory: %s", err)
   924  		return "", nil
   925  	}
   926  
   927  	if err := copy.CopyDir(tmpDir, sourceDir); err != nil {
   928  		t.Fatalf("failed to copy fixture to temporary directory: %s", err)
   929  		return "", nil
   930  	}
   931  
   932  	oldDir, err := os.Getwd()
   933  	if err != nil {
   934  		t.Fatalf("failed to determine current working directory: %s", err)
   935  		return "", nil
   936  	}
   937  
   938  	err = os.Chdir(tmpDir)
   939  	if err != nil {
   940  		t.Fatalf("failed to switch to temp dir %s: %s", tmpDir, err)
   941  		return "", nil
   942  	}
   943  
   944  	// Most of the tests need this, so we'll make it just in case.
   945  	os.MkdirAll(filepath.Join(tmpDir, ".terraform/modules"), os.ModePerm)
   946  
   947  	t.Logf("tempChdir switched to %s after copying from %s", tmpDir, sourceDir)
   948  
   949  	return tmpDir, func() {
   950  		err := os.Chdir(oldDir)
   951  		if err != nil {
   952  			panic(fmt.Errorf("failed to restore previous working directory %s: %w", oldDir, err))
   953  		}
   954  
   955  		if os.Getenv("TF_CONFIGLOAD_TEST_KEEP_TMP") == "" {
   956  			os.RemoveAll(tmpDir)
   957  		}
   958  	}
   959  }
   960  
   961  func assertNoDiagnostics(t *testing.T, diags tfdiags.Diagnostics) bool {
   962  	t.Helper()
   963  	return assertDiagnosticCount(t, diags, 0)
   964  }
   965  
   966  func assertDiagnosticCount(t *testing.T, diags tfdiags.Diagnostics, want int) bool {
   967  	t.Helper()
   968  	if len(diags) != want {
   969  		t.Errorf("wrong number of diagnostics %d; want %d", len(diags), want)
   970  		for _, diag := range diags {
   971  			t.Logf("- %#v", diag)
   972  		}
   973  		return true
   974  	}
   975  	return false
   976  }
   977  
   978  func assertDiagnosticSummary(t *testing.T, diags tfdiags.Diagnostics, want string) bool {
   979  	t.Helper()
   980  
   981  	for _, diag := range diags {
   982  		if diag.Description().Summary == want {
   983  			return false
   984  		}
   985  	}
   986  
   987  	t.Errorf("missing diagnostic summary %q", want)
   988  	for _, diag := range diags {
   989  		t.Logf("- %#v", diag)
   990  	}
   991  	return true
   992  }
   993  
   994  func assertResultDeepEqual(t *testing.T, got, want interface{}) bool {
   995  	t.Helper()
   996  	if diff := deep.Equal(got, want); diff != nil {
   997  		for _, problem := range diff {
   998  			t.Errorf("%s", problem)
   999  		}
  1000  		return true
  1001  	}
  1002  	return false
  1003  }