feat: add scaffolder
This commit is contained in:
130
internal/fetcher/fetcher.go
Normal file
130
internal/fetcher/fetcher.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package fetcher
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Fetcher allows pulling from an upstream scaffold registry. This is hard coded to the lunarway/scaffold registry, it can also be provided by a path which in that case, will not do anything
|
||||
type Fetcher struct{}
|
||||
|
||||
func NewFetcher() *Fetcher {
|
||||
return &Fetcher{}
|
||||
}
|
||||
|
||||
const readWriteExec = 0o644
|
||||
|
||||
const githubProject = "kjuulh/scaffold"
|
||||
|
||||
var (
|
||||
scaffoldFolder = os.ExpandEnv("$HOME/.scaffold")
|
||||
scaffoldClone = path.Join(scaffoldFolder, "upstream")
|
||||
scaffoldCache = path.Join(scaffoldFolder, "scaffold.updates.json")
|
||||
)
|
||||
|
||||
func (f *Fetcher) CloneRepository(ctx context.Context, registryPath *string, ui *slog.Logger) error {
|
||||
if err := os.MkdirAll(scaffoldFolder, readWriteExec); err != nil {
|
||||
return fmt.Errorf("failed to create scaffold folder: %w", err)
|
||||
}
|
||||
|
||||
if *registryPath == "" {
|
||||
if _, err := os.Stat(scaffoldClone); err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
return fmt.Errorf("failed to find the upstream folder: %w", err)
|
||||
}
|
||||
|
||||
ui.Info("cloning upstream templates")
|
||||
if err := cloneUpstream(ctx); err != nil {
|
||||
return fmt.Errorf("failed to clone upstream registry: %w", err)
|
||||
}
|
||||
} else {
|
||||
now := time.Now()
|
||||
lastUpdatedUnix := getCacheUpdate(ui, ctx)
|
||||
lastUpdated := time.Unix(lastUpdatedUnix, 0)
|
||||
|
||||
// Cache for 7 days
|
||||
if lastUpdated.Before(now.Add(-time.Hour * 24 * 7)) {
|
||||
ui.Info("update templates folder")
|
||||
if err := f.UpdateUpstream(ctx); err != nil {
|
||||
return fmt.Errorf("failed to update upstream scaffold folder: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Fetcher) UpdateUpstream(ctx context.Context) error {
|
||||
cmd := exec.CommandContext(ctx, "git", "pull", "--rebase")
|
||||
cmd.Dir = scaffoldClone
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
fmt.Printf("git pull failed with output: %s\n\n", string(output))
|
||||
return fmt.Errorf("git pull failed: %w", err)
|
||||
}
|
||||
|
||||
if err := createCacheUpdate(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func cloneUpstream(ctx context.Context) error {
|
||||
cmd := exec.CommandContext(ctx, "coffee", "repo", "clone", githubProject, scaffoldClone)
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
fmt.Printf("git clone failed with output: %s\n\n", string(output))
|
||||
return fmt.Errorf("git clone failed: %w", err)
|
||||
}
|
||||
|
||||
if err := createCacheUpdate(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type CacheUpdate struct {
|
||||
LastUpdated int64 `json:"lastUpdated"`
|
||||
}
|
||||
|
||||
func createCacheUpdate(_ context.Context) error {
|
||||
content, err := json.Marshal(CacheUpdate{
|
||||
LastUpdated: time.Now().Unix(),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to prepare cache update: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(scaffoldCache, content, readWriteExec); err != nil {
|
||||
return fmt.Errorf("failed to write cache update: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getCacheUpdate(ui *slog.Logger, _ context.Context) int64 {
|
||||
content, err := os.ReadFile(scaffoldCache)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
var cacheUpdate CacheUpdate
|
||||
if err := json.Unmarshal(content, &cacheUpdate); err != nil {
|
||||
ui.Warn("failed to read cache, it might be invalid", "error", err)
|
||||
return 0
|
||||
}
|
||||
|
||||
return cacheUpdate.LastUpdated
|
||||
}
|
163
internal/templates/loader.go
Normal file
163
internal/templates/loader.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package templates
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
gotmpl "text/template"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
// TemplateLoader reads a templates files and runs their respective templating on them.
|
||||
type TemplateLoader struct{}
|
||||
|
||||
func NewTemplateLoader() *TemplateLoader {
|
||||
return &TemplateLoader{}
|
||||
}
|
||||
|
||||
type File struct {
|
||||
content []byte
|
||||
path string
|
||||
RelPath string
|
||||
}
|
||||
|
||||
var funcs = gotmpl.FuncMap{
|
||||
"ReplaceAll": strings.ReplaceAll,
|
||||
"ToLower": strings.ToLower,
|
||||
"ToUpper": strings.ToUpper,
|
||||
}
|
||||
|
||||
// TemplatePath formats the template file path using go templates, this is useful for programmatically changing the output string using go tmpls
|
||||
func TemplatePath(template *Template) (string, error) {
|
||||
tmpl, err := gotmpl.New("path").Funcs(funcs).Parse(template.File.Default.Path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
output := bytes.NewBufferString("")
|
||||
if err := tmpl.Execute(output, template); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
templatePath := strings.TrimSpace(output.String())
|
||||
|
||||
return templatePath, nil
|
||||
}
|
||||
|
||||
// Load loads the template files from disk
|
||||
func (t *TemplateLoader) Load(ctx context.Context, template *Template) ([]File, error) {
|
||||
templateFilePath := path.Join(template.Path, "files")
|
||||
if _, err := os.Stat(templateFilePath); err != nil {
|
||||
return nil, fmt.Errorf("failed to lookup template files %s, %w", templateFilePath, err)
|
||||
}
|
||||
|
||||
filePaths := make([]string, 0)
|
||||
err := filepath.WalkDir(templateFilePath, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Is a file
|
||||
if d.Type().IsRegular() {
|
||||
filePaths = append(filePaths, path)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read template files: %w", err)
|
||||
}
|
||||
|
||||
var (
|
||||
filesLock sync.Mutex
|
||||
files = make([]File, 0)
|
||||
)
|
||||
egrp, _ := errgroup.WithContext(ctx)
|
||||
for _, filePath := range filePaths {
|
||||
egrp.Go(func() error {
|
||||
fileContent, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read file: %s, %w", filePath, err)
|
||||
}
|
||||
|
||||
filesLock.Lock()
|
||||
defer filesLock.Unlock()
|
||||
|
||||
files = append(files, File{
|
||||
content: fileContent,
|
||||
path: filePath,
|
||||
RelPath: strings.TrimPrefix(strings.TrimPrefix(filePath, templateFilePath), "/"),
|
||||
})
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
}
|
||||
if err := egrp.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
type TemplatedFile struct {
|
||||
Content []byte
|
||||
DestinationPath string
|
||||
}
|
||||
|
||||
// TemplateFiles runs the actual templating on the files, and tells it where to go. The writes doesn't happen here yet.
|
||||
func (l *TemplateLoader) TemplateFiles(template *Template, files []File, scaffoldDest string) ([]TemplatedFile, error) {
|
||||
templatedFiles := make([]TemplatedFile, 0)
|
||||
for _, file := range files {
|
||||
tmpl, err := gotmpl.
|
||||
New(file.RelPath).
|
||||
Funcs(funcs).
|
||||
Parse(string(file.content))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse template file: %s, %w", file.RelPath, err)
|
||||
}
|
||||
|
||||
output := bytes.NewBufferString("")
|
||||
if err := tmpl.Execute(output, template); err != nil {
|
||||
return nil, fmt.Errorf("failed to write template file: %s, %w", file.RelPath, err)
|
||||
}
|
||||
|
||||
fileDir := path.Dir(file.RelPath)
|
||||
fileName := strings.TrimSuffix(path.Base(file.RelPath), ".gotmpl")
|
||||
if fileConfig, ok := template.File.Files[strings.TrimSuffix(file.RelPath, ".gotmpl")]; ok && fileConfig.Rename != "" {
|
||||
renameTmpl, err := gotmpl.New(file.RelPath).Funcs(funcs).Parse(fileConfig.Rename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse rename for: %s in scaffold.yaml: %w", file.RelPath, err)
|
||||
}
|
||||
|
||||
type RenameContext struct {
|
||||
Template
|
||||
OriginalFileName string
|
||||
}
|
||||
|
||||
output := bytes.NewBufferString("")
|
||||
if err := renameTmpl.Execute(output, RenameContext{
|
||||
Template: *template,
|
||||
OriginalFileName: fileName,
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("failed to template rename: %s, %w", file.RelPath, err)
|
||||
}
|
||||
|
||||
fileName = strings.TrimSpace(output.String())
|
||||
}
|
||||
|
||||
templatedFiles = append(templatedFiles, TemplatedFile{
|
||||
Content: output.Bytes(),
|
||||
DestinationPath: path.Join(scaffoldDest, fileDir, fileName),
|
||||
})
|
||||
}
|
||||
|
||||
return templatedFiles, nil
|
||||
}
|
97
internal/templates/templates.go
Normal file
97
internal/templates/templates.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package templates
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// TemplateIndexer loads all template specifications from the registry, allowing the caller to choose on of them, or simply display their properties.
|
||||
type TemplateIndexer struct{}
|
||||
|
||||
func NewTemplateIndexer() *TemplateIndexer {
|
||||
return &TemplateIndexer{}
|
||||
}
|
||||
|
||||
type Template struct {
|
||||
File TemplateFile
|
||||
Path string
|
||||
|
||||
Input map[string]string
|
||||
}
|
||||
|
||||
type TemplateDefault struct {
|
||||
Path string `yaml:"path"`
|
||||
}
|
||||
type TemplateInputs map[string]TemplateInput
|
||||
|
||||
type TemplateInput struct {
|
||||
Type string `yaml:"type"`
|
||||
Description string `yaml:"description"`
|
||||
Default string `yaml:"default"`
|
||||
}
|
||||
|
||||
type TemplateFileConfig struct {
|
||||
Rename string `yaml:"rename"`
|
||||
}
|
||||
|
||||
type TemplateFile struct {
|
||||
Name string `yaml:"name"`
|
||||
Default TemplateDefault `yaml:"default"`
|
||||
Input TemplateInputs `yaml:"input"`
|
||||
Files map[string]TemplateFileConfig
|
||||
}
|
||||
|
||||
func (t *TemplateIndexer) Index(ctx context.Context, scaffoldRegistryFolder string, ui *slog.Logger) ([]Template, error) {
|
||||
ui.Debug("Loading templates...")
|
||||
|
||||
templateDirEntries, err := os.ReadDir(scaffoldRegistryFolder)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read templates dir: %w", err)
|
||||
}
|
||||
|
||||
var (
|
||||
templates = make([]Template, 0)
|
||||
templatesLock sync.Mutex
|
||||
)
|
||||
egrp, _ := errgroup.WithContext(ctx)
|
||||
for _, templateDirEntry := range templateDirEntries {
|
||||
egrp.Go(func() error {
|
||||
templatePath := path.Join(scaffoldRegistryFolder, templateDirEntry.Name())
|
||||
|
||||
content, err := os.ReadFile(path.Join(templatePath, "scaffold.yaml"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read: %s, %w", templateDirEntry.Name(), err)
|
||||
}
|
||||
|
||||
var template TemplateFile
|
||||
if err := yaml.Unmarshal(content, &template); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal template: %s, %w", string(content), err)
|
||||
}
|
||||
|
||||
templatesLock.Lock()
|
||||
defer templatesLock.Unlock()
|
||||
templates = append(templates, Template{
|
||||
File: template,
|
||||
Path: templatePath,
|
||||
Input: make(map[string]string),
|
||||
})
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if err := egrp.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ui.Debug("Done loading templates...", "amount", len(templates))
|
||||
|
||||
return templates, nil
|
||||
}
|
84
internal/templates/writer.go
Normal file
84
internal/templates/writer.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package templates
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
const readWriteExec = 0o755
|
||||
const readExec = 0o644
|
||||
|
||||
type PromptOverride func(file TemplatedFile) (bool, error)
|
||||
|
||||
// FileWriter writes the actual files to disk, it optionally takes a promptOverride which allows the caller to stop a potential override of a file
|
||||
type FileWriter struct {
|
||||
promptOverride PromptOverride
|
||||
}
|
||||
|
||||
func NewFileWriter() *FileWriter {
|
||||
return &FileWriter{
|
||||
promptOverride: nil,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FileWriter) WithPromptOverride(po PromptOverride) *FileWriter {
|
||||
f.promptOverride = po
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *FileWriter) Write(ctx context.Context, ui *slog.Logger, templatedFiles []TemplatedFile) error {
|
||||
var fileExistsLock sync.Mutex
|
||||
|
||||
egrp, _ := errgroup.WithContext(ctx)
|
||||
for _, file := range templatedFiles {
|
||||
egrp.Go(func() error {
|
||||
if _, err := os.Stat(file.DestinationPath); err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
return fmt.Errorf("failed to check if file exists: %s, %w", file.DestinationPath, err)
|
||||
}
|
||||
} else {
|
||||
if f.promptOverride != nil {
|
||||
fileExistsLock.Lock()
|
||||
defer fileExistsLock.Unlock()
|
||||
|
||||
override, err := f.promptOverride(file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get answer to whether a file should be overwritten or not: %w", err)
|
||||
}
|
||||
|
||||
if !override {
|
||||
ui.Warn("Skipping file", "file", file.DestinationPath)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if parent := path.Dir(file.DestinationPath); parent != "" && parent != "/" {
|
||||
if err := os.MkdirAll(parent, readWriteExec); err != nil {
|
||||
return fmt.Errorf("failed to create parent dir for: %s, %w", file.DestinationPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
ui.Info("writing file", "path", file.DestinationPath)
|
||||
if err := os.WriteFile(file.DestinationPath, file.Content, readExec); err != nil {
|
||||
return fmt.Errorf("failed to write file: %s, %w", file.DestinationPath, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if err := egrp.Wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
206
internal/tests/fixture.go
Normal file
206
internal/tests/fixture.go
Normal file
@@ -0,0 +1,206 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.front.kjuulh.io/kjuulh/scaffold/internal/templates"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// ScaffoldFixture provides an api on top of the scaffold templater, this is opposed to calling the cli
|
||||
type ScaffoldFixture struct {
|
||||
vars map[string]string
|
||||
path string
|
||||
}
|
||||
|
||||
func (s *ScaffoldFixture) WithVariable(key, val string) *ScaffoldFixture {
|
||||
s.vars[key] = val
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *ScaffoldFixture) WithPath(path string) *ScaffoldFixture {
|
||||
s.path = path
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// TestFixture is an opinionated way for the templates to be able to test their code, this also works as an accepttest for the scaffolder itself.
|
||||
type TestFixture struct {
|
||||
pkg string
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
func Test(t *testing.T, pkg string) *TestFixture {
|
||||
return &TestFixture{
|
||||
pkg: pkg,
|
||||
t: t,
|
||||
}
|
||||
}
|
||||
|
||||
// ScaffoldDefaultTest tests that the code can be run with the default variables. We want to have sane defaults for most things. As such there is an opinionated way of running these tests
|
||||
func (f *TestFixture) ScaffoldDefaultTest(testName string) *TestFixture {
|
||||
f.ScaffoldTest(testName, func(fixture *ScaffoldFixture) {})
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
// ScaffoldTest is a large fixture, which allows running the accepttest over a template, in turn creating files and comparing them. either way they have to match, if the templater generated more files we expect, it fails, if they're different the test fails
|
||||
func (f *TestFixture) ScaffoldTest(testName string, input func(fixture *ScaffoldFixture)) *TestFixture {
|
||||
f.t.Run(
|
||||
f.pkg,
|
||||
func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testName := strings.ToLower(strings.ReplaceAll(testName, " ", "_"))
|
||||
|
||||
fixture := &ScaffoldFixture{
|
||||
vars: make(map[string]string),
|
||||
}
|
||||
input(fixture)
|
||||
|
||||
ctx := t.Context()
|
||||
|
||||
indexer := templates.NewTemplateIndexer()
|
||||
loader := templates.NewTemplateLoader()
|
||||
writer := templates.NewFileWriter()
|
||||
ui := slog.New(slog.NewTextHandler(os.Stderr, nil))
|
||||
|
||||
templateFiles, err := indexer.Index(ctx, "../", ui)
|
||||
require.NoError(t, err)
|
||||
|
||||
template, err := find(templateFiles, f.pkg)
|
||||
require.NoError(t, err, "failed to find a template")
|
||||
|
||||
files, err := loader.Load(ctx, template)
|
||||
require.NoError(t, err, "failed to load template files")
|
||||
|
||||
for input, inputSpec := range template.File.Input {
|
||||
template.Input[input] = inputSpec.Default
|
||||
}
|
||||
|
||||
for key, val := range fixture.vars {
|
||||
template.Input[key] = val
|
||||
}
|
||||
|
||||
templatePath, err := templates.TemplatePath(template)
|
||||
require.NoError(t, err)
|
||||
if fixture.path != "" {
|
||||
templatePath = fixture.path
|
||||
}
|
||||
|
||||
actualPath := path.Join("testdata", testName, "actual")
|
||||
expectedPath := path.Join("testdata", testName, "expected")
|
||||
|
||||
templatedFiles, err := loader.TemplateFiles(template, files, path.Join(actualPath, templatePath))
|
||||
require.NoError(t, err, "failed to template files")
|
||||
|
||||
err = os.RemoveAll(actualPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = writer.Write(ctx, ui, templatedFiles)
|
||||
require.NoError(t, err, "failed to write files")
|
||||
|
||||
actualFiles, err := getFiles(actualPath)
|
||||
require.NoError(t, err, "failed to get actual files")
|
||||
|
||||
expectedFiles, err := getFiles(expectedPath)
|
||||
assert.NoError(t, err, "failed to get expected files")
|
||||
|
||||
slices.Sort(actualFiles)
|
||||
slices.Sort(expectedFiles)
|
||||
|
||||
assert.Equal(
|
||||
t,
|
||||
makeRelative(expectedPath, expectedFiles),
|
||||
makeRelative(actualPath, actualFiles),
|
||||
"expected and actual files didn't match",
|
||||
)
|
||||
|
||||
compareFiles(t,
|
||||
expectedPath, actualPath,
|
||||
expectedFiles, actualFiles,
|
||||
)
|
||||
})
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
func compareFiles(t *testing.T, expectedPath, actualPath string, expectedFiles, actualFiles []string) {
|
||||
expectedRelativeFiles := makeRelative(expectedPath, expectedFiles)
|
||||
actualRelativeFiles := makeRelative(actualPath, actualFiles)
|
||||
|
||||
for expectedIndex, expectedRelativeFile := range expectedRelativeFiles {
|
||||
for actualIndex, actualRelativeFile := range actualRelativeFiles {
|
||||
if expectedRelativeFile == actualRelativeFile {
|
||||
expectedFilePath := expectedFiles[expectedIndex]
|
||||
actualFilePath := actualFiles[actualIndex]
|
||||
|
||||
expectedFile, err := os.ReadFile(expectedFilePath)
|
||||
require.NoError(t, err, "failed to read expected file")
|
||||
|
||||
actualFile, err := os.ReadFile(actualFilePath)
|
||||
require.NoError(t, err, "failed to read actual file")
|
||||
|
||||
assert.Equal(t,
|
||||
string(expectedFile), string(actualFile),
|
||||
"expected and actual file doesn't match\n\texpected path=%s\n\t actual path=%s",
|
||||
expectedFilePath, actualFilePath,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// makeRelative, test files are prefixed with either actual or expected, this makes it hard to compare, this makes them comparable by removing their unique folder prefix.
|
||||
func makeRelative(prefix string, filePaths []string) []string {
|
||||
output := make([]string, 0, len(filePaths))
|
||||
for _, filePath := range filePaths {
|
||||
relative := strings.TrimPrefix(strings.TrimPrefix(filePath, prefix), "/")
|
||||
|
||||
output = append(output, relative)
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
func find(templates []templates.Template, templateName string) (*templates.Template, error) {
|
||||
templateNames := make([]string, 0)
|
||||
for _, template := range templates {
|
||||
if template.File.Name == templateName {
|
||||
return &template, nil
|
||||
}
|
||||
|
||||
templateNames = append(templateNames, template.File.Name)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("template was not found: %s", strings.Join(templateNames, ", "))
|
||||
}
|
||||
|
||||
func getFiles(root string) ([]string, error) {
|
||||
actualFiles := make([]string, 0)
|
||||
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if d.Type().IsRegular() {
|
||||
actualFiles = append(actualFiles, path)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return actualFiles, nil
|
||||
}
|
Reference in New Issue
Block a user