mirror of
https://github.com/numtide/treefmt.git
synced 2024-09-11 17:07:44 +03:00
For each path we determine the list of formatters that are interested in formatting it. From there, we sort the list of formatters first by priority (lower value, higher priority) and then by name (lexicographically). With this information we create a batch key which is based on the unique sequence of formatters. When enough paths with the same sequence is ready we apply them in order to each formatter. By default, with no special configuration, this model guarantees that a given path will only be processed by one formatter at a time. If a user wishes to influence the order in which formatters are applied they can use the priority field. Signed-off-by: Brian McGee <brian@bmcgee.ie>
625 lines
15 KiB
Go
625 lines
15 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 TestCpuProfile(t *testing.T) {
|
|
as := require.New(t)
|
|
tempDir := test.TempExamples(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))
|
|
})
|
|
|
|
_, err = cmd(t, "-C", tempDir, "--allow-missing-formatter", "--cpu-profile", "cpu.pprof")
|
|
as.NoError(err)
|
|
as.FileExists(filepath.Join(tempDir, "cpu.pprof"))
|
|
_, err = os.Stat(filepath.Join(tempDir, "cpu.pprof"))
|
|
as.NoError(err)
|
|
}
|
|
|
|
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)
|
|
|
|
cfg := config2.Config{
|
|
Formatters: map[string]*config2.Formatter{
|
|
"elm": {
|
|
Command: "touch",
|
|
Options: []string{"-m"},
|
|
Includes: []string{"*.elm"},
|
|
},
|
|
"nix": {
|
|
Command: "touch",
|
|
Options: []string{"-m"},
|
|
Includes: []string{"*.nix"},
|
|
},
|
|
"ruby": {
|
|
Command: "touch",
|
|
Options: []string{"-m"},
|
|
Includes: []string{"*.rb"},
|
|
},
|
|
},
|
|
}
|
|
|
|
var tempDir, configPath string
|
|
|
|
// we reset the temp dir between successive runs as it appears that touching the file and modifying the mtime can
|
|
// is not granular enough between assertions in quick succession
|
|
setup := func() {
|
|
tempDir = test.TempExamples(t)
|
|
configPath = tempDir + "/treefmt.toml"
|
|
test.WriteConfig(t, configPath, cfg)
|
|
}
|
|
|
|
setup()
|
|
_, err := cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
|
|
as.NoError(err)
|
|
assertStats(t, as, 31, 31, 3, 3)
|
|
|
|
setup()
|
|
_, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--formatters", "elm,nix")
|
|
as.NoError(err)
|
|
assertStats(t, as, 31, 31, 2, 2)
|
|
|
|
setup()
|
|
_, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "-f", "ruby,nix")
|
|
as.NoError(err)
|
|
assertStats(t, as, 31, 31, 2, 2)
|
|
|
|
setup()
|
|
_, 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
|
|
setup()
|
|
_, 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, 2, 2, 2, 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, 3, 3, 3, 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 have no priority set, which means they default to 0 and should execute first
|
|
// a and b should execute in lexicographical order
|
|
// c should execute first since it has a priority of 1
|
|
"fmt-a": {
|
|
Command: "test-fmt",
|
|
Options: []string{"fmt-a"},
|
|
Includes: []string{"*.py"},
|
|
},
|
|
"fmt-b": {
|
|
Command: "test-fmt",
|
|
Options: []string{"fmt-b"},
|
|
Includes: []string{"*.py"},
|
|
},
|
|
"fmt-c": {
|
|
Command: "test-fmt",
|
|
Options: []string{"fmt-c"},
|
|
Includes: []string{"*.py"},
|
|
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
|
|
}
|
|
}
|
|
}
|