Skip to content

Commit

Permalink
feat: facilitate testing scaffolds (#123)
Browse files Browse the repository at this point in the history
* simplify implementation

* add test mode to scaffold new command

* implement test example in ci

* change hello to cli

* fix cli

* use proper version

* duhhhh

* pull out test into file

* docs for testing

* rework tests to support cases and AST output

* fix output example

* re-org commands folder

* guard against out of bounds access

* revert back to new command with more generic implementation

* ignore file close err

* simplify test script for cli

* fix taskfile test

* basic testing scaffold docs

* rework script tests

* change ci runner

* fix and add tests for setting 'Project' var

* nested output tests

* reorder task files
  • Loading branch information
hay-kot authored May 10, 2024
1 parent f5e7489 commit fb10bda
Show file tree
Hide file tree
Showing 38 changed files with 784 additions and 425 deletions.
6 changes: 6 additions & 0 deletions .examples/cli/scaffold.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,9 @@ questions:
- "green"
- "blue"
- "yellow"

presets:
default:
Project: "scaffold-test-default"
description: "This is a test description"
colors: ["red", "green"]
85 changes: 1 addition & 84 deletions .examples/cli/{{ .ProjectKebab }}/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,91 +2,8 @@ package main

import (
"fmt"
"os"

"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/urfave/cli/v2"
)

var (
// Build information. Populated at build-time via -ldflags flag.
version = "dev"
commit = "HEAD"
date = "now"
)

func build() string {
short := commit
if len(commit) > 7 {
short = commit[:7]
}

return fmt.Sprintf("%s (%s) %s", version, short, date)
}

func main() {
ctrl := &controller{}

app := &cli.App{
Name: "{{ .Project }}",
Usage: "{{ .Scaffold.description }}",
Version: build(),
Flags: []cli.Flag{
&cli.StringFlag{
Name: "cwd",
Usage: "current working directory",
Value: ".",
},
&cli.StringFlag{
Name: "log-level",
Usage: "log level (debug, info, warn, error, fatal, panic)",
Value: "panic",
},
},
Before: func(ctx *cli.Context) error {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})

ctrl.cwd = ctx.String("cwd")
ctrl.logLevel = ctx.String("log-level")

switch ctrl.logLevel {
case "debug":
log.Level(zerolog.DebugLevel)
case "info":
log.Level(zerolog.InfoLevel)
case "warn":
log.Level(zerolog.WarnLevel)
case "error":
log.Level(zerolog.ErrorLevel)
case "fatal":
log.Level(zerolog.FatalLevel)
default:
log.Level(zerolog.PanicLevel)
}

return nil
},
Commands: []*cli.Command{
{
Name: "hello",
Usage: "Says hello world",
Action: ctrl.HelloWorld,
},
},
}

if err := app.Run(os.Args); err != nil {
log.Fatal().Err(err).Msg("failed to run scaffold")
}
}

type controller struct {
cwd string
logLevel string
}

func (c *controller) HelloWorld(ctx *cli.Context) error {
fmt.Println("Hello, your favorite colors are {{ .Scaffold.colors | join `, ` }}")
return nil
fmt.Println("colors={{ .Scaffold.colors | join `, ` }} description={{ .Scaffold.description }}")
}
25 changes: 25 additions & 0 deletions .examples/nested/scaffold.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
questions:
- name: "question_1"
prompt:
message: "Question 1"
required: true
- name: "question_2"
prompt:
message: "Question 2"
required: true
- name: "question_3"
prompt:
message: "Question 3"
required: true
- name: "question_4"
prompt:
message: "Question 4"
required: true

presets:
default:
Project: "nested-defaults"
question_1: "Answer 1"
question_2: "Answer 2"
question_3: "Answer 3"
question_4: "Answer 4"
1 change: 1 addition & 0 deletions .examples/nested/{{ .ProjectKebab }}/child/child_1.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{ .Scaffold.question_2 }}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{ .Scaffold.question_3 }}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{ .Scaffold.question_4 }}
1 change: 1 addition & 0 deletions .examples/nested/{{ .ProjectKebab }}/root.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{ .Scaffold.question_1 }}
12 changes: 5 additions & 7 deletions .examples/role/scaffold.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,8 @@ rewrites:
computed:
snaked: "{{ snakecase .Scaffold.role_name }}"
static: false
inject:
- name: "add role to site.yaml"
path: site.yaml
at: "# $Scaffold.role_name"
template: |
- name: {{ .Scaffold.role_name }}
role: {{ .Computed.snaked }}

test:
role_name: "test_role"
toggle: true
description: "This is a test role"
5 changes: 4 additions & 1 deletion .github/workflows/partial-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ jobs:
Go:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4
- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
Expand All @@ -26,3 +26,6 @@ jobs:

- name: Test
run: go test ./... -race

- name: Test 'cli' example
run: ./tests/runner.sh
File renamed without changes.
File renamed without changes.
File renamed without changes.
168 changes: 168 additions & 0 deletions app/commands/cmd_new.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package commands

