Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit dbda676

Browse files
committedOct 14, 2024··
fix: do not cache formatting errors
This was a regression introduced when the new `walk.Reader` approach was applied. When a formatter errors out during processing, we should not update those files in the cache to ensure they are retried during later invocations. I've extended the `walk.ReleaseFunc` to accept an optional `error` which can be used to indicate the file participated in a batch that had errors when being formatted. This allows the hook implementation to decide what it should do. Closes #449 Signed-off-by: Brian McGee <[email protected]>
1 parent a14e1b3 commit dbda676

File tree

6 files changed

+115
-32
lines changed

6 files changed

+115
-32
lines changed
 

‎cmd/format/format.go

+16-2
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ func applyFormatters(
377377

378378
if release {
379379
// release the file as there is no more processing to be done on it
380-
if err := file.Release(); err != nil {
380+
if err := file.Release(nil); err != nil {
381381
return fmt.Errorf("failed to release file: %w", err)
382382
}
383383
}
@@ -420,6 +420,20 @@ func postProcessing(
420420
// grab the underlying file reference
421421
file := task.File
422422

423+
// check if there were any errors processing the file
424+
if len(task.Errors) > 0 {
425+
// release the file, passing the first task error
426+
// note: task errors are related to the batch in which a task was applied
427+
// this does not necessarily indicate this file had a problem being formatted, but this approach
428+
// serves our purpose for now of indicating some sort of error condition to the release hooks
429+
if err := file.Release(task.Errors[0]); err != nil {
430+
return fmt.Errorf("failed to release file: %w", err)
431+
}
432+
433+
// continue processing next task
434+
continue
435+
}
436+
423437
// check if the file has changed
424438
changed, newInfo, err := file.Stat()
425439
if err != nil {
@@ -451,7 +465,7 @@ func postProcessing(
451465
file.Info = newInfo
452466
}
453467

454-
if err := file.Release(); err != nil {
468+
if err := file.Release(nil); err != nil {
455469
return fmt.Errorf("failed to release file: %w", err)
456470
}
457471
}

‎cmd/root_test.go

+62-3
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,58 @@ func TestCache(t *testing.T) {
473473
stats.Formatted: 32,
474474
stats.Changed: 0,
475475
})
476+
477+
// test that formatting errors are not cached
478+
479+
// update the config with a failing formatter
480+
cfg = &config.Config{
481+
FormatterConfigs: map[string]*config.Formatter{
482+
// fails to execute
483+
"fail": {
484+
Command: "touch",
485+
Options: []string{"--bad-arg"},
486+
Includes: []string{"*.hs"},
487+
},
488+
},
489+
}
490+
test.WriteConfig(t, configPath, cfg)
491+
492+
// running should match but not format anything
493+
_, statz, err = treefmt(t, "--config-file", configPath, "--tree-root", tempDir)
494+
as.NoError(err)
495+
496+
assertStats(t, as, statz, map[stats.Type]int32{
497+
stats.Traversed: 32,
498+
stats.Matched: 6,
499+
stats.Formatted: 0,
500+
stats.Changed: 0,
501+
})
502+
503+
// running again should provide the same result
504+
_, statz, err = treefmt(t, "--config-file", configPath, "--tree-root", tempDir, "-vv")
505+
as.NoError(err)
506+
507+
assertStats(t, as, statz, map[stats.Type]int32{
508+
stats.Traversed: 32,
509+
stats.Matched: 6,
510+
stats.Formatted: 0,
511+
stats.Changed: 0,
512+
})
513+
514+
// let's fix the python config so it no longer fails
515+
cfg.FormatterConfigs["fail"].Options = nil
516+
test.WriteConfig(t, configPath, cfg)
517+
518+
// we should now format the haskell files
519+
_, statz, err = treefmt(t, "--config-file", configPath, "--tree-root", tempDir)
520+
as.NoError(err)
521+
522+
assertStats(t, as, statz, map[stats.Type]int32{
523+
stats.Traversed: 32,
524+
stats.Matched: 6,
525+
stats.Formatted: 6,
526+
stats.Changed: 6,
527+
})
476528
}
477529

478530
func TestChangeWorkingDirectory(t *testing.T) {
@@ -547,9 +599,6 @@ func TestFailOnChange(t *testing.T) {
547599
_, _, err := treefmt(t, "--fail-on-change", "--config-file", configPath, "--tree-root", tempDir)
548600
as.ErrorIs(err, format2.ErrFailOnChange)
549601

550-
// we have second precision mod time tracking
551-
time.Sleep(time.Second)
552-
553602
// test with no cache
554603
t.Setenv("TREEFMT_FAIL_ON_CHANGE", "true")
555604
test.WriteConfig(t, configPath, cfg)
@@ -1204,6 +1253,16 @@ func treefmt(t *testing.T, args ...string) ([]byte, *stats.Stats, error) {
12041253
root.SetOut(tempOut)
12051254
root.SetErr(tempOut)
12061255

1256+
// record the start time
1257+
start := time.Now()
1258+
1259+
defer func() {
1260+
// Wait until we tick over into the next second before continuing.
1261+
// This ensures we correctly detect changes within tests as treefmt compares modtime at second level precision.
1262+
waitUntil := start.Truncate(time.Second).Add(time.Second)
1263+
time.Sleep(time.Until(waitUntil))
1264+
}()
1265+
12071266
if err := root.Execute(); err != nil {
12081267
return nil, nil, err
12091268
}

‎walk/cached.go

+13-7
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ type CachedReader struct {
2424
delegate Reader
2525

2626
eg *errgroup.Group
27-
// releaseCh contains files which have been released after processing and can be updated in the cache.
28-
releaseCh chan *File
27+
28+
// updateCh contains files which have been released after processing and can should be updated in the cache.
29+
updateCh chan *File
2930
}
3031

3132
// process updates cached file entries by batching file updates and flushing them to the database periodically.
@@ -59,7 +60,7 @@ func (c *CachedReader) process() error {
5960
})
6061
}
6162

62-
for file := range c.releaseCh {
63+
for file := range c.updateCh {
6364
batch = append(batch, file)
6465
if len(batch) == c.batchSize {
6566
if err := flush(); err != nil {
@@ -95,8 +96,13 @@ func (c *CachedReader) Read(ctx context.Context, files []*File) (n int, err erro
9596
}
9697

9798
// set a release function which inserts this file into the release channel for updating
98-
file.AddReleaseFunc(func() error {
99-
c.releaseCh <- file
99+
file.AddReleaseFunc(func(formatErr error) error {
100+
// in the event of a formatting error, we do not want to update this file in the cache
101+
// this ensures later invocations will try and re-format this file
102+
if formatErr == nil {
103+
c.updateCh <- file
104+
}
105+
100106
return nil
101107
})
102108
}
@@ -116,7 +122,7 @@ func (c *CachedReader) Read(ctx context.Context, files []*File) (n int, err erro
116122
// Close waits for any processing to complete.
117123
func (c *CachedReader) Close() error {
118124
// close the release channel
119-
close(c.releaseCh)
125+
close(c.updateCh)
120126

121127
// wait for any pending releases to be processed
122128
return c.eg.Wait()
@@ -138,7 +144,7 @@ func NewCachedReader(db *bolt.DB, batchSize int, delegate Reader) (*CachedReader
138144
delegate: delegate,
139145
log: log.WithPrefix("walk[cache]"),
140146
eg: eg,
141-
releaseCh: make(chan *File, batchSize*runtime.NumCPU()),
147+
updateCh: make(chan *File, batchSize*runtime.NumCPU()),
142148
}
143149

144150
// start the processing loop

‎walk/cached_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ func TestCachedReader(t *testing.T) {
5050
changeCount++
5151
}
5252

53-
as.NoError(file.Release())
53+
as.NoError(file.Release(nil))
5454
}
5555

5656
cancel()

‎walk/stdin.go

+18-14
Original file line numberDiff line numberDiff line change
@@ -54,22 +54,26 @@ func (s StdinReader) Read(_ context.Context, files []*File) (n int, err error) {
5454
}
5555

5656
// dump the temp file to stdout and remove it once the file is finished being processed
57-
files[0].AddReleaseFunc(func() error {
58-
// open the temp file
59-
file, err := os.Open(file.Name())
60-
if err != nil {
61-
return fmt.Errorf("failed to open temp file %s: %w", file.Name(), err)
62-
}
63-
64-
// dump file into stdout
65-
if _, err = io.Copy(os.Stdout, file); err != nil {
66-
return fmt.Errorf("failed to copy %s to stdout: %w", file.Name(), err)
67-
}
68-
69-
if err = file.Close(); err != nil {
70-
return fmt.Errorf("failed to close temp file %s: %w", file.Name(), err)
57+
files[0].AddReleaseFunc(func(formatErr error) error {
58+
// if formatting was successful, we dump its contents into os.Stdout
59+
if formatErr == nil {
60+
// open the temp file
61+
file, err := os.Open(file.Name())
62+
if err != nil {
63+
return fmt.Errorf("failed to open temp file %s: %w", file.Name(), err)
64+
}
65+
66+
// dump file into stdout
67+
if _, err = io.Copy(os.Stdout, file); err != nil {
68+
return fmt.Errorf("failed to copy %s to stdout: %w", file.Name(), err)
69+
}
70+
71+
if err = file.Close(); err != nil {
72+
return fmt.Errorf("failed to close temp file %s: %w", file.Name(), err)
73+
}
7174
}
7275

76+
// clean up the temp file
7377
if err = os.Remove(file.Name()); err != nil {
7478
return fmt.Errorf("failed to remove temp file %s: %w", file.Name(), err)
7579
}

‎walk/walk.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const (
2727
BatchSize = 1024
2828
)
2929

30-
type ReleaseFunc func() error
30+
type ReleaseFunc func(formatErr error) error
3131

3232
// File represents a file object with its path, relative path, file info, and potential cache entry.
3333
type File struct {
@@ -41,11 +41,11 @@ type File struct {
4141
releaseFuncs []ReleaseFunc
4242
}
4343

44-
// Release invokes all registered release functions for the File.
45-
// If any release function returns an error, Release stops and returns that error.
46-
func (f *File) Release() error {
44+
// Release calls all registered release functions for the File and returns an error if any function fails.
45+
// Accepts formatErr, which indicates if an error occurred when formatting this file.
46+
func (f *File) Release(formatErr error) error {
4747
for _, fn := range f.releaseFuncs {
48-
if err := fn(); err != nil {
48+
if err := fn(formatErr); err != nil {
4949
return err
5050
}
5151
}

0 commit comments

Comments
 (0)
Please sign in to comment.