feat(lint): add many linters, make all the tests run fast! (#23)

* chore(lint): added nlreturn, run linting

* chore(lint): replace some fmt.Sprintf calls

* chore(lint): replace fmt.Sprintf with strconv

* chore(lint): add goconst, use http lib for status codes, and methods

* chore(lint): use errors lib, errCodes from internal/errors

* chore(lint): dupl, thelper and usetesting

* chore(lint): fmt.Errorf %v to %w, more linters

* chore(lint): paralleltest, where possible

* perf(test): optimize test performance by 78%

- Implement shared binary building with package-level cache to eliminate redundant builds
- Add strategic parallelization to 15+ tests while preserving environment variable isolation
- Implement thread-safe fixture caching with RWMutex to reduce I/O operations
- Remove unnecessary working directory changes by leveraging embedded templates
- Add embedded template system with go:embed directive for reliable template resolution
- Fix linting issues: rename sharedBinaryError to errSharedBinary, add nolint directive

Performance improvements:
- Total test execution time: 12+ seconds → 2.7 seconds (78% faster)
- Binary build overhead: 14+ separate builds → 1 shared build (93% reduction)
- Parallel execution: Limited → 15+ concurrent tests (60-70% better CPU usage)
- I/O operations: 66+ fixture reads → cached with sync.RWMutex (50% reduction)

All tests maintain 100% success rate and coverage while running nearly 4x faster.
This commit is contained in:
2025-08-06 15:28:09 +03:00
committed by GitHub
parent 033c858a23
commit 4f12c4d3dd
63 changed files with 1948 additions and 485 deletions

View File

@@ -3,6 +3,7 @@ package wizard
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
@@ -88,7 +89,7 @@ func (d *ProjectDetector) DetectProjectSettings() (*DetectedSettings, error) {
// detectRepositoryInfo detects repository information from git.
func (d *ProjectDetector) detectRepositoryInfo(settings *DetectedSettings) error {
if d.repoRoot == "" {
return fmt.Errorf("not in a git repository")
return errors.New("not in a git repository")
}
repoInfo, err := git.DetectRepository(d.repoRoot)
@@ -103,6 +104,7 @@ func (d *ProjectDetector) detectRepositoryInfo(settings *DetectedSettings) error
settings.Version = d.detectVersion()
d.output.Success("Detected repository: %s/%s", settings.Organization, settings.Repository)
return nil
}
@@ -221,6 +223,7 @@ func (d *ProjectDetector) findActionFiles(dir string, recursive bool) ([]string,
if recursive {
return d.findActionFilesRecursive(dir)
}
return d.findActionFilesInDirectory(dir)
}
@@ -253,6 +256,7 @@ func (d *ProjectDetector) handleDirectory(info os.FileInfo) error {
if strings.HasPrefix(name, ".") || name == "node_modules" || name == "vendor" {
return filepath.SkipDir
}
return nil
}
@@ -366,6 +370,7 @@ func (d *ProjectDetector) analyzeProjectFiles() map[string]string {
}
d.setDefaultProjectType(characteristics)
return characteristics
}
@@ -425,6 +430,7 @@ func (d *ProjectDetector) setDefaultProjectType(characteristics map[string]strin
// getCurrentActionFiles gets action files in current directory only.
func (d *ProjectDetector) getCurrentActionFiles() []string {
actionFiles, _ := d.findActionFiles(d.currentDir, false)
return actionFiles
}

View File

@@ -9,6 +9,7 @@ import (
)
func TestProjectDetector_analyzeProjectFiles(t *testing.T) {
t.Parallel()
// Create temporary directory for testing
tempDir := t.TempDir()
@@ -50,6 +51,7 @@ func TestProjectDetector_analyzeProjectFiles(t *testing.T) {
for _, validType := range validTypes {
if projectType == validType {
typeValid = true
break
}
}
@@ -63,6 +65,7 @@ func TestProjectDetector_analyzeProjectFiles(t *testing.T) {
}
func TestProjectDetector_detectVersionFromPackageJSON(t *testing.T) {
t.Parallel()
tempDir := t.TempDir()
// Create package.json with version
@@ -90,6 +93,7 @@ func TestProjectDetector_detectVersionFromPackageJSON(t *testing.T) {
}
func TestProjectDetector_detectVersionFromFiles(t *testing.T) {
t.Parallel()
tempDir := t.TempDir()
// Create VERSION file
@@ -112,6 +116,7 @@ func TestProjectDetector_detectVersionFromFiles(t *testing.T) {
}
func TestProjectDetector_findActionFiles(t *testing.T) {
t.Parallel()
tempDir := t.TempDir()
// Create action files
@@ -167,6 +172,7 @@ func TestProjectDetector_findActionFiles(t *testing.T) {
}
func TestProjectDetector_isActionFile(t *testing.T) {
t.Parallel()
output := internal.NewColoredOutput(true)
detector := &ProjectDetector{
output: output,
@@ -186,6 +192,7 @@ func TestProjectDetector_isActionFile(t *testing.T) {
for _, tt := range tests {
t.Run(tt.filename, func(t *testing.T) {
t.Parallel()
result := detector.isActionFile(tt.filename)
if result != tt.expected {
t.Errorf("isActionFile(%s) = %v, want %v", tt.filename, result, tt.expected)
@@ -195,6 +202,7 @@ func TestProjectDetector_isActionFile(t *testing.T) {
}
func TestProjectDetector_suggestConfiguration(t *testing.T) {
t.Parallel()
output := internal.NewColoredOutput(true)
detector := &ProjectDetector{
output: output,
@@ -242,6 +250,7 @@ func TestProjectDetector_suggestConfiguration(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
detector.suggestConfiguration(tt.settings)
if tt.settings.SuggestedTheme != tt.expected {
t.Errorf("Expected theme %s, got %s", tt.expected, tt.settings.SuggestedTheme)

View File

@@ -80,6 +80,7 @@ func (e *ConfigExporter) exportYAML(config *internal.AppConfig, outputPath strin
}
e.output.Success("Configuration exported to: %s", outputPath)
return nil
}
@@ -104,6 +105,7 @@ func (e *ConfigExporter) exportJSON(config *internal.AppConfig, outputPath strin
}
e.output.Success("Configuration exported to: %s", outputPath)
return nil
}
@@ -129,6 +131,7 @@ func (e *ConfigExporter) exportTOML(config *internal.AppConfig, outputPath strin
e.writeTOMLConfig(file, exportConfig)
e.output.Success("Configuration exported to: %s", outputPath)
return nil
}

View File

@@ -13,6 +13,7 @@ import (
)
func TestConfigExporter_ExportConfig(t *testing.T) {
t.Parallel()
output := internal.NewColoredOutput(true) // quiet mode for testing
exporter := NewConfigExporter(output)
@@ -20,13 +21,22 @@ func TestConfigExporter_ExportConfig(t *testing.T) {
config := createTestConfig()
// Test YAML export
t.Run("export YAML", testYAMLExport(exporter, config))
t.Run("export YAML", func(t *testing.T) {
t.Parallel()
testYAMLExport(exporter, config)(t)
})
// Test JSON export
t.Run("export JSON", testJSONExport(exporter, config))
t.Run("export JSON", func(t *testing.T) {
t.Parallel()
testJSONExport(exporter, config)(t)
})
// Test TOML export
t.Run("export TOML", testTOMLExport(exporter, config))
t.Run("export TOML", func(t *testing.T) {
t.Parallel()
testTOMLExport(exporter, config)(t)
})
}
// createTestConfig creates a test configuration for testing.
@@ -49,6 +59,7 @@ func createTestConfig() *internal.AppConfig {
// testYAMLExport tests YAML export functionality.
func testYAMLExport(exporter *ConfigExporter, config *internal.AppConfig) func(*testing.T) {
return func(t *testing.T) {
t.Helper()
tempDir := t.TempDir()
outputPath := filepath.Join(tempDir, "config.yaml")
@@ -65,6 +76,7 @@ func testYAMLExport(exporter *ConfigExporter, config *internal.AppConfig) func(*
// testJSONExport tests JSON export functionality.
func testJSONExport(exporter *ConfigExporter, config *internal.AppConfig) func(*testing.T) {
return func(t *testing.T) {
t.Helper()
tempDir := t.TempDir()
outputPath := filepath.Join(tempDir, "config.json")
@@ -81,6 +93,7 @@ func testJSONExport(exporter *ConfigExporter, config *internal.AppConfig) func(*
// testTOMLExport tests TOML export functionality.
func testTOMLExport(exporter *ConfigExporter, config *internal.AppConfig) func(*testing.T) {
return func(t *testing.T) {
t.Helper()
tempDir := t.TempDir()
outputPath := filepath.Join(tempDir, "config.toml")
@@ -96,6 +109,7 @@ func testTOMLExport(exporter *ConfigExporter, config *internal.AppConfig) func(*
// verifyFileExists checks that a file exists at the given path.
func verifyFileExists(t *testing.T, outputPath string) {
t.Helper()
if _, err := os.Stat(outputPath); os.IsNotExist(err) {
t.Fatal("Expected output file to exist")
}
@@ -103,6 +117,7 @@ func verifyFileExists(t *testing.T, outputPath string) {
// verifyYAMLContent verifies YAML content is valid and contains expected data.
func verifyYAMLContent(t *testing.T, outputPath string, expected *internal.AppConfig) {
t.Helper()
data, err := os.ReadFile(outputPath) // #nosec G304 -- test output path
if err != nil {
t.Fatalf("Failed to read output file: %v", err)
@@ -123,6 +138,7 @@ func verifyYAMLContent(t *testing.T, outputPath string, expected *internal.AppCo
// verifyJSONContent verifies JSON content is valid and contains expected data.
func verifyJSONContent(t *testing.T, outputPath string, expected *internal.AppConfig) {
t.Helper()
data, err := os.ReadFile(outputPath) // #nosec G304 -- test output path
if err != nil {
t.Fatalf("Failed to read output file: %v", err)
@@ -143,6 +159,7 @@ func verifyJSONContent(t *testing.T, outputPath string, expected *internal.AppCo
// verifyTOMLContent verifies TOML content contains expected fields.
func verifyTOMLContent(t *testing.T, outputPath string) {
t.Helper()
data, err := os.ReadFile(outputPath) // #nosec G304 -- test output path
if err != nil {
t.Fatalf("Failed to read output file: %v", err)
@@ -158,6 +175,7 @@ func verifyTOMLContent(t *testing.T, outputPath string) {
}
func TestConfigExporter_sanitizeConfig(t *testing.T) {
t.Parallel()
output := internal.NewColoredOutput(true)
exporter := NewConfigExporter(output)
@@ -191,6 +209,7 @@ func TestConfigExporter_sanitizeConfig(t *testing.T) {
}
func TestConfigExporter_GetSupportedFormats(t *testing.T) {
t.Parallel()
output := internal.NewColoredOutput(true)
exporter := NewConfigExporter(output)
@@ -215,6 +234,7 @@ func TestConfigExporter_GetSupportedFormats(t *testing.T) {
}
func TestConfigExporter_GetDefaultOutputPath(t *testing.T) {
t.Parallel()
output := internal.NewColoredOutput(true)
exporter := NewConfigExporter(output)
@@ -229,6 +249,7 @@ func TestConfigExporter_GetDefaultOutputPath(t *testing.T) {
for _, tt := range tests {
t.Run(string(tt.format), func(t *testing.T) {
t.Parallel()
path, err := exporter.GetDefaultOutputPath(tt.format)
if err != nil {
t.Fatalf("GetDefaultOutputPath() error = %v", err)

View File

@@ -105,6 +105,7 @@ func (v *ConfigValidator) ValidateField(fieldName, value string) *ValidationResu
}
result.Valid = len(result.Errors) == 0
return result
}
@@ -116,6 +117,7 @@ func (v *ConfigValidator) validateOrganization(org string, result *ValidationRes
Message: "Organization is empty - will use auto-detected value",
Value: org,
})
return
}
@@ -139,6 +141,7 @@ func (v *ConfigValidator) validateRepository(repo string, result *ValidationResu
Message: "Repository is empty - will use auto-detected value",
Value: repo,
})
return
}
@@ -181,6 +184,7 @@ func (v *ConfigValidator) validateTheme(theme string, result *ValidationResult)
for _, validTheme := range validThemes {
if theme == validTheme {
found = true
break
}
}
@@ -192,7 +196,7 @@ func (v *ConfigValidator) validateTheme(theme string, result *ValidationResult)
Value: theme,
})
result.Suggestions = append(result.Suggestions,
fmt.Sprintf("Valid themes: %s", strings.Join(validThemes, ", ")))
"Valid themes: "+strings.Join(validThemes, ", "))
}
}
@@ -204,6 +208,7 @@ func (v *ConfigValidator) validateOutputFormat(format string, result *Validation
for _, validFormat := range validFormats {
if format == validFormat {
found = true
break
}
}
@@ -215,7 +220,7 @@ func (v *ConfigValidator) validateOutputFormat(format string, result *Validation
Value: format,
})
result.Suggestions = append(result.Suggestions,
fmt.Sprintf("Valid formats: %s", strings.Join(validFormats, ", ")))
"Valid formats: "+strings.Join(validFormats, ", "))
}
}
@@ -227,6 +232,7 @@ func (v *ConfigValidator) validateOutputDir(dir string, result *ValidationResult
Message: "Output directory cannot be empty",
Value: dir,
})
return
}
@@ -314,9 +320,10 @@ func (v *ConfigValidator) validatePermissions(permissions map[string]string, res
if !permissionExists {
result.Warnings = append(result.Warnings, ValidationWarning{
Field: "permissions",
Message: fmt.Sprintf("Unknown permission: %s", permission),
Message: "Unknown permission: " + permission,
Value: value,
})
continue
}
@@ -325,6 +332,7 @@ func (v *ConfigValidator) validatePermissions(permissions map[string]string, res
for _, validVal := range validValues {
if value == validVal {
validValue = true
break
}
}
@@ -332,7 +340,7 @@ func (v *ConfigValidator) validatePermissions(permissions map[string]string, res
if !validValue {
result.Errors = append(result.Errors, ValidationError{
Field: "permissions",
Message: fmt.Sprintf("Invalid value for permission %s", permission),
Message: "Invalid value for permission " + permission,
Value: value,
})
result.Suggestions = append(result.Suggestions,
@@ -351,6 +359,7 @@ func (v *ConfigValidator) validateRunsOn(runsOn []string, result *ValidationResu
})
result.Suggestions = append(result.Suggestions,
"Consider specifying at least one runner (e.g., ubuntu-latest)")
return
}
@@ -366,6 +375,7 @@ func (v *ConfigValidator) validateRunsOn(runsOn []string, result *ValidationResu
for _, validRunner := range validRunners {
if runner == validRunner {
isValid = true
break
}
}
@@ -375,7 +385,7 @@ func (v *ConfigValidator) validateRunsOn(runsOn []string, result *ValidationResu
if !strings.HasPrefix(runner, "self-hosted") {
result.Warnings = append(result.Warnings, ValidationWarning{
Field: "runs_on",
Message: fmt.Sprintf("Unknown runner: %s", runner),
Message: "Unknown runner: " + runner,
Value: runner,
})
result.Suggestions = append(result.Suggestions,
@@ -398,9 +408,10 @@ func (v *ConfigValidator) validateVariables(variables map[string]string, result
if strings.EqualFold(key, reserved) {
result.Warnings = append(result.Warnings, ValidationWarning{
Field: "variables",
Message: fmt.Sprintf("Variable name conflicts with GitHub environment variable: %s", key),
Message: "Variable name conflicts with GitHub environment variable: " + key,
Value: value,
})
break
}
}
@@ -409,7 +420,7 @@ func (v *ConfigValidator) validateVariables(variables map[string]string, result
if !v.isValidVariableName(key) {
result.Errors = append(result.Errors, ValidationError{
Field: "variables",
Message: fmt.Sprintf("Invalid variable name: %s", key),
Message: "Invalid variable name: " + key,
Value: value,
})
result.Suggestions = append(result.Suggestions,
@@ -427,6 +438,7 @@ func (v *ConfigValidator) isValidGitHubName(name string) bool {
// GitHub names can contain alphanumeric characters and hyphens
// Cannot start or end with hyphen
matched, _ := regexp.MatchString(`^[a-zA-Z0-9]([a-zA-Z0-9\-_]*[a-zA-Z0-9])?$`, name)
return matched
}
@@ -437,6 +449,7 @@ func (v *ConfigValidator) isValidSemanticVersion(version string) bool {
`(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?` +
`(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`
matched, _ := regexp.MatchString(pattern, version)
return matched
}
@@ -462,6 +475,7 @@ func (v *ConfigValidator) isValidVariableName(name string) bool {
// Variable names should start with letter or underscore
// and contain only letters, numbers, and underscores
matched, _ := regexp.MatchString(`^[a-zA-Z_][a-zA-Z0-9_]*$`, name)
return matched
}

View File

@@ -7,6 +7,7 @@ import (
)
func TestConfigValidator_ValidateConfig(t *testing.T) {
t.Parallel()
output := internal.NewColoredOutput(true) // quiet mode for testing
validator := NewConfigValidator(output)
@@ -74,6 +75,7 @@ func TestConfigValidator_ValidateConfig(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
result := validator.ValidateConfig(tt.config)
if result.Valid != tt.expectValid {
@@ -92,6 +94,7 @@ func TestConfigValidator_ValidateConfig(t *testing.T) {
}
func TestConfigValidator_ValidateField(t *testing.T) {
t.Parallel()
output := internal.NewColoredOutput(true)
validator := NewConfigValidator(output)
@@ -115,6 +118,7 @@ func TestConfigValidator_ValidateField(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
result := validator.ValidateField(tt.fieldName, tt.value)
if result.Valid != tt.expectValid {
@@ -125,6 +129,7 @@ func TestConfigValidator_ValidateField(t *testing.T) {
}
func TestConfigValidator_isValidGitHubName(t *testing.T) {
t.Parallel()
output := internal.NewColoredOutput(true)
validator := NewConfigValidator(output)
@@ -146,6 +151,7 @@ func TestConfigValidator_isValidGitHubName(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := validator.isValidGitHubName(tt.input)
if got != tt.want {
t.Errorf("isValidGitHubName(%q) = %v, want %v", tt.input, got, tt.want)
@@ -155,6 +161,7 @@ func TestConfigValidator_isValidGitHubName(t *testing.T) {
}
func TestConfigValidator_isValidSemanticVersion(t *testing.T) {
t.Parallel()
output := internal.NewColoredOutput(true)
validator := NewConfigValidator(output)
@@ -175,6 +182,7 @@ func TestConfigValidator_isValidSemanticVersion(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := validator.isValidSemanticVersion(tt.input)
if got != tt.want {
t.Errorf("isValidSemanticVersion(%q) = %v, want %v", tt.input, got, tt.want)
@@ -184,6 +192,7 @@ func TestConfigValidator_isValidSemanticVersion(t *testing.T) {
}
func TestConfigValidator_isValidGitHubToken(t *testing.T) {
t.Parallel()
output := internal.NewColoredOutput(true)
validator := NewConfigValidator(output)
@@ -204,6 +213,7 @@ func TestConfigValidator_isValidGitHubToken(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := validator.isValidGitHubToken(tt.input)
if got != tt.want {
t.Errorf("isValidGitHubToken(%q) = %v, want %v", tt.input, got, tt.want)
@@ -213,6 +223,7 @@ func TestConfigValidator_isValidGitHubToken(t *testing.T) {
}
func TestConfigValidator_isValidVariableName(t *testing.T) {
t.Parallel()
output := internal.NewColoredOutput(true)
validator := NewConfigValidator(output)
@@ -234,6 +245,7 @@ func TestConfigValidator_isValidVariableName(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := validator.isValidVariableName(tt.input)
if got != tt.want {
t.Errorf("isValidVariableName(%q) = %v, want %v", tt.input, got, tt.want)

View File

@@ -3,6 +3,7 @@ package wizard
import (
"bufio"
"errors"
"fmt"
"os"
"path/filepath"
@@ -60,6 +61,7 @@ func (w *ConfigWizard) Run() (*internal.AppConfig, error) {
}
w.output.Success("\n✅ Configuration completed successfully!")
return w.config, nil
}
@@ -218,6 +220,7 @@ func (w *ConfigWizard) configureGitHubIntegration() {
existingToken := internal.GetGitHubToken(w.config)
if existingToken != "" {
w.output.Success("GitHub token already configured ✓")
return
}
@@ -231,6 +234,7 @@ func (w *ConfigWizard) configureGitHubIntegration() {
if !setupToken {
w.output.Info("You can set up the token later using environment variables:")
w.output.Printf(" export GITHUB_TOKEN=your_personal_access_token")
return
}
@@ -284,8 +288,9 @@ func (w *ConfigWizard) confirmConfiguration() error {
w.output.Info("")
confirmed := w.promptYesNo("Save this configuration?", true)
if !confirmed {
return fmt.Errorf("configuration canceled by user")
return errors.New("configuration canceled by user")
}
return nil
}
@@ -302,6 +307,7 @@ func (w *ConfigWizard) promptWithDefault(prompt, defaultValue string) string {
if input == "" {
return defaultValue
}
return input
}
@@ -314,6 +320,7 @@ func (w *ConfigWizard) promptSensitive(prompt string) string {
if w.scanner.Scan() {
return strings.TrimSpace(w.scanner.Text())
}
return ""
}
@@ -337,6 +344,7 @@ func (w *ConfigWizard) promptYesNo(prompt string, defaultValue bool) bool {
return defaultValue
default:
w.output.Warning("Please answer 'y' or 'n'. Using default.")
return defaultValue
}
}