Files
opentf/internal/configs/testing_helpers.go
Martin Atkins 3833222e05 configs: Module-loading helpers for easier testing elsewhere
Various other packages currently have somewhat-complicated helpers for
loading configuration that involve building temporary directories on disk
and filling them with source files, but "package configs" itself can
provide similar helpers without so much complexity by relying on its own
internals a little to bypass details that are not so important for simple
test cases.

This introduces some new helpers but doesn't yet introduce any callers of
them because these helpers in particular are aimed at an experimental new
approach to config evaluation that makes it some other package's problem
to assemble the configuration tree, and so it just wants individual
configs.Module objects instead of configs.Config trees.

Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
2025-10-27 10:15:41 -07:00

122 lines
4.1 KiB
Go

// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package configs
import (
"testing"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/opentofu/opentofu/internal/addrs"
)
// The functions in this file are intended only for use in tests written in
// this and other packages, offering various shortcuts for getting
// modules and configurations built in preparation for testing other systems
// whose behavior depends on configuration.
// ModuleFromStringForTesting interprets the given string as if it were the
// content of a ".tofu" file in a module directory, parsing and decoding it
// as a single-file module.
//
// Note that THIS FUNCTION DOES NOT PERFORM EARLY EVALUATION. This is intended
// mainly for an experimental new config evaluation strategy where early
// evaluation and config tree assembly are handled outside of this package.
//
// If the configuration is not valid then this halts testing by calling
// [testing.TB.FailNow].
//
// Language experiments are always allowed in the "modules" loaded by this
// function.
func ModuleFromStringForTesting(t testing.TB, src string) *Module {
t.Helper()
ret := moduleFromStringForTesting(t, src, "<ModuleFromStringForTesting>")
if ret == nil {
t.FailNow() // prevent further execution if the config was invalid
}
return ret
}
// ModulesFromStringsForTesting calls [ModuleFromStringForTesting] for each
// element of the given map and then treats the map keys as local module
// source addresses to construct a map from source address to module.
//
// As with [ModuleFromStringForTesting], if any of the given configuration
// strings are invalid then this halts testing by calling [testing.TB.FailNow],
// and experiments are always allowed. The map keys must also be valid local
// module source addresses.
func ModulesFromStringsForTesting(t testing.TB, srcs map[string]string) map[addrs.ModuleSourceLocal]*Module {
t.Helper()
if len(srcs) == 0 {
return nil // weird to ask for nothing, but okay!
}
ret := make(map[addrs.ModuleSourceLocal]*Module, len(srcs))
problem := false
for sourceAddrRaw, sourceRaw := range srcs {
sourceAddr, err := addrs.ParseModuleSource(sourceAddrRaw)
if err != nil {
t.Errorf("invalid source address %q: %s", sourceAddrRaw, err)
problem = true
continue
}
localSourceAddr, ok := sourceAddr.(addrs.ModuleSourceLocal)
if !ok {
t.Errorf("invalid source address %q: only _local_ source addresses are allowed", sourceAddrRaw)
problem = true
continue
}
module := moduleFromStringForTesting(t, sourceRaw, sourceAddrRaw+"/for-testing.tf")
if module == nil {
// moduleFromStringForTesting should already have written log
// lines explaining the problem it encountered.
problem = true
continue
}
ret[localSourceAddr] = module
}
if problem {
// If we encountered at least one problem in the loop above then
// we'll halt testing now. (We wait to get here so that we can
// report errors in multiple elements at once when appropriate.)
t.FailNow()
}
return ret
}
// moduleFromStringForTesting is the common code from both
// [ModuleFromStringForTesting] and [ModulesFromStringsForTesting] which
// actually does the module loading.
//
// If errors occur then it calls t.Fail (indirectly) and emits log lines
// explaining the problem before returning nil. It's the caller's responsibility
// to halt further test execution with t.FailNow at some appropriate time.
func moduleFromStringForTesting(t testing.TB, src string, fakeFilename string) *Module {
t.Helper()
hclFile, diags := hclsyntax.ParseConfig([]byte(src), fakeFilename, hcl.InitialPos)
if diags.HasErrors() {
t.Errorf("unexpected syntax error: %s", diags.Error())
return nil
}
file, diags := loadConfigFileBody(hclFile.Body, fakeFilename, false, true)
if diags.HasErrors() {
t.Errorf("unexpected file analysis error: %s", diags.Error())
return nil
}
ret, diags := NewModuleUneval([]*File{file}, nil, fakeFilename, SelectiveLoadAll)
if diags.HasErrors() {
t.Errorf("unexpected module analysis error: %s", diags.Error())
return nil
}
return ret
}