From 3b73393759048271a3055786c2ce0137395e633c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Garc=C3=ADa?= <58498506+negarciacamilo@users.noreply.github.com> Date: Wed, 25 Feb 2026 12:31:11 -0300 Subject: [PATCH 1/3] refactor(mockery): streamline config handling and execution flow --- internal/actions/mockery/mockery.go | 530 +++++++--------------------- 1 file changed, 135 insertions(+), 395 deletions(-) diff --git a/internal/actions/mockery/mockery.go b/internal/actions/mockery/mockery.go index 88d6e24..05a52be 100644 --- a/internal/actions/mockery/mockery.go +++ b/internal/actions/mockery/mockery.go @@ -4,12 +4,10 @@ import ( "context" "crypto/rand" "encoding/hex" - "errors" "fmt" "os" "path/filepath" "strings" - "sync" "time" "github.com/charmbracelet/huh/spinner" @@ -28,40 +26,14 @@ const ( tmpConfigSuffix = ".yml" ) -type ( - Mockery struct { - ctx context.Context - configFiles []string - jobsNum int - dry bool - gitMod bool - tmpFiles []string // Track for cleanup - mu sync.Mutex - } - - mockeryJob struct { - configFile string - tmpFile string - err error - duration time.Duration - } - - executionStats struct { - total int - succeeded int - failed int - duration time.Duration - } - - progressUpdate struct { - current int - total int - configFile string - success bool - err error - duration time.Duration - } -) +type Mockery struct { + ctx context.Context + configFiles []string + jobsNum int + dry bool + gitMod bool + tmpFiles []string +} func New(ctx context.Context, configFiles []string, jobsNum int, dry bool, gitMod bool) *Mockery { return &Mockery{ @@ -76,7 +48,7 @@ func New(ctx context.Context, configFiles []string, jobsNum int, dry bool, gitMo func (m *Mockery) Exec() error { log.Info("Running mockery...") - // Validate inputs + if err := m.validate(); err != nil { return err } @@ -87,7 +59,6 @@ func (m *Mockery) Exec() error { startTime := time.Now() - // Find and validate config files configFiles, err := m.resolveConfigFiles() if err != nil { return err @@ -99,53 +70,58 @@ func (m *Mockery) Exec() error { return nil } - // Load base config baseConfig, err := m.loadBaseConfig() if err != nil { return err } - // Create temporary config files - if errTmp := m.createTempConfigs(configFiles, baseConfig); errTmp != nil { - m.cleanup() // Clean up any temp files created before the error - return errTmp + // Merge all package configs into a single unified config and run mockery once. + // This avoids N separate mockery process startups and duplicate `go list` + type-checking + // costs across packages. + mergedConfig, err := m.mergeSingleConfig(configFiles, baseConfig) + if err != nil { + return err + } + + tmpFile, err := m.createSingleTempConfig(mergedConfig, len(configFiles)) + if err != nil { + return err } - // Ensure cleanup on exit defer m.cleanup() - // Execute mockery concurrently with the progress spinner - results := m.executeConcurrentWithProgress(configFiles) + execErr := m.runSingleInvocation(tmpFile, len(configFiles)) - // Check if context was canceled if m.ctx.Err() != nil { - stats := m.calculateStats(results, startTime) - m.displayCancellationSummary(stats, results) + log.Warnf("⚠ Operation cancelled by user after %.2fs", time.Since(startTime).Seconds()) return fmt.Errorf("operation cancelled: %w", m.ctx.Err()) } - // Calculate and display stats - stats := m.calculateStats(results, startTime) - m.displaySummary(stats, results) + duration := time.Since(startTime) - // Return error if any executions failed - if stats.failed > 0 { - return fmt.Errorf("mockery execution failed for %d package(s)", stats.failed) + if execErr != nil { + log.Errorf("✗ mockery failed for %d package(s) (%.2fs)", len(configFiles), duration.Seconds()) + log.Errorf(" %v", execErr) + log.Info("Tip: Check the error messages above for details on how to fix the configurations") + return execErr + } + + if m.dry { + log.Successf("✓ All %d package(s) validated successfully (%.2fs)", len(configFiles), duration.Seconds()) + log.Info("Dry run completed - no mockery commands were executed") + } else { + log.Successf("✓ All %d package(s) completed successfully (%.2fs)", len(configFiles), duration.Seconds()) } return nil } -// validate validates input parameters +// validate validates input parameters. func (m *Mockery) validate() error { if m.jobsNum <= 0 { return fmt.Errorf("invalid --jobs-num value: %d (must be greater than 0)", m.jobsNum) } - if m.jobsNum > 100 { - log.Warnf("Very high concurrency (%d) may cause performance issues", m.jobsNum) - } - if m.gitMod && len(m.configFiles) > 0 { return fmt.Errorf("cannot use --git-mod with explicit config file paths") } @@ -153,12 +129,11 @@ func (m *Mockery) validate() error { return nil } -// resolveConfigFiles resolves, validates, and deduplicates config files +// resolveConfigFiles resolves, validates, and deduplicates config files. func (m *Mockery) resolveConfigFiles() ([]string, error) { log.Info("Resolving config files...") var configFiles []string - // If git-mod is enabled, find configs from modified files if m.gitMod { found, err := m.findConfigsFromGitDiff() if err != nil { @@ -166,14 +141,12 @@ func (m *Mockery) resolveConfigFiles() ([]string, error) { } configFiles = found } else if len(m.configFiles) > 0 { - // If files provided, validate them validated, err := m.validateProvidedConfigs(m.configFiles) if err != nil { return nil, err } configFiles = validated } else { - // Search for config files found, err := m.findPackageConfigs() if err != nil { return nil, err @@ -181,23 +154,20 @@ func (m *Mockery) resolveConfigFiles() ([]string, error) { configFiles = found } - // Deduplicate configFiles = m.deduplicateFiles(configFiles) return configFiles, nil } -// validateProvidedConfigs validates user-provided config file paths +// validateProvidedConfigs validates user-provided config file paths. func (m *Mockery) validateProvidedConfigs(paths []string) ([]string, error) { var validated []string for _, path := range paths { - // Check if a file exists if !files.Exists(path) { return nil, fmt.Errorf("config file not found: %s", path) } - // Check if it's a file stat, err := os.Stat(path) if err != nil { return nil, fmt.Errorf("failed to stat %s: %w", path, err) @@ -207,7 +177,6 @@ func (m *Mockery) validateProvidedConfigs(paths []string) ([]string, error) { return nil, fmt.Errorf("path is a directory, not a file: %s", path) } - // Warn if it doesn't follow naming convention if !strings.HasSuffix(path, pkgConfigSuffix) { log.Warnf("Config file %s doesn't follow naming convention (.mockery.pkg.yml)", path) } @@ -218,13 +187,12 @@ func (m *Mockery) validateProvidedConfigs(paths []string) ([]string, error) { return validated, nil } -// deduplicateFiles removes duplicate file paths -func (m *Mockery) deduplicateFiles(files []string) []string { +// deduplicateFiles removes duplicate file paths. +func (m *Mockery) deduplicateFiles(fileList []string) []string { seen := make(map[string]bool) var result []string - for _, file := range files { - // Normalize a path + for _, file := range fileList { normalized, err := filepath.Abs(file) if err != nil { normalized = file @@ -236,14 +204,14 @@ func (m *Mockery) deduplicateFiles(files []string) []string { } } - if len(result) < len(files) { - log.Warnf("Removed %d duplicate config file(s)", len(files)-len(result)) + if len(result) < len(fileList) { + log.Warnf("Removed %d duplicate config file(s)", len(fileList)-len(result)) } return result } -// findPackageConfigs searches for all .mockery.pkg.yml files in the project +// findPackageConfigs searches for all .mockery.pkg.yml files in the project. func (m *Mockery) findPackageConfigs() ([]string, error) { var configs []string @@ -252,13 +220,10 @@ func (m *Mockery) findPackageConfigs() ([]string, error) { return err } - // Skip hidden directories (but not the root "." directory) if info.IsDir() { - // Don't skip the root directory if path == "." { return nil } - if strings.HasPrefix(info.Name(), ".") { return filepath.SkipDir } @@ -279,9 +244,8 @@ func (m *Mockery) findPackageConfigs() ([]string, error) { return configs, nil } -// findConfigsFromGitDiff finds .mockery.pkg.yml files in directories with modified files +// findConfigsFromGitDiff finds .mockery.pkg.yml files in directories with modified files. func (m *Mockery) findConfigsFromGitDiff() ([]string, error) { - // Get modified files comparing HEAD with the main branch modifiedFiles, err := m.getModifiedFiles() if err != nil { return nil, fmt.Errorf("failed to get modified files: %w", err) @@ -292,10 +256,7 @@ func (m *Mockery) findConfigsFromGitDiff() ([]string, error) { return nil, nil } - // Extract and deduplicate base directories directories := m.extractDirectories(modifiedFiles) - - // Search for .mockery.pkg.yml in each directory and its parents configs := m.findConfigsInDirectories(directories) if len(configs) == 0 { @@ -305,29 +266,24 @@ func (m *Mockery) findConfigsFromGitDiff() ([]string, error) { return configs, nil } -// getModifiedFiles returns a list of modified files using git diff +// getModifiedFiles returns a list of modified files using git diff. func (m *Mockery) getModifiedFiles() ([]string, error) { - // Try to get the main branch name mainBranch := "main" - // Check if origin/main exists checkCmd := "git rev-parse --verify origin/main" if _, err := exec.Command(checkCmd); err != nil { - // Try master as fallback checkCmd = "git rev-parse --verify origin/master" if _, err := exec.Command(checkCmd); err == nil { mainBranch = "master" } } - // Get modified and new files cmd := fmt.Sprintf("git diff --name-only --diff-filter=AM origin/%s...HEAD", mainBranch) output, err := exec.Command(cmd) if err != nil { return nil, fmt.Errorf("failed to run git diff: %w\nOutput: %s\nTip: Ensure you're in a git repository and origin/%s exists", err, output, mainBranch) } - // Parse output into a file list fileList := strings.Split(strings.TrimSpace(output), "\n") var result []string for _, file := range fileList { @@ -340,19 +296,17 @@ func (m *Mockery) getModifiedFiles() ([]string, error) { return result, nil } -// extractDirectories extracts unique directories from file paths -func (m *Mockery) extractDirectories(files []string) []string { +// extractDirectories extracts unique directories from file paths. +func (m *Mockery) extractDirectories(fileList []string) []string { dirMap := make(map[string]struct{}) - for _, file := range files { - // Get the directory of the file + for _, file := range fileList { dir := filepath.Dir(file) if dir != "" && dir != "." { dirMap[dir] = struct{}{} } } - // Convert map to slice var directories []string for dir := range dirMap { directories = append(directories, dir) @@ -361,17 +315,15 @@ func (m *Mockery) extractDirectories(files []string) []string { return directories } -// findConfigsInDirectories searches for .mockery.pkg.yml files in directories and their parents +// findConfigsInDirectories searches for .mockery.pkg.yml files in directories and their parents. func (m *Mockery) findConfigsInDirectories(directories []string) []string { configMap := make(map[string]struct{}) for _, dir := range directories { - // Check the current directory and walk up to find .mockery.pkg.yml currentDir := dir for { configPath := filepath.Join(currentDir, pkgConfigSuffix) if files.Exists(configPath) { - // Normalize a path normalized, err := filepath.Abs(configPath) if err != nil { normalized = configPath @@ -379,7 +331,6 @@ func (m *Mockery) findConfigsInDirectories(directories []string) []string { configMap[normalized] = struct{}{} } - // Move to the parent directory parent := filepath.Dir(currentDir) if parent == currentDir || parent == "." || parent == "/" { break @@ -388,7 +339,6 @@ func (m *Mockery) findConfigsInDirectories(directories []string) []string { } } - // Convert map to slice var configs []string for config := range configMap { configs = append(configs, config) @@ -397,7 +347,7 @@ func (m *Mockery) findConfigsInDirectories(directories []string) []string { return configs } -// loadBaseConfig loads the base configuration file +// loadBaseConfig loads the base configuration file. func (m *Mockery) loadBaseConfig() (map[string]any, error) { log.Info("Loading base configuration file...") if !files.Exists(baseConfigFile) { @@ -417,273 +367,134 @@ func (m *Mockery) loadBaseConfig() (map[string]any, error) { return config, nil } -// deepMerge performs a deep merge of two maps, with b taking precedence -func (m *Mockery) deepMerge(a, b map[string]any) map[string]any { - result := make(map[string]any) - - // Copy all from a - for k, v := range a { - result[k] = v - } - - // Merge from b - for k, v := range b { - if vMap, ok := v.(map[string]any); ok { - // If b[k] is a map, try to deep merge with a[k] - if aMap, ok := result[k].(map[string]any); ok { - result[k] = m.deepMerge(aMap, vMap) - continue - } - } - // Otherwise, b[k] overwrites result[k] - result[k] = v - } +// mergeSingleConfig merges all package configs into a single unified config. +// The base config provides all top-level settings (dir, filename, etc.) and each +// pkg config contributes only its packages: entries. This works correctly when dir +// uses mockery template variables like {{.InterfaceDir}}, which resolve per-interface +// at runtime regardless of where mockery is invoked from. +func (m *Mockery) mergeSingleConfig(configFiles []string, baseConfig map[string]any) (map[string]any, error) { + log.Infof("Merging %d package config(s) into single mockery invocation...", len(configFiles)) - return result -} + result := m.deepMerge(baseConfig, map[string]any{}) + allPackages := make(map[string]any) -// createTempConfigs creates temporary config files for each package -func (m *Mockery) createTempConfigs(configFiles []string, baseConfig map[string]any) error { - log.Info("Creating temporary config files...") - for i, configFile := range configFiles { + for _, configFile := range configFiles { var pkgConfig map[string]any - - if errLoad := files.LoadYAML(configFile, &pkgConfig); errLoad != nil { - return fmt.Errorf("failed to load %s: %w", configFile, errLoad) + if err := files.LoadYAML(configFile, &pkgConfig); err != nil { + return nil, fmt.Errorf("failed to load %s: %w", configFile, err) } - // Deep merge configs (package takes precedence) - merged := m.deepMerge(baseConfig, pkgConfig) - - // Generate temp file name with context - tmpFile, err := m.generateTempFileName(configFile, i) - if err != nil { - return fmt.Errorf("failed to generate temp file name: %w", err) + pkgs, ok := pkgConfig["packages"] + if !ok { + continue } - // Marshal merged config - mergedData, err := yaml.Marshal(merged) - if err != nil { - return fmt.Errorf("failed to marshal merged config for %s: %w", configFile, err) + pkgMap, ok := pkgs.(map[string]any) + if !ok { + continue } - // Write the temp file - if errCreate := files.Create(tmpFile, mergedData); errCreate != nil { - return fmt.Errorf("failed to write temp config %s: %w", tmpFile, errCreate) + for pkgPath, pkgDef := range pkgMap { + if existing, dup := allPackages[pkgPath]; dup { + // Same package in multiple configs: deep merge their definitions so + // interfaces from both are generated. + if existingMap, ok := existing.(map[string]any); ok { + if newMap, ok := pkgDef.(map[string]any); ok { + allPackages[pkgPath] = m.deepMerge(existingMap, newMap) + continue + } + } + log.Warnf("Duplicate package %q in %s, overwriting previous definition", pkgPath, configFile) + } + allPackages[pkgPath] = pkgDef } + } - // Track for cleanup - m.mu.Lock() - m.tmpFiles = append(m.tmpFiles, tmpFile) - m.mu.Unlock() + if len(allPackages) > 0 { + result["packages"] = allPackages } - return nil + return result, nil } -// generateTempFileName generates a descriptive temp file name -func (m *Mockery) generateTempFileName(configFile string, index int) (string, error) { +// createSingleTempConfig writes the merged config to a single temporary file. +func (m *Mockery) createSingleTempConfig(config map[string]any, pkgCount int) (string, error) { randID, err := generateRandomID() if err != nil { - return "", err + return "", fmt.Errorf("failed to generate temp file name: %w", err) + } + + tmpFile := fmt.Sprintf("%smerged.%s%s", tmpConfigPrefix, randID, tmpConfigSuffix) + + data, err := yaml.Marshal(config) + if err != nil { + return "", fmt.Errorf("failed to marshal merged config: %w", err) } - // Extract a meaningful name from the config file path - dir := filepath.Dir(configFile) - pkgName := filepath.Base(dir) - if pkgName == "." { - pkgName = "root" + if err := files.Create(tmpFile, data); err != nil { + return "", fmt.Errorf("failed to write temp config %s: %w", tmpFile, err) } - // Create a descriptive temp file name: .mockery.tmp.[pkg-name].[idx].[rand].yml - tmpFile := fmt.Sprintf("%s%s.%d.%s%s", tmpConfigPrefix, pkgName, index, randID, tmpConfigSuffix) + m.tmpFiles = append(m.tmpFiles, tmpFile) + + log.Debugf("Merged config written to %s (%d packages)", tmpFile, pkgCount) return tmpFile, nil } -// executeConcurrentWithProgress executes mockery commands concurrently with progress spinner -func (m *Mockery) executeConcurrentWithProgress(configFiles []string) []mockeryJob { - log.Info("Executing mockery commands...") - var ( - wg sync.WaitGroup - results = make([]mockeryJob, 0, len(configFiles)) - resultsChan = make(chan mockeryJob, len(configFiles)) - semaphore = make(chan struct{}, m.jobsNum) - progressChan = make(chan progressUpdate, len(configFiles)) - completed = 0 - execErr error - doneChan = make(chan struct{}) - ) +// runSingleInvocation runs mockery once with the merged config. +func (m *Mockery) runSingleInvocation(tmpFile string, pkgCount int) error { + var execErr error + doneChan := make(chan struct{}) - total := len(configFiles) - - spin := spinner.New().Title(fmt.Sprintf("[0 / %d] Preparing...", total)) + spin := spinner.New().Title(fmt.Sprintf("Running mockery for %d package(s)...", pkgCount)) action := func() { defer close(doneChan) - var progressWg sync.WaitGroup - var cancelled bool - progressWg.Add(1) - - // Start a progress reader goroutine before spawning work goroutines - go func() { - defer progressWg.Done() - // Process progress updates as they arrive - for update := range progressChan { - completed++ - status := "✓" - if !update.success { - status = "✗" - } - - // Extract a shorter name from the config file path - shortName := m.shortenConfigPath(update.configFile) - - spin.Title(fmt.Sprintf("[%s] [%2d / %d] %s (%.2fs)", - status, completed, total, shortName, update.duration.Seconds())) - } - }() - - // Start goroutines to process configs - for idx := range m.tmpFiles { - // Check if context is cancelled before starting new goroutine - if m.ctx.Err() != nil { - if !cancelled { - log.Warn("Operation cancelled by user, waiting for ongoing tasks to complete...") - cancelled = true - execErr = m.ctx.Err() - } - goto waitForCompletion - } - - // Acquire semaphore before spawning a goroutine - select { - case semaphore <- struct{}{}: - // Successfully acquired semaphore - case <-m.ctx.Done(): - // Context cancelled while waiting for semaphore - if !cancelled { - log.Warn("Operation cancelled by user, waiting for ongoing tasks to complete...") - cancelled = true - execErr = m.ctx.Err() - } - goto waitForCompletion - } - - wg.Add(1) - go m.execute( - idx, - resultsChan, - progressChan, - &wg, - semaphore, - configFiles[idx], - m.tmpFiles[idx], - total, - ) + select { + case <-m.ctx.Done(): + execErr = m.ctx.Err() + return + default: } - waitForCompletion: - // Wait for all work goroutines to complete - wg.Wait() - close(progressChan) - close(resultsChan) - - // Wait for the progress reader to finish processing all updates - progressWg.Wait() + execErr = m.runMockery(tmpFile, tmpFile) } if err := spin.Action(action).Run(); err != nil { - execErr = fmt.Errorf("execution error: %w", err) + return fmt.Errorf("execution error: %w", err) } - // Wait for action to complete <-doneChan - if execErr != nil && !errors.Is(execErr, context.Canceled) { - log.Errorf("Execution encountered errors: %v", execErr) - } - - for result := range resultsChan { - results = append(results, result) - } - - return results + return execErr } -// execute runs a mockery command for a specific configuration file and communicates results and progress updates. -func (m *Mockery) execute( - idx int, - resultChan chan mockeryJob, - progressChan chan progressUpdate, - wg *sync.WaitGroup, - sem chan struct{}, - configFile string, - tmpFile string, - total int, -) { - defer wg.Done() - defer func() { <-sem }() // Release semaphore after work - - // Check if context is cancelled before starting work - select { - case <-m.ctx.Done(): - // Context cancelled, skip execution - result := mockeryJob{ - configFile: configFile, - tmpFile: tmpFile, - err: m.ctx.Err(), - duration: 0, - } - resultChan <- result - return - default: - // Continue with execution - } - - startTime := time.Now() - err := m.runMockery(tmpFile, configFile) - duration := time.Since(startTime) - - result := mockeryJob{ - configFile: configFile, - tmpFile: tmpFile, - err: err, - duration: duration, - } - - resultChan <- result +// deepMerge performs a deep merge of two maps, with b taking precedence. +func (m *Mockery) deepMerge(a, b map[string]any) map[string]any { + result := make(map[string]any) - // Send progress update - progressChan <- progressUpdate{ - current: idx + 1, - total: total, - configFile: configFile, - success: err == nil, - err: err, - duration: duration, + for k, v := range a { + result[k] = v } -} - -// shortenConfigPath extracts a meaningful short name from a config file path -func (m *Mockery) shortenConfigPath(configPath string) string { - // Remove .mockery.pkg.yml suffix - path := strings.TrimSuffix(configPath, "/.mockery.pkg.yml") - // If a path is too long, show only the last 2-3 segments - parts := strings.Split(path, "/") - if len(parts) > 3 { - return ".../" + strings.Join(parts[len(parts)-3:], "/") + for k, v := range b { + if vMap, ok := v.(map[string]any); ok { + if aMap, ok := result[k].(map[string]any); ok { + result[k] = m.deepMerge(aMap, vMap) + continue + } + } + result[k] = v } - return path + return result } -// runMockery executes mockery with the given config file +// runMockery executes mockery with the given config file. func (m *Mockery) runMockery(configPath, originalPath string) error { if m.dry { - // Dry run - skip execution and just validate the config file exists log.Debugf("Dry run: would execute mockery --config %s", configPath) return nil } @@ -691,13 +502,13 @@ func (m *Mockery) runMockery(configPath, originalPath string) error { command := fmt.Sprintf("mockery --config %s", configPath) output, err := exec.Command(command) if err != nil { - // Provide more context in error return fmt.Errorf("mockery failed for %s: %w\nOutput: %s\nTip: Check the config syntax and package paths", originalPath, err, output) } + return nil } -// cleanup removes all temporary config files +// cleanup removes all temporary config files. func (m *Mockery) cleanup() { if len(m.tmpFiles) == 0 { return @@ -715,82 +526,11 @@ func (m *Mockery) cleanup() { } } -// calculateStats calculates execution statistics -func (m *Mockery) calculateStats(results []mockeryJob, startTime time.Time) executionStats { - stats := executionStats{ - total: len(m.tmpFiles), // Use total tmpFiles, not just results length - duration: time.Since(startTime), - } - - for _, result := range results { - // Don't count context cancellation as failure - if errors.Is(result.err, context.Canceled) || errors.Is(result.err, context.DeadlineExceeded) { - continue - } - - if result.err != nil { - stats.failed++ - } else { - stats.succeeded++ - } - } - - return stats -} - -// displaySummary displays execution summary -func (m *Mockery) displaySummary(stats executionStats, results []mockeryJob) { - if stats.failed > 0 { - log.Errorf("✗ Failed: %d/%d packages (%.2fs)", stats.failed, stats.total, stats.duration.Seconds()) - log.Errorf("Failed packages:") - - for _, result := range results { - if result.err != nil { - log.Errorf(" • %s", result.configFile) - log.Errorf(" %v", result.err) - } - } - - log.Info("Tip: Check the error messages above for details on how to fix the configurations") - } else { - if m.dry { - log.Successf("✓ All %d package(s) validated successfully (%.2fs)", stats.total, stats.duration.Seconds()) - log.Info("Dry run completed - no mockery commands were executed") - } else { - log.Successf("✓ All %d package(s) completed successfully (%.2fs)", stats.total, stats.duration.Seconds()) - } - } -} - -// displayCancellationSummary displays summary when operation is cancelled -func (m *Mockery) displayCancellationSummary(stats executionStats, results []mockeryJob) { - completed := stats.succeeded + stats.failed - cancelled := stats.total - completed - - log.Warnf("⚠ Operation cancelled by user") - log.Infof("Completed: %d/%d packages", completed, stats.total) - log.Infof("Cancelled: %d packages", cancelled) - log.Infof("Duration: %.2fs", stats.duration.Seconds()) - - if stats.failed > 0 { - log.Warnf("Failed packages before cancellation:") - - for _, result := range results { - if result.err != nil && !errors.Is(result.err, context.Canceled) && !errors.Is(result.err, context.DeadlineExceeded) { - log.Errorf(" • %s", result.configFile) - log.Errorf(" %v", result.err) - } - } - } - - log.Info("Temporary files have been cleaned up") -} - -// generateRandomID generates a random hex string for temp file naming +// generateRandomID generates a random hex string for temp file naming. func generateRandomID() (string, error) { - bytes := make([]byte, 4) // Reduced from 8 for shorter names - if _, err := rand.Read(bytes); err != nil { + b := make([]byte, 4) + if _, err := rand.Read(b); err != nil { return "", fmt.Errorf("failed to generate random ID: %w", err) } - return hex.EncodeToString(bytes), nil + return hex.EncodeToString(b), nil } From 2424828009ee9fb2774eb4a222ea1079f72a3f9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Garc=C3=ADa?= <58498506+negarciacamilo@users.noreply.github.com> Date: Wed, 25 Feb 2026 12:40:00 -0300 Subject: [PATCH 2/3] refactor(mockery): remove jobsNum parameter and simplify execution logic --- cmd/commands/mockery/mockery.go | 20 +++++++------------- internal/actions/mockery/mockery.go | 8 +------- internal/actions/newdomain/mockery.go | 6 +----- 3 files changed, 9 insertions(+), 25 deletions(-) diff --git a/cmd/commands/mockery/mockery.go b/cmd/commands/mockery/mockery.go index b9fae41..d8705aa 100644 --- a/cmd/commands/mockery/mockery.go +++ b/cmd/commands/mockery/mockery.go @@ -9,23 +9,21 @@ import ( ) var ( - jobsNum int - dry bool - gitMod bool + dry bool + gitMod bool ) var mockeryCmd = &cobra.Command{ Use: "mockery [config-files...]", Short: "Run mockery with base and package-specific configurations", - Long: `Run mockery by merging .mockery.base.yml with .mockery.pkg.yml files. + Long: `Run mockery by merging all .mockery.pkg.yml files into a single invocation. This command will: 1. Load base configuration from .mockery.base.yml 2. Search for all .mockery.pkg.yml files (or use provided list) -3. Merge base config with package config (package takes precedence) -4. Generate temporary config files and execute mockery concurrently -5. Report failures without stopping execution -6. Clean up temporary files and exit with code 1 if any failed +3. Merge all package configs into a single unified config +4. Execute mockery once for all packages +5. Clean up temporary files and exit with code 1 if failed Examples: # Run mockery for all .mockery.pkg.yml files found in project @@ -34,9 +32,6 @@ Examples: # Run mockery for specific package config files draft mockery services/user/.mockery.pkg.yml services/auth/.mockery.pkg.yml - # Run with custom concurrent job limit (default: 5) - draft mockery --jobs-num 5 - # Dry run - validate configs without executing mockery draft mockery --dry @@ -46,7 +41,6 @@ Examples: } func init() { - mockeryCmd.Flags().IntVarP(&jobsNum, "jobs-num", "j", 5, "Number of concurrent mockery jobs to run") mockeryCmd.Flags().BoolVar(&dry, "dry", false, "Dry run - validate and prepare configs without executing mockery") mockeryCmd.Flags().BoolVar(&gitMod, "git-mod", false, "Only run mockery for packages with modified files (compares HEAD with main branch)") } @@ -54,7 +48,7 @@ func init() { func run(cmd *cobra.Command, args []string) { common.ChDir(cmd) - if err := mockery.New(cmd.Context(), args, jobsNum, dry, gitMod).Exec(); err != nil { + if err := mockery.New(cmd.Context(), args, dry, gitMod).Exec(); err != nil { log.Exitf(1, "Failed to run mockery: %v", err) } diff --git a/internal/actions/mockery/mockery.go b/internal/actions/mockery/mockery.go index 124466c..764fbb3 100644 --- a/internal/actions/mockery/mockery.go +++ b/internal/actions/mockery/mockery.go @@ -30,17 +30,15 @@ const ( type Mockery struct { ctx context.Context configFiles []string - jobsNum int dry bool gitMod bool tmpFiles []string } -func New(ctx context.Context, configFiles []string, jobsNum int, dry bool, gitMod bool) *Mockery { +func New(ctx context.Context, configFiles []string, dry bool, gitMod bool) *Mockery { return &Mockery{ ctx: ctx, configFiles: configFiles, - jobsNum: jobsNum, dry: dry, gitMod: gitMod, tmpFiles: make([]string, 0), @@ -119,10 +117,6 @@ func (m *Mockery) Exec() error { // validate validates input parameters. func (m *Mockery) validate() error { - if m.jobsNum <= 0 { - return fmt.Errorf("invalid --jobs-num value: %d (must be greater than 0)", m.jobsNum) - } - if m.gitMod && len(m.configFiles) > 0 { return fmt.Errorf("cannot use --git-mod with explicit config file paths") } diff --git a/internal/actions/newdomain/mockery.go b/internal/actions/newdomain/mockery.go index 6382253..6631347 100644 --- a/internal/actions/newdomain/mockery.go +++ b/internal/actions/newdomain/mockery.go @@ -33,11 +33,7 @@ func (nd *NewDomain) runMockery() error { configFiles := []string{serviceMockeryPath, repositoryMockeryPath} - // Run mockery action with config files - // Using jobsNum=2 (one for service, one for repository) - // dry=false (actually execute mockery) - // gitMod=false (not using git diff mode) - m := mockery.New(nd.ctx, configFiles, 2, false, false) + m := mockery.New(nd.ctx, configFiles, false, false) return m.Exec() } From 6e34314a1eb9590d0ebdc647f51e5e281aff6430 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Garc=C3=ADa?= <58498506+negarciacamilo@users.noreply.github.com> Date: Wed, 25 Feb 2026 12:47:10 -0300 Subject: [PATCH 3/3] refactor(mockery): enhance package validation and streamline execution flow --- internal/actions/mockery/mockery.go | 36 ++++++++++++++--------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/internal/actions/mockery/mockery.go b/internal/actions/mockery/mockery.go index 764fbb3..4e088f1 100644 --- a/internal/actions/mockery/mockery.go +++ b/internal/actions/mockery/mockery.go @@ -82,13 +82,21 @@ func (m *Mockery) Exec() error { return err } + if _, hasPackages := mergedConfig["packages"]; !hasPackages { + log.Warn("No packages found in any config file - skipping mockery execution") + log.Info("Tip: Ensure your .mockery.pkg.yml files contain a 'packages:' section") + return nil + } + + // Register cleanup before creating the temp file so it runs even if creation + // partially succeeds (file appended to m.tmpFiles) and then returns an error. + defer m.cleanup() + tmpFile, err := m.createSingleTempConfig(mergedConfig, len(configFiles)) if err != nil { return err } - defer m.cleanup() - execErr := m.runSingleInvocation(tmpFile, len(configFiles)) if m.ctx.Err() != nil { @@ -441,37 +449,29 @@ func (m *Mockery) createSingleTempConfig(config map[string]any, pkgCount int) (s // Respects TTY mode: shows an animated spinner in interactive terminals, // falls back to plain log output in CI / non-TTY environments. func (m *Mockery) runSingleInvocation(tmpFile string, pkgCount int) error { - var execErr error - doneChan := make(chan struct{}) - title := fmt.Sprintf("Running mockery for %d package(s)...", pkgCount) - runWork := func() { - defer close(doneChan) - + runWork := func() error { select { case <-m.ctx.Done(): - execErr = m.ctx.Err() - return + return m.ctx.Err() default: } - execErr = m.runMockery(tmpFile, tmpFile) + return m.runMockery(tmpFile, tmpFile) } if isTTY() { + var execErr error spin := spinner.New().Title(title) - if err := spin.Action(func() { runWork() }).Run(); err != nil { + if err := spin.Action(func() { execErr = runWork() }).Run(); err != nil { return fmt.Errorf("execution error: %w", err) } - } else { - log.Info(title) - runWork() + return execErr } - <-doneChan - - return execErr + log.Info(title) + return runWork() } // isTTY returns true when animated UI (spinners) should be used.