Skip to content

Commit 40b76b7

Browse files
committed
feat: ensure deterministic application of formatters
Signed-off-by: Brian McGee <[email protected]>
1 parent 710efbd commit 40b76b7

File tree

4 files changed

+89
-3
lines changed

4 files changed

+89
-3
lines changed

cli/format.go

+12-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"path/filepath"
1212
"runtime"
1313
"slices"
14+
"sort"
1415
"strings"
1516
"syscall"
1617
"time"
@@ -84,8 +85,18 @@ func (f *Format) Run() (err error) {
8485
}
8586
}
8687

88+
// sort the formatter names so that, as we construct pipelines, we add formatters in a determinstic fashion. This
89+
// ensures a deterministic order even when all priority values are the same e.g. 0
90+
91+
names := make([]string, 0, len(cfg.Formatters))
92+
for name := range cfg.Formatters {
93+
names = append(names, name)
94+
}
95+
sort.Strings(names)
96+
8797
// init formatters
88-
for name, formatterCfg := range cfg.Formatters {
98+
for _, name := range names {
99+
formatterCfg := cfg.Formatters[name]
89100
formatter, err := format.NewFormatter(name, Cli.TreeRoot, formatterCfg, globalExcludes)
90101
if errors.Is(err, format.ErrCommandNotFound) && Cli.AllowMissingFormatter {
91102
l.Debugf("formatter not found: %v", name)

cli/format_test.go

+63-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package cli
22

33
import (
4+
"bufio"
45
"fmt"
56
"os"
67
"os/exec"
78
"path"
89
"path/filepath"
10+
"regexp"
911
"testing"
1012

1113
config2 "git.numtide.com/numtide/treefmt/config"
@@ -23,7 +25,7 @@ import (
2325
func TestAllowMissingFormatter(t *testing.T) {
2426
as := require.New(t)
2527

26-
tempDir := t.TempDir()
28+
tempDir := test.TempExamples(t)
2729
configPath := tempDir + "/treefmt.toml"
2830

2931
test.WriteConfig(t, configPath, config2.Config{
@@ -529,3 +531,63 @@ go/main.go
529531
as.NoError(err)
530532
as.Contains(string(out), fmt.Sprintf("%d files changed", 3))
531533
}
534+
535+
func TestDeterministicOrderingInPipeline(t *testing.T) {
536+
as := require.New(t)
537+
538+
tempDir := test.TempExamples(t)
539+
configPath := tempDir + "/treefmt.toml"
540+
541+
test.WriteConfig(t, configPath, config2.Config{
542+
Formatters: map[string]*config2.Formatter{
543+
// a and b should execute in lexicographical order as they have default priority 0, with c last since it has
544+
// priority 1
545+
"fmt-a": {
546+
Command: "test-fmt",
547+
Options: []string{"fmt-a"},
548+
Includes: []string{"*.py"},
549+
Pipeline: "foo",
550+
},
551+
"fmt-b": {
552+
Command: "test-fmt",
553+
Options: []string{"fmt-b"},
554+
Includes: []string{"*.py"},
555+
Pipeline: "foo",
556+
},
557+
"fmt-c": {
558+
Command: "test-fmt",
559+
Options: []string{"fmt-c"},
560+
Includes: []string{"*.py"},
561+
Pipeline: "foo",
562+
Priority: 1,
563+
},
564+
},
565+
})
566+
567+
_, err := cmd(t, "-C", tempDir)
568+
as.NoError(err)
569+
570+
matcher := regexp.MustCompile("^fmt-(.*)")
571+
572+
// check each affected file for the sequence of test statements which should be prepended to the end
573+
sequence := []string{"fmt-a", "fmt-b", "fmt-c"}
574+
paths := []string{"python/main.py", "python/virtualenv_proxy.py"}
575+
576+
for _, p := range paths {
577+
file, err := os.Open(filepath.Join(tempDir, p))
578+
as.NoError(err)
579+
scanner := bufio.NewScanner(file)
580+
581+
idx := 0
582+
583+
for scanner.Scan() {
584+
line := scanner.Text()
585+
matches := matcher.FindAllString(line, -1)
586+
if len(matches) != 1 {
587+
continue
588+
}
589+
as.Equal(sequence[idx], matches[0])
590+
idx += 1
591+
}
592+
}
593+
}

format/formatter.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ func (f *Formatter) Apply(ctx context.Context, paths []string, filter bool) erro
9393
return nil
9494
}
9595

96-
// Wants is used to test if a Formatter wants path based on it's configured Includes and Excludes patterns.
96+
// Wants is used to test if a Formatter wants a path based on it's configured Includes and Excludes patterns.
9797
// Returns true if the Formatter should be applied to path, false otherwise.
9898
func (f *Formatter) Wants(path string) bool {
9999
match := !PathMatches(path, f.excludes) && PathMatches(path, f.includes)

nix/formatters.nix

+13
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,17 @@ with pkgs; [
1616
statix
1717
deadnix
1818
terraform
19+
# util for unit testing
20+
(pkgs.writeShellApplication {
21+
name = "test-fmt";
22+
text = ''
23+
VALUE="$1"
24+
shift
25+
26+
# append value to each file
27+
for FILE in "$@"; do
28+
echo "$VALUE" >> "$FILE"
29+
done
30+
'';
31+
})
1932
]

0 commit comments

Comments
 (0)