mirror of
https://github.com/grafana/grafana.git
synced 2024-11-28 03:34:15 -06:00
135 lines
3.6 KiB
Go
135 lines
3.6 KiB
Go
|
package codegen
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"os"
|
||
|
"path/filepath"
|
||
|
"sort"
|
||
|
|
||
|
"github.com/google/go-cmp/cmp"
|
||
|
"github.com/hashicorp/go-multierror"
|
||
|
"golang.org/x/sync/errgroup"
|
||
|
)
|
||
|
|
||
|
// WriteDiffer is a pseudo-filesystem that supports batch-writing its contents
|
||
|
// to the real filesystem, or batch-comparing its contents to the real
|
||
|
// filesystem. Its intended use is for idiomatic `go generate`-style code
|
||
|
// generators, where it is expected that the results of codegen are committed to
|
||
|
// version control.
|
||
|
//
|
||
|
// In such cases, the normal behavior of a generator is to write files to disk,
|
||
|
// but in CI, that behavior should change to verify that what is already on disk
|
||
|
// is identical to the results of code generation. This allows CI to ensure that
|
||
|
// the results of code generation are always up to date. WriteDiffer supports
|
||
|
// these related behaviors through its Write() and Verify() methods, respectively.
|
||
|
//
|
||
|
// Note that the statelessness of WriteDiffer means that, if a particular input
|
||
|
// to the code generator goes away, it will not notice generated files left
|
||
|
// behind if their inputs are removed.
|
||
|
// TODO introduce a search/match system
|
||
|
type WriteDiffer map[string][]byte
|
||
|
|
||
|
func NewWriteDiffer() WriteDiffer {
|
||
|
return WriteDiffer(make(map[string][]byte))
|
||
|
}
|
||
|
|
||
|
type writeSlice []struct {
|
||
|
path string
|
||
|
contents []byte
|
||
|
}
|
||
|
|
||
|
// Verify checks the contents of each file against the filesystem. It emits an error
|
||
|
// if any of its contained files differ.
|
||
|
func (wd WriteDiffer) Verify() error {
|
||
|
var result error
|
||
|
|
||
|
for _, item := range wd.toSlice() {
|
||
|
if _, err := os.Stat(item.path); err != nil {
|
||
|
if errors.Is(err, os.ErrNotExist) {
|
||
|
result = multierror.Append(result, fmt.Errorf("%s: generated file should exist, but does not", item.path))
|
||
|
} else {
|
||
|
result = multierror.Append(result, fmt.Errorf("%s: could not stat generated file: %w", item.path, err))
|
||
|
}
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
f, err := os.Open(filepath.Clean(item.path))
|
||
|
if err != nil {
|
||
|
result = multierror.Append(result, fmt.Errorf("%s: %w", item.path, err))
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
ob, err := io.ReadAll(f)
|
||
|
if err != nil {
|
||
|
result = multierror.Append(result, fmt.Errorf("%s: %w", item.path, err))
|
||
|
continue
|
||
|
}
|
||
|
dstr := cmp.Diff(string(ob), string(item.contents))
|
||
|
if dstr != "" {
|
||
|
result = multierror.Append(result, fmt.Errorf("%s would have changed:\n\n%s", item.path, dstr))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return result
|
||
|
}
|
||
|
|
||
|
// Write writes all of the files to their indicated paths.
|
||
|
func (wd WriteDiffer) Write() error {
|
||
|
g, _ := errgroup.WithContext(context.TODO())
|
||
|
g.SetLimit(12)
|
||
|
|
||
|
for _, item := range wd.toSlice() {
|
||
|
it := item
|
||
|
g.Go(func() error {
|
||
|
err := os.MkdirAll(filepath.Dir(it.path), os.ModePerm)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("%s: failed to ensure parent directory exists: %w", it.path, err)
|
||
|
}
|
||
|
|
||
|
if err := os.WriteFile(it.path, it.contents, 0644); err != nil {
|
||
|
return fmt.Errorf("%s: error while writing file: %w", it.path, err)
|
||
|
}
|
||
|
return nil
|
||
|
})
|
||
|
}
|
||
|
|
||
|
return g.Wait()
|
||
|
}
|
||
|
|
||
|
func (wd WriteDiffer) toSlice() writeSlice {
|
||
|
sl := make(writeSlice, 0, len(wd))
|
||
|
type ws struct {
|
||
|
path string
|
||
|
contents []byte
|
||
|
}
|
||
|
|
||
|
for k, v := range wd {
|
||
|
sl = append(sl, ws{
|
||
|
path: k,
|
||
|
contents: v,
|
||
|
})
|
||
|
}
|
||
|
|
||
|
sort.Slice(sl, func(i, j int) bool {
|
||
|
return sl[i].path < sl[j].path
|
||
|
})
|
||
|
|
||
|
return sl
|
||
|
}
|
||
|
|
||
|
// Merge combines all the entries from the provided WriteDiffer into the callee
|
||
|
// WriteDiffer. Duplicate paths result in an error.
|
||
|
func (wd WriteDiffer) Merge(wd2 WriteDiffer) error {
|
||
|
for k, v := range wd2 {
|
||
|
if _, has := wd[k]; has {
|
||
|
return fmt.Errorf("path %s already exists in write differ", k)
|
||
|
}
|
||
|
wd[k] = v
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|