mirror of
https://github.com/ivuorinen/gh-action-readme.git
synced 2026-01-26 11:14:04 +00:00
* 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.
477 lines
15 KiB
Go
477 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) {
|
|
t.Parallel()
|
|
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) {
|
|
t.Parallel()
|
|
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) {
|
|
t.Parallel()
|
|
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) {
|
|
t.Parallel()
|
|
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) {
|
|
t.Parallel()
|
|
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) {
|
|
t.Parallel()
|
|
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) {
|
|
t.Parallel()
|
|
// 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) {
|
|
t.Parallel()
|
|
// 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)
|
|
}
|