This repository has been archived on 2024-05-03. You can view files and clone it, but cannot push or open issues or pull requests.
treefmt/cli/format_test.go
2024-05-02 08:58:02 +01:00

593 lines
14 KiB
Go

package cli
import (
"bufio"
"os"
"os/exec"
"path"
"path/filepath"
"regexp"
"testing"
config2 "git.numtide.com/numtide/treefmt/config"
"git.numtide.com/numtide/treefmt/format"
"git.numtide.com/numtide/treefmt/test"
"github.com/go-git/go-billy/v5/osfs"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/cache"
"github.com/go-git/go-git/v5/storage/filesystem"
"github.com/stretchr/testify/require"
)
func TestAllowMissingFormatter(t *testing.T) {
as := require.New(t)
tempDir := test.TempExamples(t)
configPath := tempDir + "/treefmt.toml"
test.WriteConfig(t, configPath, config2.Config{
Formatters: map[string]*config2.Formatter{
"foo-fmt": {
Command: "foo-fmt",
},
},
})
_, err := cmd(t, "--config-file", configPath, "--tree-root", tempDir)
as.ErrorIs(err, format.ErrCommandNotFound)
_, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir, "--allow-missing-formatter")
as.NoError(err)
}
func TestSpecifyingFormatters(t *testing.T) {
as := require.New(t)
tempDir := test.TempExamples(t)
configPath := tempDir + "/treefmt.toml"
test.WriteConfig(t, configPath, config2.Config{
Formatters: map[string]*config2.Formatter{
"elm": {
Command: "touch",
Includes: []string{"*.elm"},
},
"nix": {
Command: "touch",
Includes: []string{"*.nix"},
},
"ruby": {
Command: "touch",
Includes: []string{"*.rb"},
},
},
})
_, err := cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
as.NoError(err)
assertStats(t, as, 31, 31, 3, 3)
_, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--formatters", "elm,nix")
as.NoError(err)
assertStats(t, as, 31, 31, 2, 2)
_, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--formatters", "ruby,nix")
as.NoError(err)
assertStats(t, as, 31, 31, 2, 2)
_, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--formatters", "nix")
as.NoError(err)
assertStats(t, as, 31, 31, 1, 1)
// test bad names
_, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--formatters", "foo")
as.Errorf(err, "formatter not found in config: foo")
_, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--formatters", "bar,foo")
as.Errorf(err, "formatter not found in config: bar")
}
func TestIncludesAndExcludes(t *testing.T) {
as := require.New(t)
tempDir := test.TempExamples(t)
configPath := tempDir + "/touch.toml"
// test without any excludes
cfg := config2.Config{
Formatters: map[string]*config2.Formatter{
"echo": {
Command: "echo",
Includes: []string{"*"},
},
},
}
test.WriteConfig(t, configPath, cfg)
_, err := cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
as.NoError(err)
assertStats(t, as, 31, 31, 31, 0)
// globally exclude nix files
cfg.Global.Excludes = []string{"*.nix"}
test.WriteConfig(t, configPath, cfg)
_, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
as.NoError(err)
assertStats(t, as, 31, 31, 30, 0)
// add haskell files to the global exclude
cfg.Global.Excludes = []string{"*.nix", "*.hs"}
test.WriteConfig(t, configPath, cfg)
_, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
as.NoError(err)
assertStats(t, as, 31, 31, 24, 0)
echo := cfg.Formatters["echo"]
// remove python files from the echo formatter
echo.Excludes = []string{"*.py"}
test.WriteConfig(t, configPath, cfg)
_, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
as.NoError(err)
assertStats(t, as, 31, 31, 22, 0)
// remove go files from the echo formatter
echo.Excludes = []string{"*.py", "*.go"}
test.WriteConfig(t, configPath, cfg)
_, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
as.NoError(err)
assertStats(t, as, 31, 31, 21, 0)
// adjust the includes for echo to only include elm files
echo.Includes = []string{"*.elm"}
test.WriteConfig(t, configPath, cfg)
_, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
as.NoError(err)
assertStats(t, as, 31, 31, 1, 0)
// add js files to echo formatter
echo.Includes = []string{"*.elm", "*.js"}
test.WriteConfig(t, configPath, cfg)
_, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
as.NoError(err)
assertStats(t, as, 31, 31, 2, 0)
}
func TestCache(t *testing.T) {
as := require.New(t)
tempDir := test.TempExamples(t)
configPath := tempDir + "/touch.toml"
// test without any excludes
cfg := config2.Config{
Formatters: map[string]*config2.Formatter{
"echo": {
Command: "echo",
Includes: []string{"*"},
},
},
}
test.WriteConfig(t, configPath, cfg)
out, err := cmd(t, "--config-file", configPath, "--tree-root", tempDir)
as.NoError(err)
assertStats(t, as, 31, 31, 31, 0)
out, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir)
as.NoError(err)
assertFormatted(t, as, out, 0)
// clear cache
out, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir, "-c")
as.NoError(err)
assertStats(t, as, 31, 31, 31, 0)
out, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir)
as.NoError(err)
assertFormatted(t, as, out, 0)
// clear cache
out, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir, "-c")
as.NoError(err)
assertStats(t, as, 31, 31, 31, 0)
out, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir)
as.NoError(err)
assertFormatted(t, as, out, 0)
// no cache
out, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir, "--no-cache")
as.NoError(err)
assertStats(t, as, 31, 31, 31, 0)
}
func TestChangeWorkingDirectory(t *testing.T) {
as := require.New(t)
// capture current cwd, so we can replace it after the test is finished
cwd, err := os.Getwd()
as.NoError(err)
t.Cleanup(func() {
// return to the previous working directory
as.NoError(os.Chdir(cwd))
})
tempDir := test.TempExamples(t)
configPath := tempDir + "/treefmt.toml"
// test without any excludes
cfg := config2.Config{
Formatters: map[string]*config2.Formatter{
"echo": {
Command: "echo",
Includes: []string{"*"},
},
},
}
test.WriteConfig(t, configPath, cfg)
// by default, we look for ./treefmt.toml and use the cwd for the tree root
// this should fail if the working directory hasn't been changed first
_, err = cmd(t, "-C", tempDir)
as.NoError(err)
assertStats(t, as, 31, 31, 31, 0)
}
func TestFailOnChange(t *testing.T) {
as := require.New(t)
tempDir := test.TempExamples(t)
configPath := tempDir + "/touch.toml"
// test without any excludes
cfg := config2.Config{
Formatters: map[string]*config2.Formatter{
"touch": {
Command: "touch",
Includes: []string{"*"},
},
},
}
test.WriteConfig(t, configPath, cfg)
_, err := cmd(t, "--fail-on-change", "--config-file", configPath, "--tree-root", tempDir)
as.ErrorIs(err, ErrFailOnChange)
}
func TestBustCacheOnFormatterChange(t *testing.T) {
as := require.New(t)
tempDir := test.TempExamples(t)
configPath := tempDir + "/touch.toml"
// symlink some formatters into temp dir, so we can mess with their mod times
binPath := tempDir + "/bin"
as.NoError(os.Mkdir(binPath, 0o755))
binaries := []string{"black", "elm-format", "gofmt"}
for _, name := range binaries {
src, err := exec.LookPath(name)
as.NoError(err)
as.NoError(os.Symlink(src, binPath+"/"+name))
}
// prepend our test bin directory to PATH
as.NoError(os.Setenv("PATH", binPath+":"+os.Getenv("PATH")))
// start with 2 formatters
cfg := config2.Config{
Formatters: map[string]*config2.Formatter{
"python": {
Command: "black",
Includes: []string{"*.py"},
},
"elm": {
Command: "elm-format",
Options: []string{"--yes"},
Includes: []string{"*.elm"},
},
},
}
test.WriteConfig(t, configPath, cfg)
args := []string{"--config-file", configPath, "--tree-root", tempDir}
_, err := cmd(t, args...)
as.NoError(err)
assertStats(t, as, 31, 31, 3, 0)
// tweak mod time of elm formatter
as.NoError(test.RecreateSymlink(t, binPath+"/"+"elm-format"))
_, err = cmd(t, args...)
as.NoError(err)
assertStats(t, as, 31, 31, 3, 0)
// check cache is working
_, err = cmd(t, args...)
as.NoError(err)
assertStats(t, as, 31, 0, 0, 0)
// tweak mod time of python formatter
as.NoError(test.RecreateSymlink(t, binPath+"/"+"black"))
_, err = cmd(t, args...)
as.NoError(err)
assertStats(t, as, 31, 31, 3, 0)
// check cache is working
_, err = cmd(t, args...)
as.NoError(err)
assertStats(t, as, 31, 0, 0, 0)
// add go formatter
cfg.Formatters["go"] = &config2.Formatter{
Command: "gofmt",
Options: []string{"-w"},
Includes: []string{"*.go"},
}
test.WriteConfig(t, configPath, cfg)
_, err = cmd(t, args...)
as.NoError(err)
assertStats(t, as, 31, 31, 4, 0)
// check cache is working
_, err = cmd(t, args...)
as.NoError(err)
assertStats(t, as, 31, 0, 0, 0)
// remove python formatter
delete(cfg.Formatters, "python")
test.WriteConfig(t, configPath, cfg)
_, err = cmd(t, args...)
as.NoError(err)
assertStats(t, as, 31, 31, 2, 0)
// check cache is working
_, err = cmd(t, args...)
as.NoError(err)
assertStats(t, as, 31, 0, 0, 0)
// remove elm formatter
delete(cfg.Formatters, "elm")
test.WriteConfig(t, configPath, cfg)
_, err = cmd(t, args...)
as.NoError(err)
assertStats(t, as, 31, 31, 1, 0)
// check cache is working
_, err = cmd(t, args...)
as.NoError(err)
assertStats(t, as, 31, 0, 0, 0)
}
func TestGitWorktree(t *testing.T) {
as := require.New(t)
tempDir := test.TempExamples(t)
configPath := filepath.Join(tempDir, "/treefmt.toml")
// basic config
cfg := config2.Config{
Formatters: map[string]*config2.Formatter{
"echo": {
Command: "echo",
Includes: []string{"*"},
},
},
}
test.WriteConfig(t, configPath, cfg)
// init a git repo
repo, err := git.Init(
filesystem.NewStorage(
osfs.New(path.Join(tempDir, ".git")),
cache.NewObjectLRUDefault(),
),
osfs.New(tempDir),
)
as.NoError(err, "failed to init git repository")
// get worktree
wt, err := repo.Worktree()
as.NoError(err, "failed to get git worktree")
run := func(traversed int, emitted int, matched int, formatted int) {
out, err := cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
as.NoError(err)
assertFormatted(t, as, out, formatted)
}
// run before adding anything to the worktree
run(0, 0, 0, 0)
// add everything to the worktree
as.NoError(wt.AddGlob("."))
as.NoError(err)
run(31, 31, 31, 0)
// remove python directory
as.NoError(wt.RemoveGlob("python/*"))
run(28, 28, 28, 0)
// walk with filesystem instead of git
_, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--walk", "filesystem")
as.NoError(err)
assertStats(t, as, 59, 59, 59, 0)
}
func TestPathsArg(t *testing.T) {
as := require.New(t)
// capture current cwd, so we can replace it after the test is finished
cwd, err := os.Getwd()
as.NoError(err)
t.Cleanup(func() {
// return to the previous working directory
as.NoError(os.Chdir(cwd))
})
tempDir := test.TempExamples(t)
configPath := filepath.Join(tempDir, "/treefmt.toml")
// change working directory to temp root
as.NoError(os.Chdir(tempDir))
// basic config
cfg := config2.Config{
Formatters: map[string]*config2.Formatter{
"echo": {
Command: "echo",
Includes: []string{"*"},
},
},
}
test.WriteConfig(t, configPath, cfg)
// without any path args
_, err = cmd(t, "-C", tempDir)
as.NoError(err)
assertStats(t, as, 31, 31, 31, 0)
// specify some explicit paths
_, err = cmd(t, "-C", tempDir, "-c", "elm/elm.json", "haskell/Nested/Foo.hs")
as.NoError(err)
assertStats(t, as, 4, 4, 4, 0)
// specify a bad path
_, err = cmd(t, "-C", tempDir, "-c", "elm/elm.json", "haskell/Nested/Bar.hs")
as.ErrorContains(err, "no such file or directory")
}
func TestStdIn(t *testing.T) {
as := require.New(t)
// capture current cwd, so we can replace it after the test is finished
cwd, err := os.Getwd()
as.NoError(err)
t.Cleanup(func() {
// return to the previous working directory
as.NoError(os.Chdir(cwd))
})
tempDir := test.TempExamples(t)
configPath := filepath.Join(tempDir, "/treefmt.toml")
// change working directory to temp root
as.NoError(os.Chdir(tempDir))
// basic config
cfg := config2.Config{
Formatters: map[string]*config2.Formatter{
"echo": {
Command: "echo",
Includes: []string{"*"},
},
},
}
test.WriteConfig(t, configPath, cfg)
// swap out stdin
prevStdIn := os.Stdin
stdin, err := os.CreateTemp("", "stdin")
as.NoError(err)
os.Stdin = stdin
t.Cleanup(func() {
os.Stdin = prevStdIn
_ = os.Remove(stdin.Name())
})
go func() {
_, err := stdin.WriteString(`treefmt.toml
elm/elm.json
go/main.go
`)
as.NoError(err, "failed to write to stdin")
as.NoError(stdin.Sync())
_, _ = stdin.Seek(0, 0)
}()
_, err = cmd(t, "-C", tempDir, "--stdin")
as.NoError(err)
assertStats(t, as, 6, 6, 6, 0)
}
func TestDeterministicOrderingInPipeline(t *testing.T) {
as := require.New(t)
tempDir := test.TempExamples(t)
configPath := tempDir + "/treefmt.toml"
test.WriteConfig(t, configPath, config2.Config{
Formatters: map[string]*config2.Formatter{
// a and b should execute in lexicographical order as they have default priority 0, with c last since it has
// priority 1
"fmt-a": {
Command: "test-fmt",
Options: []string{"fmt-a"},
Includes: []string{"*.py"},
Pipeline: "foo",
},
"fmt-b": {
Command: "test-fmt",
Options: []string{"fmt-b"},
Includes: []string{"*.py"},
Pipeline: "foo",
},
"fmt-c": {
Command: "test-fmt",
Options: []string{"fmt-c"},
Includes: []string{"*.py"},
Pipeline: "foo",
Priority: 1,
},
},
})
_, err := cmd(t, "-C", tempDir)
as.NoError(err)
matcher := regexp.MustCompile("^fmt-(.*)")
// check each affected file for the sequence of test statements which should be prepended to the end
sequence := []string{"fmt-a", "fmt-b", "fmt-c"}
paths := []string{"python/main.py", "python/virtualenv_proxy.py"}
for _, p := range paths {
file, err := os.Open(filepath.Join(tempDir, p))
as.NoError(err)
scanner := bufio.NewScanner(file)
idx := 0
for scanner.Scan() {
line := scanner.Text()
matches := matcher.FindAllString(line, -1)
if len(matches) != 1 {
continue
}
as.Equal(sequence[idx], matches[0])
idx += 1
}
}
}