diff --git a/hcl.go b/hcl.go index 8a0d885..897a04a 100644 --- a/hcl.go +++ b/hcl.go @@ -42,7 +42,7 @@ func (l HCLGenericSpecLoader) Load(s Source) ([]Specification, error) { return nil, errors.NewWithMessage( UnsupportedSourceErrorCode, fmt.Sprintf( - "invalid specification source \"%s\", unsupported format \"%s\"", + "invalid specification source %q, unsupported format %q", s.Location, s.Format, ), @@ -63,7 +63,7 @@ func (l HCLGenericSpecLoader) Load(s Source) ([]Specification, error) { return nil, errors.NewWithMessage( InvalidHCLErrorCode, fmt.Sprintf( - "invalid specification source \"%s\" at line %d:%d, block \"%s\" should contain a name", + "invalid specification source %q at line %d:%d, block %q should contain a name", s.Location, block.Range().Start.Line, block.Range().Start.Column, @@ -79,7 +79,7 @@ func (l HCLGenericSpecLoader) Load(s Source) ([]Specification, error) { err, InvalidHCLErrorCode, fmt.Sprintf( - "invalid specification source \"%s\" at line %d:%d for block \"%s\"", + "invalid specification source %q at line %d:%d for block %q", s.Location, block.Range().Start.Line, block.Range().Start.Column, @@ -190,7 +190,7 @@ func (l HCLSpecLoader) Load(s Source) ([]Specification, error) { return nil, errors.NewWithMessage( UnsupportedSourceErrorCode, fmt.Sprintf( - "invalid specification source \"%s\", unsupported format \"%s\"", + "invalid specification source %q, unsupported format %q", s.Location, s.Format, ), diff --git a/logging.go b/logging.go index f3f71a1..03c706e 100644 --- a/logging.go +++ b/logging.go @@ -29,6 +29,18 @@ type Logger interface { Success(msg string) } +type NoopLogger struct{} + +func (n NoopLogger) Trace(msg string) {} + +func (n NoopLogger) Info(msg string) {} + +func (n NoopLogger) Warning(msg string) {} + +func (n NoopLogger) Error(msg string) {} + +func (n NoopLogger) Success(msg string) {} + type DefaultLoggerConfig struct { DisableColors bool Writer io.Writer diff --git a/mkdiroutproc.go b/mkdiroutproc.go new file mode 100644 index 0000000..e7eb705 --- /dev/null +++ b/mkdiroutproc.go @@ -0,0 +1,56 @@ +package specter + +import ( + "fmt" + "github.com/morebec/go-errors/errors" + "os" +) + +type DirectoryOutput struct { + Path string + Mode os.FileMode +} + +type WriteDirectoryOutputsProcessorConfig struct { + // Add any configuration specific to directory processing if needed + UseRegistry bool +} + +type WriteDirectoryOutputsProcessor struct { + config WriteDirectoryOutputsProcessorConfig +} + +func NewWriteDirectoryOutputsProcessor(conf WriteDirectoryOutputsProcessorConfig) *WriteDirectoryOutputsProcessor { + return &WriteDirectoryOutputsProcessor{ + config: conf, + } +} + +func (p WriteDirectoryOutputsProcessor) Name() string { + return "directory_outputs_processor" +} + +func (p WriteDirectoryOutputsProcessor) Process(ctx OutputProcessingContext) error { + ctx.Logger.Info("Creating output directories ...") + + var directories []DirectoryOutput + for _, o := range ctx.Outputs { + dir, ok := o.Value.(DirectoryOutput) + if !ok { + continue + } + directories = append(directories, dir) + } + + for _, dir := range directories { + ctx.Logger.Info(fmt.Sprintf("Creating directory %s ...", dir.Path)) + err := os.MkdirAll(dir.Path, dir.Mode) + if err != nil { + ctx.Logger.Error(fmt.Sprintf("failed creating directory at %s", dir.Path)) + return errors.Wrap(err, "failed creating output directories") + } + } + + ctx.Logger.Success("Output directories created successfully.") + return nil +} diff --git a/outputregistry.go b/outputregistry.go new file mode 100644 index 0000000..8058bba --- /dev/null +++ b/outputregistry.go @@ -0,0 +1,108 @@ +package specter + +import ( + "encoding/json" + "github.com/morebec/go-errors/errors" + "os" + "sync" + "time" +) + +// JSONOutputRegistry allows tracking on the file system the files that were written by the last execution +// of the WriteFileOutputsProcessor to perform cleaning operations on next executions. +type JSONOutputRegistry struct { + GeneratedAt time.Time `json:"generatedAt"` + OutputMap map[string]*JSONOutputRegistryProcessor `json:"files"` + FilePath string + mu sync.RWMutex // Mutex to protect concurrent access +} + +type JSONOutputRegistryProcessor struct { + Outputs []string `json:"files"` +} + +// NewJSONOutputRegistry returns a new output file registry. +func NewJSONOutputRegistry(fileName string) *JSONOutputRegistry { + return &JSONOutputRegistry{ + GeneratedAt: time.Now(), + OutputMap: nil, + FilePath: fileName, + } +} + +func (r *JSONOutputRegistry) Load() error { + r.mu.Lock() + defer r.mu.Unlock() + + bytes, err := os.ReadFile(r.FilePath) + + if err != nil { + if os.IsNotExist(err) { + return nil + } + return errors.WrapWithMessage(err, errors.InternalErrorCode, "failed loading output file registry") + } + if err := json.Unmarshal(bytes, r); err != nil { + return errors.WrapWithMessage(err, errors.InternalErrorCode, "failed loading output file registry") + } + + return nil +} + +func (r *JSONOutputRegistry) Save() error { + r.mu.RLock() + defer r.mu.RUnlock() + + if r.OutputMap == nil { + return nil + } + // Generate a JSON file containing all output files for clean up later on + js, err := json.MarshalIndent(r, "", " ") + if err != nil { + return errors.Wrap(err, "failed generating output file registry") + } + if err := os.WriteFile(r.FilePath, js, os.ModePerm); err != nil { + return errors.Wrap(err, "failed generating output file registry") + } + + return nil +} + +func (r *JSONOutputRegistry) AddOutput(processorName string, outputName string) { + r.mu.Lock() + defer r.mu.Unlock() + + if _, ok := r.OutputMap[processorName]; !ok { + r.OutputMap[processorName] = &JSONOutputRegistryProcessor{} + } + r.OutputMap[processorName].Outputs = append(r.OutputMap[processorName].Outputs, outputName) +} + +func (r *JSONOutputRegistry) RemoveOutput(processorName string, outputName string) { + r.mu.Lock() + defer r.mu.Unlock() + + if _, ok := r.OutputMap[processorName]; !ok { + return + } + + var files []string + for _, file := range r.OutputMap[processorName].Outputs { + if file != outputName { + files = append(files, file) + } + } + + r.OutputMap[processorName].Outputs = files +} + +func (r *JSONOutputRegistry) Outputs(processorName string) []string { + r.mu.RLock() + defer r.mu.RUnlock() + + outputs, ok := r.OutputMap[processorName] + if !ok { + return nil + } + return outputs.Outputs +} diff --git a/processing.go b/processing.go index e58058b..8829b6e 100644 --- a/processing.go +++ b/processing.go @@ -25,7 +25,8 @@ type ProcessingOutput struct { Value any } -// SpecificationProcessor are services responsible for performing work using Specifications. +// SpecificationProcessor are services responsible for performing work using Specifications +// and which can possibly generate outputs. type SpecificationProcessor interface { // Name returns the unique FilePath of this processor. Name() string @@ -34,10 +35,75 @@ type SpecificationProcessor interface { Process(ctx ProcessingContext) ([]ProcessingOutput, error) } +// OutputRegistry provides an interface for managing a registry of outputs. This +// registry tracks outputs generated during processing runs, enabling clean-up +// in subsequent runs to avoid residual artifacts and maintain a clean slate. +// +// Implementations of the OutputRegistry interface must be thread-safe to handle +// concurrent calls to TrackFile and UntrackFile methods. Multiple goroutines may +// access the registry simultaneously, so appropriate synchronization mechanisms +// should be implemented to prevent race conditions and ensure data integrity. +type OutputRegistry interface { + // Load the registry state from persistent storage. If an error occurs, it + // should be returned to indicate the failure of the loading operation. + Load() error + + // Save the current state of the registry to persistent storage. If an + // error occurs, it should be returned to indicate the failure of the saving operation. + Save() error + + // AddOutput registers an output name under a specific processor name. This method + // should ensure that the file path is associated with the given processor name + // in the registry. + AddOutput(processorName string, outputName string) + + // RemoveOutput removes a given named output registration for a specific processor name. This + // method should ensure that the file path is disassociated from the given + // processor name in the registry. + RemoveOutput(processorName string, outputName string) + + // Outputs returns the outputs for a given processor. + Outputs(processorName string) []string +} + +type NoopOutputRegistry struct { +} + +func (n NoopOutputRegistry) Load() error { + return nil +} + +func (n NoopOutputRegistry) Save() error { + return nil +} + +func (n NoopOutputRegistry) AddOutput(processorName string, outputName string) {} + +func (n NoopOutputRegistry) RemoveOutput(processorName string, outputName string) {} + +func (n NoopOutputRegistry) Outputs(processorName string) []string { + return nil +} + type OutputProcessingContext struct { Specifications SpecificationGroup Outputs []ProcessingOutput Logger Logger + + outputRegistry OutputRegistry + processorName string +} + +func (c *OutputProcessingContext) AddToRegistry(outputName string) { + c.outputRegistry.AddOutput(c.processorName, outputName) +} + +func (c *OutputProcessingContext) RemoveFromRegistry(outputName string) { + c.outputRegistry.RemoveOutput(c.processorName, outputName) +} + +func (c *OutputProcessingContext) RegistryOutputs() []string { + return c.outputRegistry.Outputs(c.processorName) } // OutputProcessor are services responsible for processing outputs of SpecProcessors. diff --git a/processing_test.go b/processing_test.go new file mode 100644 index 0000000..dbdff0c --- /dev/null +++ b/processing_test.go @@ -0,0 +1,93 @@ +package specter + +import ( + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "testing" +) + +// MockOutputRegistry is a mock implementation of OutputRegistry +type MockOutputRegistry struct { + mock.Mock +} + +func (m *MockOutputRegistry) Load() error { + args := m.Called() + return args.Error(0) +} + +func (m *MockOutputRegistry) Save() error { + args := m.Called() + return args.Error(0) +} + +func (m *MockOutputRegistry) AddOutput(processorName string, outputName string) { + m.Called(processorName, outputName) +} + +func (m *MockOutputRegistry) RemoveOutput(processorName string, outputName string) { + m.Called(processorName, outputName) +} + +func (m *MockOutputRegistry) Outputs(processorName string) []string { + args := m.Called(processorName) + return args.Get(0).([]string) +} + +func TestAddToRegistry(t *testing.T) { + // Arrange + mockRegistry := new(MockOutputRegistry) + ctx := &OutputProcessingContext{ + outputRegistry: mockRegistry, + processorName: "testProcessor", + } + + outputName := "outputFile.txt" + + mockRegistry.On("AddOutput", "testProcessor", outputName).Return() + + // Act + ctx.AddToRegistry(outputName) + + // Assert + mockRegistry.AssertExpectations(t) +} + +func TestRemoveFromRegistry(t *testing.T) { + // Arrange + mockRegistry := new(MockOutputRegistry) + ctx := &OutputProcessingContext{ + outputRegistry: mockRegistry, + processorName: "testProcessor", + } + + outputName := "outputFile.txt" + + mockRegistry.On("RemoveOutput", "testProcessor", outputName).Return() + + // Act + ctx.RemoveFromRegistry(outputName) + + // Assert + mockRegistry.AssertExpectations(t) +} + +func TestRegistryOutputs(t *testing.T) { + // Arrange + mockRegistry := new(MockOutputRegistry) + ctx := &OutputProcessingContext{ + outputRegistry: mockRegistry, + processorName: "testProcessor", + } + + expectedOutputs := []string{"file1.txt", "file2.txt"} + + mockRegistry.On("Outputs", "testProcessor").Return(expectedOutputs) + + // Act + outputs := ctx.RegistryOutputs() + + // Assert + assert.Equal(t, expectedOutputs, outputs) + mockRegistry.AssertExpectations(t) +} diff --git a/specter.go b/specter.go index 50a5ed9..d4e84ff 100644 --- a/specter.go +++ b/specter.go @@ -24,6 +24,7 @@ type Specter struct { Loaders []SpecificationLoader Processors []SpecificationProcessor OutputProcessors []OutputProcessor + OutputRegistry OutputRegistry Logger Logger ExecutionMode ExecutionMode } @@ -101,6 +102,7 @@ func (s Specter) Run(sourceLocations []string) (RunResult, error) { return run, nil } + // Process Output if err = s.ProcessOutputs(specifications, outputs); err != nil { e := errors.WrapWithMessage(err, errors.InternalErrorCode, "failed processing outputs") s.Logger.Error(e.Error()) @@ -118,7 +120,7 @@ func (s Specter) LoadSources(sourceLocations []string) ([]Source, error) { s.Logger.Info(fmt.Sprintf("\nLoading sources from (%d) locations:", len(sourceLocations))) for _, sl := range sourceLocations { - s.Logger.Info(fmt.Sprintf("-> \"%s\"", sl)) + s.Logger.Info(fmt.Sprintf("-> %q", sl)) } for _, sl := range sourceLocations { @@ -136,7 +138,7 @@ func (s Specter) LoadSources(sourceLocations []string) ([]Source, error) { } } if !loaded { - s.Logger.Warning(fmt.Sprintf("source location \"%s\" was not loaded.", sl)) + s.Logger.Warning(fmt.Sprintf("source location %q was not loaded.", sl)) } } @@ -177,7 +179,7 @@ func (s Specter) LoadSpecifications(sources []Source) ([]Specification, error) { if len(sourcesNotLoaded) > 0 { for _, src := range sourcesNotLoaded { - s.Logger.Warning(fmt.Sprintf("%s could not be loaded.", src)) + s.Logger.Warning(fmt.Sprintf("%q could not be loaded.", src)) } s.Logger.Warning("%d specifications were not loaded.") @@ -190,6 +192,10 @@ func (s Specter) LoadSpecifications(sources []Source) ([]Specification, error) { // ProcessSpecifications sends the specifications to processors. func (s Specter) ProcessSpecifications(specs []Specification) ([]ProcessingOutput, error) { + if s.Logger == nil { + s.Logger = NoopLogger{} + } + ctx := ProcessingContext{ Specifications: specs, Outputs: nil, @@ -200,7 +206,7 @@ func (s Specter) ProcessSpecifications(specs []Specification) ([]ProcessingOutpu for _, p := range s.Processors { outputs, err := p.Process(ctx) if err != nil { - return nil, errors.WrapWithMessage(err, errors.InternalErrorCode, fmt.Sprintf("processor \"%s\" failed", p.Name())) + return nil, errors.WrapWithMessage(err, errors.InternalErrorCode, fmt.Sprintf("processor %q failed", p.Name())) } ctx.Outputs = append(ctx.Outputs, outputs...) } @@ -216,20 +222,33 @@ func (s Specter) ProcessSpecifications(specs []Specification) ([]ProcessingOutpu // ProcessOutputs sends a list of ProcessingOutputs to the registered OutputProcessors. func (s Specter) ProcessOutputs(specifications []Specification, outputs []ProcessingOutput) error { + if s.OutputRegistry == nil { + s.OutputRegistry = NoopOutputRegistry{} + } + ctx := OutputProcessingContext{ Specifications: specifications, Outputs: outputs, Logger: s.Logger, + outputRegistry: s.OutputRegistry, } s.Logger.Info("\nProcessing outputs ...") + if err := s.OutputRegistry.Load(); err != nil { + return fmt.Errorf("failed loading output registry: %w", err) + } + for _, p := range s.OutputProcessors { err := p.Process(ctx) if err != nil { - return errors.WrapWithMessage(err, errors.InternalErrorCode, fmt.Sprintf("output processor \"%s\" failed", p.Name())) + return errors.WrapWithMessage(err, errors.InternalErrorCode, fmt.Sprintf("output processor %q failed", p.Name())) } } + if err := s.OutputRegistry.Save(); err != nil { + return fmt.Errorf("failed saving output registry: %w", err) + } + s.Logger.Success("Outputs processed successfully.") return nil } @@ -290,3 +309,14 @@ func WithExecutionMode(m ExecutionMode) Option { s.ExecutionMode = m } } +func WithOutputRegistry(r OutputRegistry) Option { + return func(s *Specter) { + s.OutputRegistry = r + } +} + +// Defaults + +func WithDefaultLogger() Option { + return WithLogger(NewDefaultLogger(DefaultLoggerConfig{DisableColors: false, Writer: os.Stdout})) +} diff --git a/specter_test.go b/specter_test.go new file mode 100644 index 0000000..00122da --- /dev/null +++ b/specter_test.go @@ -0,0 +1,19 @@ +package specter + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestWithOutputRegistry(t *testing.T) { + s := &Specter{} + r := &JSONOutputRegistry{} + WithOutputRegistry(r)(s) + assert.Equal(t, r, s.OutputRegistry) +} + +func TestWithDefaultLogger(t *testing.T) { + s := &Specter{} + WithDefaultLogger()(s) + assert.IsType(t, &DefaultLogger{}, s.Logger) +} diff --git a/write_file_outputs_processor.go b/write_file_outputs_processor.go deleted file mode 100644 index 0e8e553..0000000 --- a/write_file_outputs_processor.go +++ /dev/null @@ -1,172 +0,0 @@ -package specter - -import ( - "encoding/json" - "fmt" - "github.com/morebec/go-errors/errors" - "io/ioutil" - "os" - "sync" - "time" -) - -const WriteFileOutputsProcessorErrorCode = "write_file_outputs_processor_error" - -// OutputFileRegistry allows tracking on the file system the files that were written by the last execution -// of the WriteFileOutputsProcessor to perform cleaning operations on next executions. -type OutputFileRegistry struct { - GeneratedAt time.Time `json:"generatedAt"` - Files []string `json:"files"` - FilePath string -} - -// NewOutputFileRegistry returns a new output file registry. -func NewOutputFileRegistry(fileName string) OutputFileRegistry { - return OutputFileRegistry{ - GeneratedAt: time.Now(), - Files: nil, - FilePath: fileName, - } -} - -// Load loads the registry. -func (r *OutputFileRegistry) Load() error { - bytes, err := os.ReadFile(r.FilePath) - - if err != nil { - if os.IsNotExist(err) { - return nil - } - return errors.WrapWithMessage(err, errors.InternalErrorCode, "failed loading output file registry") - } - if err := json.Unmarshal(bytes, r); err != nil { - return errors.WrapWithMessage(err, errors.InternalErrorCode, "failed loading output file registry") - } - - return nil -} - -// Write will write the registry file to disk. -func (r *OutputFileRegistry) Write() error { - if r.Files == nil { - return nil - } - // Generate a JSON file containing all output files for clean up later on - js, err := json.MarshalIndent(r, "", " ") - if err != nil { - return errors.Wrap(err, "failed generating output file registry") - } - if err := ioutil.WriteFile(r.FilePath, js, os.ModePerm); err != nil { - return errors.Wrap(err, "failed generating output file registry") - } - - return nil -} - -// Clean will delete all files listed in the registry. -func (r *OutputFileRegistry) Clean() error { - var wg sync.WaitGroup - for _, f := range r.Files { - wg.Add(1) - f := f - go func() { - defer wg.Done() - if err := os.Remove(f); err != nil { - if errors.Is(err, os.ErrNotExist) { - return - } - panic(errors.Wrap(err, "failed cleaning output registry files")) - } - }() - } - wg.Wait() - - return nil -} - -type WriteFileOutputsProcessorConfig struct { - // Indicates if a registry file should be used to clean up generated files when running the WriteFileOutputsProcessor. - UseRegistry bool -} - -// WriteFileOutputsProcessor is a processor responsible for writing ProcessingOutput referring to files. -// To perform its work this processor looks at the processing context for any FileOutput. -type WriteFileOutputsProcessor struct { - config WriteFileOutputsProcessorConfig -} - -func NewWriteFilesProcessor(conf WriteFileOutputsProcessorConfig) *WriteFileOutputsProcessor { - return &WriteFileOutputsProcessor{ - config: conf, - } -} - -func (f WriteFileOutputsProcessor) Name() string { - return "file_outputs_processor" -} - -// FileOutput is a data structure that can be used by a SpecificationProcessor to output files that can be written by tje WriteFileOutputsProcessor. -type FileOutput struct { - Path string - Data []byte - Mode os.FileMode -} - -func (f WriteFileOutputsProcessor) Process(ctx OutputProcessingContext) error { - ctx.Logger.Info("Writing output files ...") - - var files []FileOutput - for _, o := range ctx.Outputs { - fo, ok := o.Value.(FileOutput) - if !ok { - continue - } - files = append(files, fo) - } - - registry := NewOutputFileRegistry(".specter.json") - if f.config.UseRegistry { - if err := registry.Load(); err != nil { - ctx.Logger.Error(fmt.Sprintf("failed loading output registry at %s", registry.FilePath)) - return err - } - if err := registry.Clean(); err != nil { - ctx.Logger.Error(fmt.Sprintf("failed cleaning output registry at %s", registry.FilePath)) - return err - } - } - - errs := errors.NewGroup(WriteFileOutputsProcessorErrorCode) - var wg sync.WaitGroup - for _, file := range files { - wg.Add(1) - file := file - go func() { - defer wg.Done() - ctx.Logger.Info(fmt.Sprintf("Writing file %s ...", file.Path)) - err := os.WriteFile(file.Path, file.Data, os.ModePerm) - if err != nil { - ctx.Logger.Error(fmt.Sprintf("failed writing output file at %s", file.Path)) - errs = errs.Append(err) - } - registry.Files = append(registry.Files, file.Path) - }() - } - wg.Wait() - - if f.config.UseRegistry { - ctx.Logger.Trace(fmt.Sprintf("Writing output file registry to \"%s\" ...", registry.FilePath)) - if err := registry.Write(); err != nil { - return errors.Wrap(err, "failed writing output files") - } - ctx.Logger.Trace("Output file registry written successfully.") - - if errs.HasErrors() { - return errs - } - } - - ctx.Logger.Success("Output files written successfully.") - - return nil -} diff --git a/writefileoutproc.go b/writefileoutproc.go new file mode 100644 index 0000000..2ec5adb --- /dev/null +++ b/writefileoutproc.go @@ -0,0 +1,97 @@ +package specter + +import ( + "fmt" + "github.com/morebec/go-errors/errors" + "os" + "sync" +) + +const WriteFileOutputsProcessorErrorCode = "write_file_outputs_processor_error" + +type WriteFileOutputsProcessorConfig struct { +} + +// WriteFileOutputsProcessor is a processor responsible for writing ProcessingOutput referring to files. +// To perform its work this processor looks at the processing context for any FileOutput. +type WriteFileOutputsProcessor struct { + config WriteFileOutputsProcessorConfig +} + +func NewWriteFilesProcessor(conf WriteFileOutputsProcessorConfig) *WriteFileOutputsProcessor { + return &WriteFileOutputsProcessor{ + config: conf, + } +} + +func (f WriteFileOutputsProcessor) Name() string { + return "file_outputs_processor" +} + +// FileOutput is a data structure that can be used by a SpecificationProcessor to output files that can be written by tje WriteFileOutputsProcessor. +type FileOutput struct { + Path string + Data []byte + Mode os.FileMode +} + +func (f WriteFileOutputsProcessor) Process(ctx OutputProcessingContext) error { + ctx.Logger.Info("Writing output files ...") + + var files []FileOutput + for _, o := range ctx.Outputs { + fo, ok := o.Value.(FileOutput) + if !ok { + continue + } + files = append(files, fo) + } + + if err := f.cleanRegistry(ctx); err != nil { + ctx.Logger.Error("failed cleaning output registry") + return err + } + + errs := errors.NewGroup(WriteFileOutputsProcessorErrorCode) + + // Write files concurrently to speed up process. + var wg sync.WaitGroup + for _, file := range files { + wg.Add(1) + go func(ctx OutputProcessingContext, file FileOutput) { + defer wg.Done() + ctx.Logger.Info(fmt.Sprintf("Writing file %q ...", file.Path)) + err := os.WriteFile(file.Path, file.Data, os.ModePerm) + if err != nil { + ctx.Logger.Error(fmt.Sprintf("failed writing output file at %q", file.Path)) + errs = errs.Append(err) + } + ctx.AddToRegistry(file.Path) + }(ctx, file) + } + wg.Wait() + + ctx.Logger.Success("Output files written successfully.") + + return nil +} + +func (f WriteFileOutputsProcessor) cleanRegistry(ctx OutputProcessingContext) error { + var wg sync.WaitGroup + for _, o := range ctx.RegistryOutputs() { + wg.Add(1) + go func(ctx OutputProcessingContext, o string) { + defer wg.Done() + if err := os.Remove(o); err != nil { + if errors.Is(err, os.ErrNotExist) { + return + } + panic(errors.Wrap(err, "failed cleaning output registry files")) + } + ctx.RemoveFromRegistry(o) + }(ctx, o) + } + wg.Wait() + + return nil +}