mirror of
https://github.com/ivuorinen/gh-action-readme.git
synced 2026-02-06 09:50:16 +00:00
This commit represents a comprehensive refactoring of the codebase focused on improving code quality, testability, and maintainability. Key improvements: - Implement dependency injection and interface-based architecture - Add comprehensive test framework with fixtures and test suites - Fix all linting issues (errcheck, gosec, staticcheck, goconst, etc.) - Achieve full EditorConfig compliance across all files - Replace hardcoded test data with proper fixture files - Add configuration loader with hierarchical config support - Improve error handling with contextual information - Add progress indicators for better user feedback - Enhance Makefile with help system and improved editorconfig commands - Consolidate constants and remove deprecated code - Strengthen validation logic for GitHub Actions - Add focused consumer interfaces for better separation of concerns Testing improvements: - Add comprehensive integration tests - Implement test executor pattern for better test organization - Create extensive YAML fixture library for testing - Fix all failing tests and improve test coverage - Add validation test fixtures to avoid embedded YAML in Go files Build and tooling: - Update Makefile to show help by default - Fix editorconfig commands to use eclint properly - Add comprehensive help documentation to all make targets - Improve file selection patterns to avoid glob errors This refactoring maintains backward compatibility while significantly improving the internal architecture and developer experience.
463 lines
15 KiB
Go
463 lines
15 KiB
Go
// Package internal provides tests for the focused interfaces and demonstrates improved testability.
|
|
package internal
|
|
|
|
import (
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/schollz/progressbar/v3"
|
|
|
|
"github.com/ivuorinen/gh-action-readme/internal/errors"
|
|
)
|
|
|
|
// MockMessageLogger implements MessageLogger for testing.
|
|
type MockMessageLogger struct {
|
|
InfoCalls []string
|
|
SuccessCalls []string
|
|
WarningCalls []string
|
|
BoldCalls []string
|
|
PrintfCalls []string
|
|
}
|
|
|
|
func (m *MockMessageLogger) Info(format string, args ...any) {
|
|
m.InfoCalls = append(m.InfoCalls, formatMessage(format, args...))
|
|
}
|
|
|
|
func (m *MockMessageLogger) Success(format string, args ...any) {
|
|
m.SuccessCalls = append(m.SuccessCalls, formatMessage(format, args...))
|
|
}
|
|
|
|
func (m *MockMessageLogger) Warning(format string, args ...any) {
|
|
m.WarningCalls = append(m.WarningCalls, formatMessage(format, args...))
|
|
}
|
|
|
|
func (m *MockMessageLogger) Bold(format string, args ...any) {
|
|
m.BoldCalls = append(m.BoldCalls, formatMessage(format, args...))
|
|
}
|
|
|
|
func (m *MockMessageLogger) Printf(format string, args ...any) {
|
|
m.PrintfCalls = append(m.PrintfCalls, formatMessage(format, args...))
|
|
}
|
|
|
|
func (m *MockMessageLogger) Fprintf(_ *os.File, format string, args ...any) {
|
|
// For testing, just track the formatted message
|
|
m.PrintfCalls = append(m.PrintfCalls, formatMessage(format, args...))
|
|
}
|
|
|
|
// MockErrorReporter implements ErrorReporter for testing.
|
|
type MockErrorReporter struct {
|
|
ErrorCalls []string
|
|
ErrorWithSuggestionsCalls []string
|
|
ErrorWithContextCalls []string
|
|
ErrorWithSimpleFixCalls []string
|
|
}
|
|
|
|
func (m *MockErrorReporter) Error(format string, args ...any) {
|
|
m.ErrorCalls = append(m.ErrorCalls, formatMessage(format, args...))
|
|
}
|
|
|
|
func (m *MockErrorReporter) ErrorWithSuggestions(err *errors.ContextualError) {
|
|
if err != nil {
|
|
m.ErrorWithSuggestionsCalls = append(m.ErrorWithSuggestionsCalls, err.Error())
|
|
}
|
|
}
|
|
|
|
func (m *MockErrorReporter) ErrorWithContext(_ errors.ErrorCode, message string, _ map[string]string) {
|
|
m.ErrorWithContextCalls = append(m.ErrorWithContextCalls, message)
|
|
}
|
|
|
|
func (m *MockErrorReporter) ErrorWithSimpleFix(message, suggestion string) {
|
|
m.ErrorWithSimpleFixCalls = append(m.ErrorWithSimpleFixCalls, message+": "+suggestion)
|
|
}
|
|
|
|
// MockProgressReporter implements ProgressReporter for testing.
|
|
type MockProgressReporter struct {
|
|
ProgressCalls []string
|
|
}
|
|
|
|
func (m *MockProgressReporter) Progress(format string, args ...any) {
|
|
m.ProgressCalls = append(m.ProgressCalls, formatMessage(format, args...))
|
|
}
|
|
|
|
// MockOutputConfig implements OutputConfig for testing.
|
|
type MockOutputConfig struct {
|
|
QuietMode bool
|
|
}
|
|
|
|
func (m *MockOutputConfig) IsQuiet() bool {
|
|
return m.QuietMode
|
|
}
|
|
|
|
// MockProgressManager implements ProgressManager for testing.
|
|
type MockProgressManager struct {
|
|
CreateProgressBarCalls []string
|
|
CreateProgressBarForFilesCalls []string
|
|
FinishProgressBarCalls int
|
|
FinishProgressBarWithNewlineCalls int
|
|
UpdateProgressBarCalls int
|
|
ProcessWithProgressBarCalls []string
|
|
}
|
|
|
|
func (m *MockProgressManager) CreateProgressBar(description string, total int) *progressbar.ProgressBar {
|
|
m.CreateProgressBarCalls = append(m.CreateProgressBarCalls, formatMessage("%s (total: %d)", description, total))
|
|
return nil // Return nil for mock to avoid actual progress bar
|
|
}
|
|
|
|
func (m *MockProgressManager) CreateProgressBarForFiles(description string, files []string) *progressbar.ProgressBar {
|
|
m.CreateProgressBarForFilesCalls = append(
|
|
m.CreateProgressBarForFilesCalls,
|
|
formatMessage("%s (files: %d)", description, len(files)),
|
|
)
|
|
return nil // Return nil for mock to avoid actual progress bar
|
|
}
|
|
|
|
func (m *MockProgressManager) FinishProgressBar(_ *progressbar.ProgressBar) {
|
|
m.FinishProgressBarCalls++
|
|
}
|
|
|
|
func (m *MockProgressManager) FinishProgressBarWithNewline(_ *progressbar.ProgressBar) {
|
|
m.FinishProgressBarWithNewlineCalls++
|
|
}
|
|
|
|
func (m *MockProgressManager) UpdateProgressBar(_ *progressbar.ProgressBar) {
|
|
m.UpdateProgressBarCalls++
|
|
}
|
|
|
|
func (m *MockProgressManager) ProcessWithProgressBar(
|
|
description string,
|
|
items []string,
|
|
processFunc func(item string, bar *progressbar.ProgressBar),
|
|
) {
|
|
m.ProcessWithProgressBarCalls = append(
|
|
m.ProcessWithProgressBarCalls,
|
|
formatMessage("%s (items: %d)", description, len(items)),
|
|
)
|
|
// Execute the process function for each item
|
|
for _, item := range items {
|
|
processFunc(item, nil)
|
|
}
|
|
}
|
|
|
|
// Helper function to format messages consistently.
|
|
func formatMessage(format string, args ...any) string {
|
|
if len(args) == 0 {
|
|
return format
|
|
}
|
|
// Simple formatting for test purposes
|
|
result := format
|
|
for _, arg := range args {
|
|
result = strings.Replace(result, "%s", toString(arg), 1)
|
|
result = strings.Replace(result, "%d", toString(arg), 1)
|
|
result = strings.Replace(result, "%v", toString(arg), 1)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func toString(v any) string {
|
|
switch val := v.(type) {
|
|
case string:
|
|
return val
|
|
case int:
|
|
return formatInt(val)
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|
|
|
|
func formatInt(i int) string {
|
|
// Simple int to string conversion for testing
|
|
if i == 0 {
|
|
return "0"
|
|
}
|
|
result := ""
|
|
negative := i < 0
|
|
if negative {
|
|
i = -i
|
|
}
|
|
for i > 0 {
|
|
digit := i % 10
|
|
result = string(rune('0'+digit)) + result
|
|
i /= 10
|
|
}
|
|
if negative {
|
|
result = "-" + result
|
|
}
|
|
return result
|
|
}
|
|
|
|
// Test that demonstrates improved testability with focused interfaces.
|
|
func TestFocusedInterfaces_SimpleLogger(t *testing.T) {
|
|
mockLogger := &MockMessageLogger{}
|
|
simpleLogger := NewSimpleLogger(mockLogger)
|
|
|
|
// Test successful operation
|
|
simpleLogger.LogOperation("test-operation", true)
|
|
|
|
// Verify the expected calls were made
|
|
if len(mockLogger.InfoCalls) != 1 {
|
|
t.Errorf("expected 1 Info call, got %d", len(mockLogger.InfoCalls))
|
|
}
|
|
if len(mockLogger.SuccessCalls) != 1 {
|
|
t.Errorf("expected 1 Success call, got %d", len(mockLogger.SuccessCalls))
|
|
}
|
|
if len(mockLogger.WarningCalls) != 0 {
|
|
t.Errorf("expected 0 Warning calls, got %d", len(mockLogger.WarningCalls))
|
|
}
|
|
|
|
// Check message content
|
|
if !strings.Contains(mockLogger.InfoCalls[0], "test-operation") {
|
|
t.Errorf("expected Info call to contain 'test-operation', got: %s", mockLogger.InfoCalls[0])
|
|
}
|
|
|
|
if !strings.Contains(mockLogger.SuccessCalls[0], "test-operation") {
|
|
t.Errorf("expected Success call to contain 'test-operation', got: %s", mockLogger.SuccessCalls[0])
|
|
}
|
|
}
|
|
|
|
func TestFocusedInterfaces_SimpleLogger_WithFailure(t *testing.T) {
|
|
mockLogger := &MockMessageLogger{}
|
|
simpleLogger := NewSimpleLogger(mockLogger)
|
|
|
|
// Test failed operation
|
|
simpleLogger.LogOperation("failing-operation", false)
|
|
|
|
// Verify the expected calls were made
|
|
if len(mockLogger.InfoCalls) != 1 {
|
|
t.Errorf("expected 1 Info call, got %d", len(mockLogger.InfoCalls))
|
|
}
|
|
if len(mockLogger.SuccessCalls) != 0 {
|
|
t.Errorf("expected 0 Success calls, got %d", len(mockLogger.SuccessCalls))
|
|
}
|
|
if len(mockLogger.WarningCalls) != 1 {
|
|
t.Errorf("expected 1 Warning call, got %d", len(mockLogger.WarningCalls))
|
|
}
|
|
}
|
|
|
|
func TestFocusedInterfaces_ErrorManager(t *testing.T) {
|
|
mockReporter := &MockErrorReporter{}
|
|
mockFormatter := &MockErrorFormatter{}
|
|
mockManager := &mockErrorManager{
|
|
reporter: mockReporter,
|
|
formatter: mockFormatter,
|
|
}
|
|
errorManager := NewFocusedErrorManager(mockManager)
|
|
|
|
// Test validation error handling
|
|
errorManager.HandleValidationError("test-file.yml", []string{"name", "description"})
|
|
|
|
// Verify the expected calls were made
|
|
if len(mockReporter.ErrorWithContextCalls) != 1 {
|
|
t.Errorf("expected 1 ErrorWithContext call, got %d", len(mockReporter.ErrorWithContextCalls))
|
|
}
|
|
|
|
if !strings.Contains(mockReporter.ErrorWithContextCalls[0], "test-file.yml") {
|
|
t.Errorf("expected error message to contain 'test-file.yml', got: %s", mockReporter.ErrorWithContextCalls[0])
|
|
}
|
|
}
|
|
|
|
func TestFocusedInterfaces_TaskProgress(t *testing.T) {
|
|
mockReporter := &MockProgressReporter{}
|
|
taskProgress := NewTaskProgress(mockReporter)
|
|
|
|
// Test progress reporting
|
|
taskProgress.ReportProgress("compile", 3, 10)
|
|
|
|
// Verify the expected calls were made
|
|
if len(mockReporter.ProgressCalls) != 1 {
|
|
t.Errorf("expected 1 Progress call, got %d", len(mockReporter.ProgressCalls))
|
|
}
|
|
|
|
if !strings.Contains(mockReporter.ProgressCalls[0], "compile") {
|
|
t.Errorf("expected progress message to contain 'compile', got: %s", mockReporter.ProgressCalls[0])
|
|
}
|
|
}
|
|
|
|
func TestFocusedInterfaces_ConfigAwareComponent(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
quietMode bool
|
|
shouldShow bool
|
|
}{
|
|
{
|
|
name: "normal mode should output",
|
|
quietMode: false,
|
|
shouldShow: true,
|
|
},
|
|
{
|
|
name: "quiet mode should not output",
|
|
quietMode: true,
|
|
shouldShow: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
mockConfig := &MockOutputConfig{QuietMode: tt.quietMode}
|
|
component := NewConfigAwareComponent(mockConfig)
|
|
|
|
result := component.ShouldOutput()
|
|
|
|
if result != tt.shouldShow {
|
|
t.Errorf("expected ShouldOutput() to return %v, got %v", tt.shouldShow, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFocusedInterfaces_CompositeOutputWriter(t *testing.T) {
|
|
// Create a composite mock that implements OutputWriter
|
|
mockLogger := &MockMessageLogger{}
|
|
mockProgress := &MockProgressReporter{}
|
|
mockConfig := &MockOutputConfig{QuietMode: false}
|
|
|
|
compositeWriter := &CompositeOutputWriter{
|
|
writer: &mockOutputWriter{
|
|
logger: mockLogger,
|
|
reporter: mockProgress,
|
|
config: mockConfig,
|
|
},
|
|
}
|
|
|
|
items := []string{"item1", "item2", "item3"}
|
|
compositeWriter.ProcessWithOutput(items)
|
|
|
|
// Verify that the composite writer uses both message logging and progress reporting
|
|
// Should have called Info and Success for overall status
|
|
if len(mockLogger.InfoCalls) != 1 {
|
|
t.Errorf("expected 1 Info call, got %d", len(mockLogger.InfoCalls))
|
|
}
|
|
if len(mockLogger.SuccessCalls) != 1 {
|
|
t.Errorf("expected 1 Success call, got %d", len(mockLogger.SuccessCalls))
|
|
}
|
|
|
|
// Should have called Progress for each item
|
|
if len(mockProgress.ProgressCalls) != 3 {
|
|
t.Errorf("expected 3 Progress calls, got %d", len(mockProgress.ProgressCalls))
|
|
}
|
|
}
|
|
|
|
func TestFocusedInterfaces_GeneratorWithDependencyInjection(t *testing.T) {
|
|
// Create focused mocks
|
|
mockOutput := &mockCompleteOutput{
|
|
logger: &MockMessageLogger{},
|
|
reporter: &MockErrorReporter{},
|
|
formatter: &MockErrorFormatter{},
|
|
progress: &MockProgressReporter{},
|
|
config: &MockOutputConfig{QuietMode: false},
|
|
}
|
|
mockProgress := &MockProgressManager{}
|
|
|
|
// Create generator with dependency injection
|
|
config := &AppConfig{
|
|
Theme: "default",
|
|
OutputFormat: "md",
|
|
OutputDir: ".",
|
|
Verbose: false,
|
|
Quiet: false,
|
|
}
|
|
|
|
generator := NewGeneratorWithDependencies(config, mockOutput, mockProgress)
|
|
|
|
// Verify the generator was created with the injected dependencies
|
|
if generator == nil {
|
|
t.Fatal("expected generator to be created")
|
|
}
|
|
if generator.Config != config {
|
|
t.Error("expected generator to have the provided config")
|
|
}
|
|
if generator.Output != mockOutput {
|
|
t.Error("expected generator to have the injected output")
|
|
}
|
|
if generator.Progress != mockProgress {
|
|
t.Error("expected generator to have the injected progress manager")
|
|
}
|
|
}
|
|
|
|
// Composite mock types to implement the composed interfaces
|
|
|
|
type mockCompleteOutput struct {
|
|
logger MessageLogger
|
|
reporter ErrorReporter
|
|
formatter ErrorFormatter
|
|
progress ProgressReporter
|
|
config OutputConfig
|
|
}
|
|
|
|
func (m *mockCompleteOutput) Info(format string, args ...any) { m.logger.Info(format, args...) }
|
|
func (m *mockCompleteOutput) Success(format string, args ...any) { m.logger.Success(format, args...) }
|
|
func (m *mockCompleteOutput) Warning(format string, args ...any) { m.logger.Warning(format, args...) }
|
|
func (m *mockCompleteOutput) Bold(format string, args ...any) { m.logger.Bold(format, args...) }
|
|
func (m *mockCompleteOutput) Printf(format string, args ...any) { m.logger.Printf(format, args...) }
|
|
func (m *mockCompleteOutput) Fprintf(w *os.File, format string, args ...any) {
|
|
m.logger.Fprintf(w, format, args...)
|
|
}
|
|
func (m *mockCompleteOutput) Error(format string, args ...any) { m.reporter.Error(format, args...) }
|
|
func (m *mockCompleteOutput) ErrorWithSuggestions(err *errors.ContextualError) {
|
|
m.reporter.ErrorWithSuggestions(err)
|
|
}
|
|
func (m *mockCompleteOutput) ErrorWithContext(code errors.ErrorCode, message string, context map[string]string) {
|
|
m.reporter.ErrorWithContext(code, message, context)
|
|
}
|
|
func (m *mockCompleteOutput) ErrorWithSimpleFix(message, suggestion string) {
|
|
m.reporter.ErrorWithSimpleFix(message, suggestion)
|
|
}
|
|
func (m *mockCompleteOutput) FormatContextualError(err *errors.ContextualError) string {
|
|
return m.formatter.FormatContextualError(err)
|
|
}
|
|
func (m *mockCompleteOutput) Progress(format string, args ...any) {
|
|
m.progress.Progress(format, args...)
|
|
}
|
|
func (m *mockCompleteOutput) IsQuiet() bool { return m.config.IsQuiet() }
|
|
|
|
type mockOutputWriter struct {
|
|
logger MessageLogger
|
|
reporter ProgressReporter
|
|
config OutputConfig
|
|
}
|
|
|
|
func (m *mockOutputWriter) Info(format string, args ...any) { m.logger.Info(format, args...) }
|
|
func (m *mockOutputWriter) Success(format string, args ...any) { m.logger.Success(format, args...) }
|
|
func (m *mockOutputWriter) Warning(format string, args ...any) { m.logger.Warning(format, args...) }
|
|
func (m *mockOutputWriter) Bold(format string, args ...any) { m.logger.Bold(format, args...) }
|
|
func (m *mockOutputWriter) Printf(format string, args ...any) { m.logger.Printf(format, args...) }
|
|
func (m *mockOutputWriter) Fprintf(w *os.File, format string, args ...any) {
|
|
m.logger.Fprintf(w, format, args...)
|
|
}
|
|
func (m *mockOutputWriter) Progress(format string, args ...any) { m.reporter.Progress(format, args...) }
|
|
func (m *mockOutputWriter) IsQuiet() bool { return m.config.IsQuiet() }
|
|
|
|
// MockErrorFormatter implements ErrorFormatter for testing.
|
|
type MockErrorFormatter struct {
|
|
FormatContextualErrorCalls []string
|
|
}
|
|
|
|
func (m *MockErrorFormatter) FormatContextualError(err *errors.ContextualError) string {
|
|
if err != nil {
|
|
formatted := err.Error()
|
|
m.FormatContextualErrorCalls = append(m.FormatContextualErrorCalls, formatted)
|
|
return formatted
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// mockErrorManager implements ErrorManager for testing.
|
|
type mockErrorManager struct {
|
|
reporter ErrorReporter
|
|
formatter ErrorFormatter
|
|
}
|
|
|
|
func (m *mockErrorManager) Error(format string, args ...any) { m.reporter.Error(format, args...) }
|
|
func (m *mockErrorManager) ErrorWithSuggestions(err *errors.ContextualError) {
|
|
m.reporter.ErrorWithSuggestions(err)
|
|
}
|
|
func (m *mockErrorManager) ErrorWithContext(code errors.ErrorCode, message string, context map[string]string) {
|
|
m.reporter.ErrorWithContext(code, message, context)
|
|
}
|
|
func (m *mockErrorManager) ErrorWithSimpleFix(message, suggestion string) {
|
|
m.reporter.ErrorWithSimpleFix(message, suggestion)
|
|
}
|
|
func (m *mockErrorManager) FormatContextualError(err *errors.ContextualError) string {
|
|
return m.formatter.FormatContextualError(err)
|
|
}
|