import (
"fmt"
"math/rand"
"os"
"strings"

"github.com/go-git/go-git/v5/plumbing/transport"
"github.com/go-git/go-git/v5/plumbing/transport/http"
"github.com/hay-kot/scaffold/app/core/fsast"
"github.com/hay-kot/scaffold/app/scaffold"
"github.com/hay-kot/scaffold/app/scaffold/pkgs"
"github.com/sahilm/fuzzy"
)

type FlagsNew struct {
NoPrompt bool
Preset string
Snapshot string
}

func (ctrl *Controller) New(args []string, flags FlagsNew) error {
if len(args) == 0 {
return fmt.Errorf("missing scaffold name")
}

path, err := ctrl.resolve(args[0], flags.NoPrompt)
if err != nil {
return err
}

if path == "" {
return fmt.Errorf("missing scaffold path")
}

rest := args[1:]
argvars, err := parseArgVars(rest)
if err != nil {
return err
}

var varfunc func(*scaffold.Project) (map[string]any, error)
switch {
case flags.NoPrompt:
varfunc = func(p *scaffold.Project) (map[string]any, error) {
caseVars, ok := p.Conf.Presets[flags.Preset]
if !ok {
return nil, fmt.Errorf("case %s not found", flags.Preset)
}

project, ok := caseVars["Project"].(string)
if !ok || project == "" {
// Generate 4 random digits
name := fmt.Sprintf("scaffold-test-%04d", rand.Intn(10000))
caseVars["Project"] = name
project = name
}
p.Name = project

// Test cases do not use rc.Defaults
vars := scaffold.MergeMaps(caseVars, argvars)
return vars, nil
}

default:
varfunc = func(p *scaffold.Project) (map[string]any, error) {
vars := scaffold.MergeMaps(argvars, ctrl.rc.Defaults)
vars, err = p.AskQuestions(vars, ctrl.engine)
if err != nil {
return nil, err
}

return vars, nil
}
}

outfs := ctrl.Flags.OutputFS()

err = ctrl.runscaffold(runconf{
scaffolddir: path,
showMessages: !flags.NoPrompt,
varfunc: varfunc,
outputfs: outfs,
})
if err != nil {
return err
}

if flags.Snapshot != "" {
ast, err := fsast.New(outfs)
if err != nil {
return err
}

if flags.Snapshot == "stdout" {
fmt.Println(ast.String())
} else {
file, err := os.Create(flags.Snapshot)
if err != nil {
return err
}

_ = file.Close()

_, err = file.WriteString(ast.String())
if err != nil {
return err
}
}
}

return nil
}

func (ctrl *Controller) fuzzyFallBack(str string) ([]string, []string, error) {
systemScaffolds, err := pkgs.ListSystem(os.DirFS(ctrl.Flags.Cache))
if err != nil {
return nil, nil, err
}

localScaffolds, err := pkgs.ListLocal(os.DirFS(ctrl.Flags.OutputDir))
if err != nil {
return nil, nil, err
}

systemMatches := fuzzy.Find(str, systemScaffolds)
systemMatchesOutput := make([]string, len(systemMatches))
for i, match := range systemMatches {
systemMatchesOutput[i] = match.Str
}

localMatches := fuzzy.Find(str, localScaffolds)
localMatchesOutput := make([]string, len(localMatches))
for i, match := range localMatches {
localMatchesOutput[i] = match.Str
}

return systemMatchesOutput, localMatchesOutput, nil
}

func basicAuthAuthorizer(pkgurl, username, password string) pkgs.AuthProviderFunc {
return func(url string) (transport.AuthMethod, bool) {
if url != pkgurl {
return nil, false
}

return &http.BasicAuth{
Username: username,
Password: password,
}, true
}
}

func parseArgVars(args []string) (map[string]any, error) {
vars := make(map[string]any, len(args))

for _, v := range args {
if !strings.Contains(v, "=") {
return nil, fmt.Errorf("variable %s is not in the form of key=value", v)
}

kv := strings.Split(v, "=")
vars[kv[0]] = kv[1]
}

return vars, nil
}
15 changes: 12 additions & 3 deletions app/commands/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package commands

import (
"github.com/hay-kot/scaffold/app/core/engine"
"github.com/hay-kot/scaffold/app/core/rwfs"
"github.com/hay-kot/scaffold/app/scaffold"
)

Expand All @@ -13,16 +14,24 @@ type Flags struct {
Cache string
OutputDir string
ScaffoldDirs []string
Cwd string
}

// OutputFS returns a WriteFS based on the OutputDir flag
func (f Flags) OutputFS() rwfs.WriteFS {
if f.OutputDir == ":memory:" {
return rwfs.NewMemoryWFS()
}

return rwfs.NewOsWFS(f.OutputDir)
}

type Controller struct {
// Global Flags
// Flags contains the CLI flags
// that are from the root command
Flags Flags

engine *engine.Engine
rc *scaffold.ScaffoldRC
vars map[string]string
prepared bool
}

Expand Down
Loading

0 comments on commit fb10bda

Please sign in to comment.