feat: match before checking cache

This changes the flow of processing to make unmatched behaviour more consistent.

Before, we had been:

- traversing the filesystem
- comparing with the cache and only emitting files which had changed
- applying the matching rules to determine which formatters should be applied to a given file
- applying the formatters

Now, we do the following:

- traverse the filesystem
- apply the matching rules to determine which formatters should be applied to a given file
- compare with the cache and only emit files which have changed for formatting
- apply the formatters

It does mean we are applying the matching rules against files which we may not have to format, but in testing against Nixpkgs the performance impact appears negligible.

This makes sense since most of the processing time will be spent in the formatters, not applying some globs to file paths.

Signed-off-by: Brian McGee <brian@bmcgee.ie>
This commit is contained in:
Brian McGee 2024-10-13 13:47:36 +01:00
parent ed8979ed39
commit 71b262fd68
No known key found for this signature in database
GPG Key ID: D49016E76AD1E8C0
6 changed files with 506 additions and 103 deletions

View File

@ -205,33 +205,34 @@ func Run(v *viper.Viper, statz *stats.Stats, cmd *cobra.Command, paths []string)
return fmt.Errorf("failed to create walker: %w", err)
}
// start traversing
files := make([]*walk.File, BatchSize)
for {
ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
for {
// read the next batch
ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
n, err := reader.Read(ctx, files)
for idx := 0; idx < n; idx++ {
file := files[idx]
// check if this file is new or has changed when compared to the cache entry
if file.Cache == nil || file.Cache.HasChanged(file.Info) {
filesCh <- file
statz.Add(stats.Emitted, 1)
}
}
// ensure context is cancelled to release resources
cancel()
// pass each file into the file channel for processing
for idx := 0; idx < n; idx++ {
filesCh <- files[idx]
}
if errors.Is(err, io.EOF) {
// we have finished traversing
break
} else if err != nil {
// something went wrong
log.Errorf("failed to read files: %v", err)
cancel()
break
}
}
// indicate no further files for processing
close(filesCh)
// wait for everything to complete
@ -263,6 +264,8 @@ func applyFormatters(
// formatters which should be applied to their respective files
batches := make(map[string][]*format.Task)
// apply check if the given batch key has enough tasks to trigger processing
// flush is used to force processing regardless of the number of tasks
apply := func(key string, flush bool) {
// lookup the batch and exit early if it's empty
batch := batches[key]
@ -304,6 +307,7 @@ func applyFormatters(
}
}
// tryApply batches tasks by their batch key and processes the batch if there is enough ready
tryApply := func(task *format.Task) {
// append to batch
key := task.BatchKey
@ -314,53 +318,68 @@ func applyFormatters(
return func() error {
defer func() {
// close processed channel
// indicate processing has finished
close(formattedCh)
}()
// parse unmatched log level
unmatchedLevel, err := log.ParseLevel(cfg.OnUnmatched)
if err != nil {
return fmt.Errorf("invalid on-unmatched value: %w", err)
}
// iterate the files channel
// iterate the file channel
for file := range filesCh {
// a list of formatters that match this file
var matches []*format.Formatter
// first check if this file has been globally excluded
if format.PathMatches(file.RelPath, globalExcludes) {
log.Debugf("path matched global excludes: %s", file.RelPath)
// mark it as processed and continue to the next
formattedCh <- &format.Task{
File: file,
}
continue
}
// check if any formatters are interested in this file
var matches []*format.Formatter
for _, formatter := range formatters {
if formatter.Wants(file) {
matches = append(matches, formatter)
} else {
// otherwise, check if any formatters are interested in it
for _, formatter := range formatters {
if formatter.Wants(file) {
matches = append(matches, formatter)
}
}
}
// see if any formatters matched
// indicates no further processing
var release bool
// check if there were no matches
if len(matches) == 0 {
// log that there was no match, exiting with an error if the unmatched level was set to fatal
if unmatchedLevel == log.FatalLevel {
return fmt.Errorf("no formatter for path: %s", file.RelPath)
}
log.Logf(unmatchedLevel, "no formatter for path: %s", file.RelPath)
// mark it as processed and continue to the next
formattedCh <- &format.Task{
File: file,
}
// no further processing
release = true
} else {
// record the match
// record there was a match
statz.Add(stats.Matched, 1)
// create a new format task, add it to a batch based on its batch key and try to apply if the batch is full
task := format.NewTask(file, matches)
tryApply(&task)
// check if the file is new or has changed when compared to the cache entry
if file.Cache == nil || file.Cache.HasChanged(file.Info) {
// if so, generate a format task, add it to the relevant batch (by batch key) and try to process
task := format.NewTask(file, matches)
tryApply(&task)
} else {
// indicate no further processing
release = true
}
}
if release {
// release the file as there is no more processing to be done on it
if err := file.Release(); err != nil {
return fmt.Errorf("failed to release file: %w", err)
}
}
}
@ -398,16 +417,20 @@ func postProcessing(
break LOOP
}
// check if the file has changed
// grab the underlying file reference
file := task.File
// check if the file has changed
changed, newInfo, err := file.Stat()
if err != nil {
return err
}
statz.Add(stats.Formatted, 1)
if changed {
// record the change
statz.Add(stats.Formatted, 1)
// record that a change in the underlying file occurred
statz.Add(stats.Changed, 1)
logMethod := log.Debug
if cfg.FailOnChange {
@ -434,8 +457,8 @@ func postProcessing(
}
}
// if fail on change has been enabled, check that no files were actually formatted, throwing an error if so
if cfg.FailOnChange && statz.Value(stats.Formatted) != 0 {
// if fail on change has been enabled, check that no files were actually changed, throwing an error if so
if cfg.FailOnChange && statz.Value(stats.Changed) != 0 {
return ErrFailOnChange
}

View File

@ -72,24 +72,24 @@ func TestOnUnmatched(t *testing.T) {
var out []byte
// default is warn
out, _, err = treefmt(t, "-C", tempDir, "--allow-missing-formatter", "-c")
out, _, err = treefmt(t, "-C", tempDir, "--allow-missing-formatter")
as.NoError(err)
checkOutput("WARN", out)
out, _, err = treefmt(t, "-C", tempDir, "--allow-missing-formatter", "-c", "--on-unmatched", "warn")
out, _, err = treefmt(t, "-C", tempDir, "--allow-missing-formatter", "--on-unmatched", "warn")
as.NoError(err)
checkOutput("WARN", out)
out, _, err = treefmt(t, "-C", tempDir, "--allow-missing-formatter", "-c", "-u", "error")
out, _, err = treefmt(t, "-C", tempDir, "--allow-missing-formatter", "-u", "error")
as.NoError(err)
checkOutput("ERRO", out)
out, _, err = treefmt(t, "-C", tempDir, "--allow-missing-formatter", "-c", "-v", "--on-unmatched", "info")
out, _, err = treefmt(t, "-C", tempDir, "--allow-missing-formatter", "-v", "--on-unmatched", "info")
as.NoError(err)
checkOutput("INFO", out)
t.Setenv("TREEFMT_ON_UNMATCHED", "debug")
out, _, err = treefmt(t, "-C", tempDir, "--allow-missing-formatter", "-c", "-vv")
out, _, err = treefmt(t, "-C", tempDir, "--allow-missing-formatter", "-vv")
as.NoError(err)
checkOutput("DEBU", out)
}
@ -182,25 +182,53 @@ func TestSpecifyingFormatters(t *testing.T) {
setup()
_, statz, err := treefmt(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
as.NoError(err)
assertStats(t, as, statz, 32, 32, 3, 3)
assertStats(t, as, statz, map[stats.Type]int32{
stats.Traversed: 32,
stats.Matched: 3,
stats.Formatted: 3,
stats.Changed: 3,
})
setup()
_, statz, err = treefmt(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--formatters", "elm,nix")
as.NoError(err)
assertStats(t, as, statz, 32, 32, 2, 2)
assertStats(t, as, statz, map[stats.Type]int32{
stats.Traversed: 32,
stats.Matched: 2,
stats.Formatted: 2,
stats.Changed: 2,
})
setup()
_, statz, err = treefmt(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "-f", "ruby,nix")
as.NoError(err)
assertStats(t, as, statz, 32, 32, 2, 2)
assertStats(t, as, statz, map[stats.Type]int32{
stats.Traversed: 32,
stats.Matched: 2,
stats.Formatted: 2,
stats.Changed: 2,
})
setup()
_, statz, err = treefmt(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--formatters", "nix")
as.NoError(err)
assertStats(t, as, statz, 32, 32, 1, 1)
assertStats(t, as, statz, map[stats.Type]int32{
stats.Traversed: 32,
stats.Matched: 1,
stats.Formatted: 1,
stats.Changed: 1,
})
// test bad names
setup()
_, _, err = treefmt(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--formatters", "foo")
as.Errorf(err, "formatter not found in config: foo")
@ -228,7 +256,13 @@ func TestIncludesAndExcludes(t *testing.T) {
test.WriteConfig(t, configPath, cfg)
_, statz, err := treefmt(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
as.NoError(err)
assertStats(t, as, statz, 32, 32, 32, 0)
assertStats(t, as, statz, map[stats.Type]int32{
stats.Traversed: 32,
stats.Matched: 32,
stats.Formatted: 32,
stats.Changed: 0,
})
// globally exclude nix files
cfg.Excludes = []string{"*.nix"}
@ -236,7 +270,13 @@ func TestIncludesAndExcludes(t *testing.T) {
test.WriteConfig(t, configPath, cfg)
_, statz, err = treefmt(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
as.NoError(err)
assertStats(t, as, statz, 32, 32, 31, 0)
assertStats(t, as, statz, map[stats.Type]int32{
stats.Traversed: 32,
stats.Matched: 31,
stats.Formatted: 31,
stats.Changed: 0,
})
// add haskell files to the global exclude
cfg.Excludes = []string{"*.nix", "*.hs"}
@ -244,7 +284,13 @@ func TestIncludesAndExcludes(t *testing.T) {
test.WriteConfig(t, configPath, cfg)
_, statz, err = treefmt(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
as.NoError(err)
assertStats(t, as, statz, 32, 32, 25, 0)
assertStats(t, as, statz, map[stats.Type]int32{
stats.Traversed: 32,
stats.Matched: 25,
stats.Formatted: 25,
stats.Changed: 0,
})
echo := cfg.FormatterConfigs["echo"]
@ -254,7 +300,13 @@ func TestIncludesAndExcludes(t *testing.T) {
test.WriteConfig(t, configPath, cfg)
_, statz, err = treefmt(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
as.NoError(err)
assertStats(t, as, statz, 32, 32, 23, 0)
assertStats(t, as, statz, map[stats.Type]int32{
stats.Traversed: 32,
stats.Matched: 23,
stats.Formatted: 23,
stats.Changed: 0,
})
// remove go files from the echo formatter via env
t.Setenv("TREEFMT_FORMATTER_ECHO_EXCLUDES", "*.py,*.go")
@ -262,7 +314,13 @@ func TestIncludesAndExcludes(t *testing.T) {
test.WriteConfig(t, configPath, cfg)
_, statz, err = treefmt(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
as.NoError(err)
assertStats(t, as, statz, 32, 32, 22, 0)
assertStats(t, as, statz, map[stats.Type]int32{
stats.Traversed: 32,
stats.Matched: 22,
stats.Formatted: 22,
stats.Changed: 0,
})
t.Setenv("TREEFMT_FORMATTER_ECHO_EXCLUDES", "") // reset
@ -272,7 +330,13 @@ func TestIncludesAndExcludes(t *testing.T) {
test.WriteConfig(t, configPath, cfg)
_, statz, err = treefmt(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
as.NoError(err)
assertStats(t, as, statz, 32, 32, 1, 0)
assertStats(t, as, statz, map[stats.Type]int32{
stats.Traversed: 32,
stats.Matched: 1,
stats.Formatted: 1,
stats.Changed: 0,
})
// add js files to echo formatter via env
t.Setenv("TREEFMT_FORMATTER_ECHO_INCLUDES", "*.elm,*.js")
@ -280,7 +344,13 @@ func TestIncludesAndExcludes(t *testing.T) {
test.WriteConfig(t, configPath, cfg)
_, statz, err = treefmt(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
as.NoError(err)
assertStats(t, as, statz, 32, 32, 2, 0)
assertStats(t, as, statz, map[stats.Type]int32{
stats.Traversed: 32,
stats.Matched: 2,
stats.Formatted: 2,
stats.Changed: 0,
})
}
func TestPrjRootEnvVariable(t *testing.T) {
@ -303,7 +373,13 @@ func TestPrjRootEnvVariable(t *testing.T) {
t.Setenv("PRJ_ROOT", tempDir)
_, statz, err := treefmt(t, "--config-file", configPath)
as.NoError(err)
assertStats(t, as, statz, 32, 32, 32, 0)
assertStats(t, as, statz, map[stats.Type]int32{
stats.Traversed: 32,
stats.Matched: 32,
stats.Formatted: 32,
stats.Changed: 0,
})
}
func TestCache(t *testing.T) {
@ -327,34 +403,76 @@ func TestCache(t *testing.T) {
test.WriteConfig(t, configPath, cfg)
_, statz, err := treefmt(t, "--config-file", configPath, "--tree-root", tempDir)
as.NoError(err)
assertStats(t, as, statz, 32, 32, 32, 0)
assertStats(t, as, statz, map[stats.Type]int32{
stats.Traversed: 32,
stats.Matched: 32,
stats.Formatted: 32,
stats.Changed: 0,
})
_, statz, err = treefmt(t, "--config-file", configPath, "--tree-root", tempDir)
as.NoError(err)
assertStats(t, as, statz, 32, 0, 0, 0)
assertStats(t, as, statz, map[stats.Type]int32{
stats.Traversed: 32,
stats.Matched: 32,
stats.Formatted: 0,
stats.Changed: 0,
})
// clear cache
_, statz, err = treefmt(t, "--config-file", configPath, "--tree-root", tempDir, "-c")
as.NoError(err)
assertStats(t, as, statz, 32, 32, 32, 0)
assertStats(t, as, statz, map[stats.Type]int32{
stats.Traversed: 32,
stats.Matched: 32,
stats.Formatted: 32,
stats.Changed: 0,
})
_, statz, err = treefmt(t, "--config-file", configPath, "--tree-root", tempDir)
as.NoError(err)
assertStats(t, as, statz, 32, 0, 0, 0)
assertStats(t, as, statz, map[stats.Type]int32{
stats.Traversed: 32,
stats.Matched: 32,
stats.Formatted: 0,
stats.Changed: 0,
})
// clear cache
_, statz, err = treefmt(t, "--config-file", configPath, "--tree-root", tempDir, "-c")
as.NoError(err)
assertStats(t, as, statz, 32, 32, 32, 0)
assertStats(t, as, statz, map[stats.Type]int32{
stats.Traversed: 32,
stats.Matched: 32,
stats.Formatted: 32,
stats.Changed: 0,
})
_, statz, err = treefmt(t, "--config-file", configPath, "--tree-root", tempDir)
as.NoError(err)
assertStats(t, as, statz, 32, 0, 0, 0)
assertStats(t, as, statz, map[stats.Type]int32{
stats.Traversed: 32,
stats.Matched: 32,
stats.Formatted: 0,
stats.Changed: 0,
})
// no cache
_, statz, err = treefmt(t, "--config-file", configPath, "--tree-root", tempDir, "--no-cache")
as.NoError(err)
assertStats(t, as, statz, 32, 32, 32, 0)
assertStats(t, as, statz, map[stats.Type]int32{
stats.Traversed: 32,
stats.Matched: 32,
stats.Formatted: 32,
stats.Changed: 0,
})
}
func TestChangeWorkingDirectory(t *testing.T) {
@ -388,13 +506,25 @@ func TestChangeWorkingDirectory(t *testing.T) {
// this should fail if the working directory hasn't been changed first
_, statz, err := treefmt(t, "-C", tempDir)
as.NoError(err)
assertStats(t, as, statz, 32, 32, 32, 0)
assertStats(t, as, statz, map[stats.Type]int32{
stats.Traversed: 32,
stats.Matched: 32,
stats.Formatted: 32,
stats.Changed: 0,
})
// use env
t.Setenv("TREEFMT_WORKING_DIR", tempDir)
_, statz, err = treefmt(t, "-c")
as.NoError(err)
assertStats(t, as, statz, 32, 32, 32, 0)
assertStats(t, as, statz, map[stats.Type]int32{
stats.Traversed: 32,
stats.Matched: 32,
stats.Formatted: 32,
stats.Changed: 0,
})
}
func TestFailOnChange(t *testing.T) {
@ -467,31 +597,61 @@ func TestBustCacheOnFormatterChange(t *testing.T) {
args := []string{"--config-file", configPath, "--tree-root", tempDir}
_, statz, err := treefmt(t, args...)
as.NoError(err)
assertStats(t, as, statz, 32, 32, 3, 0)
assertStats(t, as, statz, map[stats.Type]int32{
stats.Traversed: 32,
stats.Matched: 3,
stats.Formatted: 3,
stats.Changed: 0,
})
// tweak mod time of elm formatter
as.NoError(test.RecreateSymlink(t, binPath+"/"+"elm-format"))
_, statz, err = treefmt(t, args...)
as.NoError(err)
assertStats(t, as, statz, 32, 32, 3, 0)
assertStats(t, as, statz, map[stats.Type]int32{
stats.Traversed: 32,
stats.Matched: 3,
stats.Formatted: 3,
stats.Changed: 0,
})
// check cache is working
_, statz, err = treefmt(t, args...)
as.NoError(err)
assertStats(t, as, statz, 32, 0, 0, 0)
assertStats(t, as, statz, map[stats.Type]int32{
stats.Traversed: 32,
stats.Matched: 3,
stats.Formatted: 0,
stats.Changed: 0,
})
// tweak mod time of python formatter
as.NoError(test.RecreateSymlink(t, binPath+"/"+"black"))
_, statz, err = treefmt(t, args...)
as.NoError(err)
assertStats(t, as, statz, 32, 32, 3, 0)
assertStats(t, as, statz, map[stats.Type]int32{
stats.Traversed: 32,
stats.Matched: 3,
stats.Formatted: 3,
stats.Changed: 0,
})
// check cache is working
_, statz, err = treefmt(t, args...)
as.NoError(err)
assertStats(t, as, statz, 32, 0, 0, 0)
assertStats(t, as, statz, map[stats.Type]int32{
stats.Traversed: 32,
stats.Matched: 3,
stats.Formatted: 0,
stats.Changed: 0,
})
// add go formatter
cfg.FormatterConfigs["go"] = &config.Formatter{
@ -503,12 +663,24 @@ func TestBustCacheOnFormatterChange(t *testing.T) {
_, statz, err = treefmt(t, args...)
as.NoError(err)
assertStats(t, as, statz, 32, 32, 4, 0)
assertStats(t, as, statz, map[stats.Type]int32{
stats.Traversed: 32,
stats.Matched: 4,
stats.Formatted: 4,
stats.Changed: 0,
})
// check cache is working
_, statz, err = treefmt(t, args...)
as.NoError(err)
assertStats(t, as, statz, 32, 0, 0, 0)
assertStats(t, as, statz, map[stats.Type]int32{
stats.Traversed: 32,
stats.Matched: 4,
stats.Formatted: 0,
stats.Changed: 0,
})
// remove python formatter
delete(cfg.FormatterConfigs, "python")
@ -516,12 +688,24 @@ func TestBustCacheOnFormatterChange(t *testing.T) {
_, statz, err = treefmt(t, args...)
as.NoError(err)
assertStats(t, as, statz, 32, 32, 2, 0)
assertStats(t, as, statz, map[stats.Type]int32{
stats.Traversed: 32,
stats.Matched: 2,
stats.Formatted: 2,
stats.Changed: 0,
})
// check cache is working
_, statz, err = treefmt(t, args...)
as.NoError(err)
assertStats(t, as, statz, 32, 0, 0, 0)
assertStats(t, as, statz, map[stats.Type]int32{
stats.Traversed: 32,
stats.Matched: 2,
stats.Formatted: 0,
stats.Changed: 0,
})
// remove elm formatter
delete(cfg.FormatterConfigs, "elm")
@ -529,12 +713,24 @@ func TestBustCacheOnFormatterChange(t *testing.T) {
_, statz, err = treefmt(t, args...)
as.NoError(err)
assertStats(t, as, statz, 32, 32, 1, 0)
assertStats(t, as, statz, map[stats.Type]int32{
stats.Traversed: 32,
stats.Matched: 1,
stats.Formatted: 1,
stats.Changed: 0,
})
// check cache is working
_, statz, err = treefmt(t, args...)
as.NoError(err)
assertStats(t, as, statz, 32, 0, 0, 0)
assertStats(t, as, statz, map[stats.Type]int32{
stats.Traversed: 32,
stats.Matched: 1,
stats.Formatted: 0,
stats.Changed: 0,
})
}
func TestGitWorktree(t *testing.T) {
@ -569,10 +765,16 @@ func TestGitWorktree(t *testing.T) {
wt, err := repo.Worktree()
as.NoError(err, "failed to get git worktree")
run := func(traversed int32, emitted int32, matched int32, formatted int32) {
run := func(traversed int32, matched int32, formatted int32, changed int32) {
_, statz, err := treefmt(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
as.NoError(err)
assertStats(t, as, statz, traversed, emitted, matched, formatted)
assertStats(t, as, statz, map[stats.Type]int32{
stats.Traversed: traversed,
stats.Matched: matched,
stats.Formatted: formatted,
stats.Changed: changed,
})
}
// run before adding anything to the worktree
@ -592,9 +794,15 @@ func TestGitWorktree(t *testing.T) {
run(28, 28, 28, 0)
// walk with filesystem instead of git
// we should traverse more files since we will look in the .git folder
_, statz, err := treefmt(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--walk", "filesystem")
as.NoError(err)
assertStats(t, as, statz, 60, 60, 60, 0)
assertStats(t, as, statz, map[stats.Type]int32{
stats.Traversed: 60,
stats.Matched: 60,
stats.Changed: 0,
})
// capture current cwd, so we can replace it after the test is finished
cwd, err := os.Getwd()
@ -608,15 +816,30 @@ func TestGitWorktree(t *testing.T) {
// format specific sub paths
_, statz, err = treefmt(t, "-C", tempDir, "-c", "go", "-vv")
as.NoError(err)
assertStats(t, as, statz, 2, 2, 2, 0)
assertStats(t, as, statz, map[stats.Type]int32{
stats.Traversed: 2,
stats.Matched: 2,
stats.Changed: 0,
})
_, statz, err = treefmt(t, "-C", tempDir, "-c", "go", "haskell")
as.NoError(err)
assertStats(t, as, statz, 9, 9, 9, 0)
assertStats(t, as, statz, map[stats.Type]int32{
stats.Traversed: 9,
stats.Matched: 9,
stats.Changed: 0,
})
_, statz, err = treefmt(t, "-C", tempDir, "-c", "go", "haskell", "ruby")
as.NoError(err)
assertStats(t, as, statz, 10, 10, 10, 0)
assertStats(t, as, statz, map[stats.Type]int32{
stats.Traversed: 10,
stats.Matched: 10,
stats.Changed: 0,
})
// try with a bad path
_, _, err = treefmt(t, "-C", tempDir, "-c", "haskell", "foo")
@ -628,11 +851,21 @@ func TestGitWorktree(t *testing.T) {
_, statz, err = treefmt(t, "-C", tempDir, "-c", "haskell", "foo.txt")
as.NoError(err)
assertStats(t, as, statz, 8, 8, 8, 0)
assertStats(t, as, statz, map[stats.Type]int32{
stats.Traversed: 8,
stats.Matched: 8,
stats.Changed: 0,
})
_, statz, err = treefmt(t, "-C", tempDir, "-c", "foo.txt")
as.NoError(err)
assertStats(t, as, statz, 1, 1, 1, 0)
assertStats(t, as, statz, map[stats.Type]int32{
stats.Traversed: 1,
stats.Matched: 1,
stats.Changed: 0,
})
}
func TestPathsArg(t *testing.T) {
@ -677,19 +910,38 @@ func TestPathsArg(t *testing.T) {
// without any path args
_, statz, err := treefmt(t)
as.NoError(err)
assertStats(t, as, statz, 32, 32, 32, 0)
assertStats(t, as, statz, map[stats.Type]int32{
stats.Traversed: 32,
stats.Matched: 32,
stats.Formatted: 32,
stats.Changed: 0,
})
// specify some explicit paths
_, statz, err = treefmt(t, "-c", "elm/elm.json", "haskell/Nested/Foo.hs")
as.NoError(err)
assertStats(t, as, statz, 2, 2, 2, 0)
assertStats(t, as, statz, map[stats.Type]int32{
stats.Traversed: 2,
stats.Matched: 2,
stats.Formatted: 2,
stats.Changed: 0,
})
// specify an absolute path
absoluteInternalPath, err := filepath.Abs("elm/elm.json")
as.NoError(err)
_, statz, err = treefmt(t, "-c", absoluteInternalPath)
as.NoError(err)
assertStats(t, as, statz, 1, 1, 1, 0)
assertStats(t, as, statz, map[stats.Type]int32{
stats.Traversed: 1,
stats.Matched: 1,
stats.Formatted: 1,
stats.Changed: 0,
})
// specify a bad path
_, _, err = treefmt(t, "-c", "elm/elm.json", "haskell/Nested/Bar.hs")
@ -742,7 +994,13 @@ func TestStdin(t *testing.T) {
out, statz, err := treefmt(t, "-C", tempDir, "--allow-missing-formatter", "--stdin", "test.nix")
as.NoError(err)
assertStats(t, as, statz, 1, 1, 1, 1)
assertStats(t, as, statz, map[stats.Type]int32{
stats.Traversed: 1,
stats.Matched: 1,
stats.Formatted: 1,
stats.Changed: 1,
})
// the nix formatters should have reduced the example to the following
as.Equal(`{ ...}: "hello"
@ -767,7 +1025,13 @@ func TestStdin(t *testing.T) {
out, statz, err = treefmt(t, "-C", tempDir, "--allow-missing-formatter", "--stdin", "test.md")
as.NoError(err)
assertStats(t, as, statz, 1, 1, 1, 1)
assertStats(t, as, statz, map[stats.Type]int32{
stats.Traversed: 1,
stats.Matched: 1,
stats.Formatted: 1,
stats.Changed: 1,
})
as.Equal(`| col1 | col2 |
| ------ | --------- |
@ -881,7 +1145,13 @@ func TestRunInSubdir(t *testing.T) {
// without any path args, should reformat the whole tree
_, statz, err := treefmt(t)
as.NoError(err)
assertStats(t, as, statz, 32, 32, 32, 0)
assertStats(t, as, statz, map[stats.Type]int32{
stats.Traversed: 32,
stats.Matched: 32,
stats.Formatted: 32,
stats.Changed: 0,
})
// specify some explicit paths, relative to the tree root
// this should not work, as we're in a subdirectory
@ -891,7 +1161,13 @@ func TestRunInSubdir(t *testing.T) {
// specify some explicit paths, relative to the current directory
_, statz, err = treefmt(t, "-c", "elm.json", "../haskell/Nested/Foo.hs")
as.NoError(err)
assertStats(t, as, statz, 2, 2, 2, 0)
assertStats(t, as, statz, map[stats.Type]int32{
stats.Traversed: 2,
stats.Matched: 2,
stats.Formatted: 2,
stats.Changed: 0,
})
}
func treefmt(t *testing.T, args ...string) ([]byte, *stats.Stats, error) {
@ -945,10 +1221,15 @@ func treefmt(t *testing.T, args ...string) ([]byte, *stats.Stats, error) {
return out, statz, nil
}
func assertStats(t *testing.T, as *require.Assertions, statz *stats.Stats, traversed int32, emitted int32, matched int32, formatted int32) {
func assertStats(
t *testing.T,
as *require.Assertions,
statz *stats.Stats,
expected map[stats.Type]int32,
) {
t.Helper()
as.Equal(traversed, statz.Value(stats.Traversed), "stats.traversed")
as.Equal(emitted, statz.Value(stats.Emitted), "stats.emitted")
as.Equal(matched, statz.Value(stats.Matched), "stats.matched")
as.Equal(formatted, statz.Value(stats.Formatted), "stats.formatted")
for k, v := range expected {
as.Equal(v, statz.Value(k), k.String())
}
}

View File

@ -7,13 +7,14 @@ import (
"time"
)
//go:generate enumer -type=Type -text -transform=snake -output=./stats_type.go
type Type int
const (
Traversed Type = iota
Emitted
Matched
Formatted
Changed
)
type Stats struct {
@ -44,9 +45,9 @@ func (s *Stats) Print() {
fmt.Printf(
strings.Join(components, "\n"),
s.Value(Traversed),
s.Value(Emitted),
s.Value(Matched),
s.Value(Formatted),
s.Value(Changed),
s.Elapsed().Round(time.Millisecond),
)
}
@ -55,9 +56,9 @@ func New() Stats {
// init counters
counters := make(map[Type]*atomic.Int32)
counters[Traversed] = &atomic.Int32{}
counters[Emitted] = &atomic.Int32{}
counters[Matched] = &atomic.Int32{}
counters[Formatted] = &atomic.Int32{}
counters[Changed] = &atomic.Int32{}
return Stats{
start: time.Now(),

98
stats/stats_type.go Normal file
View File

@ -0,0 +1,98 @@
// Code generated by "enumer -type=Type -text -transform=snake -output=./stats_type.go"; DO NOT EDIT.
package stats
import (
"fmt"
"strings"
)
const _TypeName = "traversedmatchedformattedchanged"
var _TypeIndex = [...]uint8{0, 9, 16, 25, 32}
const _TypeLowerName = "traversedmatchedformattedchanged"
func (i Type) String() string {
if i < 0 || i >= Type(len(_TypeIndex)-1) {
return fmt.Sprintf("Type(%d)", i)
}
return _TypeName[_TypeIndex[i]:_TypeIndex[i+1]]
}
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
func _TypeNoOp() {
var x [1]struct{}
_ = x[Traversed-(0)]
_ = x[Matched-(1)]
_ = x[Formatted-(2)]
_ = x[Changed-(3)]
}
var _TypeValues = []Type{Traversed, Matched, Formatted, Changed}
var _TypeNameToValueMap = map[string]Type{
_TypeName[0:9]: Traversed,
_TypeLowerName[0:9]: Traversed,
_TypeName[9:16]: Matched,
_TypeLowerName[9:16]: Matched,
_TypeName[16:25]: Formatted,
_TypeLowerName[16:25]: Formatted,
_TypeName[25:32]: Changed,
_TypeLowerName[25:32]: Changed,
}
var _TypeNames = []string{
_TypeName[0:9],
_TypeName[9:16],
_TypeName[16:25],
_TypeName[25:32],
}
// TypeString retrieves an enum value from the enum constants string name.
// Throws an error if the param is not part of the enum.
func TypeString(s string) (Type, error) {
if val, ok := _TypeNameToValueMap[s]; ok {
return val, nil
}
if val, ok := _TypeNameToValueMap[strings.ToLower(s)]; ok {
return val, nil
}
return 0, fmt.Errorf("%s does not belong to Type values", s)
}
// TypeValues returns all values of the enum
func TypeValues() []Type {
return _TypeValues
}
// TypeStrings returns a slice of all String values of the enum
func TypeStrings() []string {
strs := make([]string, len(_TypeNames))
copy(strs, _TypeNames)
return strs
}
// IsAType returns "true" if the value is listed in the enum definition. "false" otherwise
func (i Type) IsAType() bool {
for _, v := range _TypeValues {
if i == v {
return true
}
}
return false
}
// MarshalText implements the encoding.TextMarshaler interface for Type
func (i Type) MarshalText() ([]byte, error) {
return []byte(i.String()), nil
}
// UnmarshalText implements the encoding.TextUnmarshaler interface for Type
func (i *Type) UnmarshalText(text []byte) error {
var err error
*i, err = TypeString(string(text))
return err
}

View File

@ -81,7 +81,7 @@ func TestFilesystemReader(t *testing.T) {
as.Equal(32, count)
as.Equal(int32(32), statz.Value(stats.Traversed))
as.Equal(int32(0), statz.Value(stats.Emitted))
as.Equal(int32(0), statz.Value(stats.Matched))
as.Equal(int32(0), statz.Value(stats.Formatted))
as.Equal(int32(0), statz.Value(stats.Changed))
}

View File

@ -62,7 +62,7 @@ func TestGitReader(t *testing.T) {
as.Equal(32, count)
as.Equal(int32(32), statz.Value(stats.Traversed))
as.Equal(int32(0), statz.Value(stats.Emitted))
as.Equal(int32(0), statz.Value(stats.Matched))
as.Equal(int32(0), statz.Value(stats.Formatted))
as.Equal(int32(0), statz.Value(stats.Changed))
}