Skip to content

Commit

Permalink
Add test file HCL configuration and parser functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
liamcervante committed Jun 12, 2023
1 parent 4e373dd commit 7a8d712
Show file tree
Hide file tree
Showing 14 changed files with 692 additions and 8 deletions.
11 changes: 10 additions & 1 deletion internal/configs/configload/loader_load.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,16 @@ import (
// LoadConfig performs the basic syntax and uniqueness validations that are
// required to process the individual modules
func (l *Loader) LoadConfig(rootDir string) (*configs.Config, hcl.Diagnostics) {
rootMod, diags := l.parser.LoadConfigDir(rootDir)
return l.loadConfig(l.parser.LoadConfigDir(rootDir))
}

// LoadConfigWithTests matches LoadConfig, except the configs.Config contains
// any relevant .tftest files.
func (l *Loader) LoadConfigWithTests(rootDir string, testDir string) (*configs.Config, hcl.Diagnostics) {
return l.loadConfig(l.parser.LoadConfigDirWithTests(rootDir, testDir))
}

func (l *Loader) loadConfig(rootMod *configs.Module, diags hcl.Diagnostics) (*configs.Config, hcl.Diagnostics) {
if rootMod == nil || diags.HasErrors() {
// Ensure we return any parsed modules here so that required_version
// constraints can be verified even when encountering errors.
Expand Down
13 changes: 13 additions & 0 deletions internal/configs/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ type Module struct {
Import []*Import

Checks map[string]*Check

Tests map[string]*TestFile
}

// File describes the contents of a single configuration file.
Expand Down Expand Up @@ -92,6 +94,16 @@ type File struct {
Checks []*Check
}

// NewModuleWithTests matches NewModule except it will also load in the provided
// test files.
func NewModuleWithTests(primaryFiles, overrideFiles []*File, testFiles map[string]*TestFile) (*Module, hcl.Diagnostics) {
mod, diags := NewModule(primaryFiles, overrideFiles)
if mod != nil {
mod.Tests = testFiles
}
return mod, diags
}

// NewModule takes a list of primary files and a list of override files and
// produces a *Module by combining the files together.
//
Expand All @@ -113,6 +125,7 @@ func NewModule(primaryFiles, overrideFiles []*File) (*Module, hcl.Diagnostics) {
DataResources: map[string]*Resource{},
Checks: map[string]*Check{},
ProviderMetas: map[addrs.Provider]*ProviderMeta{},
Tests: map[string]*TestFile{},
}

// Process the required_providers blocks first, to ensure that all
Expand Down
16 changes: 16 additions & 0 deletions internal/configs/parser_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,22 @@ func (p *Parser) LoadConfigFileOverride(path string) (*File, hcl.Diagnostics) {
return p.loadConfigFile(path, true)
}

// LoadTestFile reads the file at the given path and parses it as a Terraform
// test file.
//
// It references the same LoadHCLFile as LoadConfigFile, so inherits the same
// syntax selection behaviours.
func (p *Parser) LoadTestFile(path string) (*TestFile, hcl.Diagnostics) {
body, diags := p.LoadHCLFile(path)
if body == nil {
return nil, diags
}

test, testDiags := loadTestFile(body)
diags = append(diags, testDiags...)
return test, diags
}

func (p *Parser) loadConfigFile(path string, override bool) (*File, hcl.Diagnostics) {
body, diags := p.LoadHCLFile(path)
if body == nil {
Expand Down
106 changes: 99 additions & 7 deletions internal/configs/parser_config_dir.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import (
// .tf files are parsed using the HCL native syntax while .tf.json files are
// parsed using the HCL JSON syntax.
func (p *Parser) LoadConfigDir(path string) (*Module, hcl.Diagnostics) {
primaryPaths, overridePaths, diags := p.dirFiles(path)
primaryPaths, overridePaths, _, diags := p.dirFiles(path, "")
if diags.HasErrors() {
return nil, diags
}
Expand All @@ -50,20 +50,51 @@ func (p *Parser) LoadConfigDir(path string) (*Module, hcl.Diagnostics) {
return mod, diags
}

// LoadConfigDirWithTests matches LoadConfigDir, but the return Module also
// contains any relevant .tftest files.
func (p *Parser) LoadConfigDirWithTests(path string, testDirectory string) (*Module, hcl.Diagnostics) {
primaryPaths, overridePaths, testPaths, diags := p.dirFiles(path, testDirectory)
if diags.HasErrors() {
return nil, diags
}

primary, fDiags := p.loadFiles(primaryPaths, false)
diags = append(diags, fDiags...)
override, fDiags := p.loadFiles(overridePaths, true)
diags = append(diags, fDiags...)
tests, fDiags := p.loadTestFiles(path, testPaths)
diags = append(diags, fDiags...)

mod, modDiags := NewModuleWithTests(primary, override, tests)
diags = append(diags, modDiags...)

mod.SourceDir = path

return mod, diags
}

// ConfigDirFiles returns lists of the primary and override files configuration
// files in the given directory.
//
// If the given directory does not exist or cannot be read, error diagnostics
// are returned. If errors are returned, the resulting lists may be incomplete.
func (p Parser) ConfigDirFiles(dir string) (primary, override []string, diags hcl.Diagnostics) {
return p.dirFiles(dir)
primary, override, _, diags = p.dirFiles(dir, "")
return primary, override, diags
}

// ConfigDirFilesWithTests matches ConfigDirFiles except it also returns the
// paths to any test files within the module.
func (p Parser) ConfigDirFilesWithTests(dir string, testDirectory string) (primary, override, tests []string, diags hcl.Diagnostics) {
return p.dirFiles(dir, testDirectory)
}

// IsConfigDir determines whether the given path refers to a directory that
// exists and contains at least one Terraform config file (with a .tf or
// .tf.json extension.)
// .tf.json extension.). Note, we explicitely exclude checking for tests here
// as tests must live alongside actual .tf config files.
func (p *Parser) IsConfigDir(path string) bool {
primaryPaths, overridePaths, _ := p.dirFiles(path)
primaryPaths, overridePaths, _, _ := p.dirFiles(path, "")
return (len(primaryPaths) + len(overridePaths)) > 0
}

Expand All @@ -88,7 +119,9 @@ func (p *Parser) loadFiles(paths []string, override bool) ([]*File, hcl.Diagnost
return files, diags
}

func (p *Parser) dirFiles(dir string) (primary, override []string, diags hcl.Diagnostics) {
func (p *Parser) dirFiles(dir string, testsDir string) (primary, override, tests []string, diags hcl.Diagnostics) {
includeTests := len(testsDir) > 0

infos, err := p.fs.ReadDir(dir)
if err != nil {
diags = append(diags, &hcl.Diagnostic{
Expand All @@ -101,7 +134,31 @@ func (p *Parser) dirFiles(dir string) (primary, override []string, diags hcl.Dia

for _, info := range infos {
if info.IsDir() {
// We only care about files
if includeTests && info.Name() == testsDir {
testsDir := filepath.Join(dir, info.Name())
testInfos, err := p.fs.ReadDir(testsDir)
if err != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Failed to read module test directory",
Detail: fmt.Sprintf("Module test directory %s does not exist or cannot be read.", testsDir),
})
return
}

for _, testInfo := range testInfos {
if testInfo.IsDir() || IsIgnoredFile(testInfo.Name()) {
continue
}

if strings.HasSuffix(testInfo.Name(), ".tftest") || strings.HasSuffix(testInfo.Name(), ".tftest.json") {
tests = append(tests, filepath.Join(testsDir, testInfo.Name()))
}
}
}

// We only care about the tests directory or terraform configuration
// files.
continue
}

Expand All @@ -111,6 +168,13 @@ func (p *Parser) dirFiles(dir string) (primary, override []string, diags hcl.Dia
continue
}

if ext == ".tftest" || ext == ".tftest.json" {
if includeTests {
tests = append(tests, filepath.Join(dir, name))
}
continue
}

baseName := name[:len(name)-len(ext)] // strip extension
isOverride := baseName == "override" || strings.HasSuffix(baseName, "_override")

Expand All @@ -125,13 +189,41 @@ func (p *Parser) dirFiles(dir string) (primary, override []string, diags hcl.Dia
return
}

func (p *Parser) loadTestFiles(basePath string, paths []string) (map[string]*TestFile, hcl.Diagnostics) {
var diags hcl.Diagnostics

tfs := make(map[string]*TestFile)
for _, path := range paths {
tf, fDiags := p.LoadTestFile(path)
diags = append(diags, fDiags...)
if tf != nil {
// We index test files relative to the module they are testing, so
// the key is the relative path between basePath and path.
relPath, err := filepath.Rel(basePath, path)
if err != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagWarning,
Summary: "Failed to calculate relative path",
Detail: fmt.Sprintf("Terraform could not calculate the relative path for test file %s and it has been skipped: %s", path, err),
})
continue
}
tfs[relPath] = tf
}
}

return tfs, diags
}

// fileExt returns the Terraform configuration extension of the given
// path, or a blank string if it is not a recognized extension.
func fileExt(path string) string {
if strings.HasSuffix(path, ".tf") {
return ".tf"
} else if strings.HasSuffix(path, ".tf.json") {
return ".tf.json"
} else if strings.HasSuffix(path, ".tftest") {
return ".tftest"
} else {
return ""
}
Expand All @@ -157,7 +249,7 @@ func IsEmptyDir(path string) (bool, error) {
}

p := NewParser(nil)
fs, os, diags := p.dirFiles(path)
fs, os, _, diags := p.dirFiles(path, "")
if diags.HasErrors() {
return false, diags
}
Expand Down
30 changes: 30 additions & 0 deletions internal/configs/parser_config_dir_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ func TestParserLoadConfigDirSuccess(t *testing.T) {
if mod.SourceDir != path {
t.Errorf("wrong SourceDir value %q; want %s", mod.SourceDir, path)
}

if len(mod.Tests) > 0 {
// We only load tests when requested, and we didn't request this
// time.
t.Errorf("should not have loaded tests, but found %d", len(mod.Tests))
}
})
}

Expand Down Expand Up @@ -107,6 +113,30 @@ func TestParserLoadConfigDirSuccess(t *testing.T) {

}

func TestParserLoadConfigDirWithTests(t *testing.T) {
directories := []string{
"testdata/valid-modules/with-tests",
"testdata/valid-modules/with-tests-nested",
}

for _, directory := range directories {
t.Run(directory, func(t *testing.T) {
parser := NewParser(nil)
mod, diags := parser.LoadConfigDirWithTests(directory, "tests")
if diags.HasErrors() {
t.Errorf("unexpected error diagnostics")
for _, diag := range diags {
t.Logf("- %s", diag)
}
}

if len(mod.Tests) != 2 {
t.Errorf("incorrect number of test files found: %d", len(mod.Tests))
}
})
}
}

// TestParseLoadConfigDirFailure is a simple test that just verifies that
// a number of test configuration directories (in testdata/invalid-modules)
// produce diagnostics when parsed.
Expand Down
Loading

0 comments on commit 7a8d712

Please sign in to comment.