Files
gh-action-readme/internal/generator_comprehensive_test.go
Ismo Vuorinen f94967713a refactor: major codebase improvements and test framework overhaul
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.
2025-08-05 23:20:58 +03:00

376 lines
10 KiB
Go

package internal
import (
"fmt"
"os"
"path/filepath"
"runtime"
"testing"
"github.com/ivuorinen/gh-action-readme/testutil"
)
// TestGenerator_ComprehensiveGeneration demonstrates the new table-driven testing framework
// by testing generation across all fixtures, themes, and formats systematically.
func TestGenerator_ComprehensiveGeneration(t *testing.T) {
// Create test cases using the new helper functions
cases := testutil.CreateGeneratorTestCases()
// Filter to a subset for demonstration (full test would be very large)
filteredCases := make([]testutil.GeneratorTestCase, 0)
for _, testCase := range cases {
// Only test a few combinations for demonstration
if (testCase.Theme == "default" && testCase.OutputFormat == "md") ||
(testCase.Theme == "github" && testCase.OutputFormat == "html") ||
(testCase.Theme == "minimal" && testCase.OutputFormat == "json") {
// Add custom executor for generator tests
testCase.Executor = createGeneratorTestExecutor()
filteredCases = append(filteredCases, testCase)
}
}
// Run the test suite
testutil.RunGeneratorTests(t, filteredCases)
}
// TestGenerator_AllValidFixtures tests generation with all valid fixtures.
func TestGenerator_AllValidFixtures(t *testing.T) {
validFixtures := testutil.GetValidFixtures()
for _, fixture := range validFixtures {
fixture := fixture // capture loop variable
t.Run(fixture, func(t *testing.T) {
t.Parallel()
// Create temporary action from fixture
actionPath := testutil.CreateTemporaryAction(t, fixture)
// Test with default configuration
config := &AppConfig{
Theme: "default",
OutputFormat: "md",
OutputDir: ".",
Quiet: true,
}
generator := NewGenerator(config)
// Generate documentation
err := generator.GenerateFromFile(actionPath)
if err != nil {
t.Errorf("failed to generate documentation for fixture %s: %v", fixture, err)
}
})
}
}
// TestGenerator_AllInvalidFixtures tests that invalid fixtures produce expected errors.
func TestGenerator_AllInvalidFixtures(t *testing.T) {
invalidFixtures := testutil.GetInvalidFixtures()
for _, fixture := range invalidFixtures {
fixture := fixture // capture loop variable
t.Run(fixture, func(t *testing.T) {
t.Parallel()
// Some invalid fixtures might not be loadable
actionFixture, err := testutil.LoadActionFixture(fixture)
if err != nil {
// This is expected for some invalid fixtures
return
}
// Create temporary action from fixture
tempDir, cleanup := testutil.TempDir(t)
defer cleanup()
testutil.WriteTestFile(t, tempDir+"/action.yml", actionFixture.Content)
// Test with default configuration
config := &AppConfig{
Theme: "default",
OutputFormat: "md",
OutputDir: ".",
Quiet: true,
}
generator := NewGenerator(config)
// Generate documentation - should fail
err = generator.GenerateFromFile(tempDir + "/action.yml")
if err == nil {
t.Errorf("expected generation to fail for invalid fixture %s, but it succeeded", fixture)
}
})
}
}
// TestGenerator_AllThemes demonstrates theme testing using helper functions.
func TestGenerator_AllThemes(t *testing.T) {
// Use the helper function to test all themes
testutil.TestAllThemes(t, func(t *testing.T, theme string) {
// Create a simple action for testing
actionPath := testutil.CreateTemporaryAction(t, "actions/javascript/simple.yml")
config := &AppConfig{
Theme: theme,
OutputFormat: "md",
OutputDir: ".",
Quiet: true,
}
generator := NewGenerator(config)
err := generator.GenerateFromFile(actionPath)
testutil.AssertNoError(t, err)
})
}
// TestGenerator_AllFormats demonstrates format testing using helper functions.
func TestGenerator_AllFormats(t *testing.T) {
// Use the helper function to test all formats
testutil.TestAllFormats(t, func(t *testing.T, format string) {
// Create a simple action for testing
actionPath := testutil.CreateTemporaryAction(t, "actions/javascript/simple.yml")
config := &AppConfig{
Theme: "default",
OutputFormat: format,
OutputDir: ".",
Quiet: true,
}
generator := NewGenerator(config)
err := generator.GenerateFromFile(actionPath)
testutil.AssertNoError(t, err)
})
}
// TestGenerator_ByActionType demonstrates testing by action type.
func TestGenerator_ByActionType(t *testing.T) {
actionTypes := []testutil.ActionType{
testutil.ActionTypeJavaScript,
testutil.ActionTypeComposite,
testutil.ActionTypeDocker,
}
for _, actionType := range actionTypes {
actionType := actionType // capture loop variable
t.Run(string(actionType), func(t *testing.T) {
t.Parallel()
fixtures := testutil.GetFixturesByActionType(actionType)
if len(fixtures) == 0 {
t.Skipf("no fixtures available for action type %s", actionType)
}
// Test the first fixture of this type
fixture := fixtures[0]
actionPath := testutil.CreateTemporaryAction(t, fixture)
config := &AppConfig{
Theme: "default",
OutputFormat: "md",
OutputDir: ".",
Quiet: true,
}
generator := NewGenerator(config)
err := generator.GenerateFromFile(actionPath)
testutil.AssertNoError(t, err)
})
}
}
// TestGenerator_WithMockEnvironment demonstrates testing with a complete mock environment.
func TestGenerator_WithMockEnvironment(t *testing.T) {
// Create a complete test environment
envConfig := &testutil.EnvironmentConfig{
ActionFixtures: []string{"actions/composite/with-dependencies.yml"},
WithMocks: true,
}
env := testutil.CreateTestEnvironment(t, envConfig)
// Clean up environment
defer func() {
for _, cleanup := range env.Cleanup {
if err := cleanup(); err != nil {
t.Errorf("cleanup failed: %v", err)
}
}
}()
if len(env.ActionPaths) == 0 {
t.Fatal("expected at least one action path")
}
config := &AppConfig{
Theme: "github",
OutputFormat: "md",
OutputDir: ".",
Quiet: true,
}
generator := NewGenerator(config)
err := generator.GenerateFromFile(env.ActionPaths[0])
testutil.AssertNoError(t, err)
}
// TestGenerator_FixtureValidation demonstrates fixture validation.
func TestGenerator_FixtureValidation(t *testing.T) {
// Test that all valid fixtures pass validation
validFixtures := testutil.GetValidFixtures()
for _, fixtureName := range validFixtures {
t.Run(fixtureName, func(t *testing.T) {
testutil.AssertFixtureValid(t, fixtureName)
})
}
// Test that all invalid fixtures fail validation
invalidFixtures := testutil.GetInvalidFixtures()
for _, fixtureName := range invalidFixtures {
t.Run(fixtureName, func(t *testing.T) {
testutil.AssertFixtureInvalid(t, fixtureName)
})
}
}
// createGeneratorTestExecutor returns a test executor function for generator tests.
func createGeneratorTestExecutor() testutil.TestExecutor {
return func(t *testing.T, testCase testutil.TestCase, ctx *testutil.TestContext) *testutil.TestResult {
t.Helper()
result := &testutil.TestResult{
Context: ctx,
}
var actionPath string
// If we have a fixture, load it and create action file
if testCase.Fixture != "" {
fixture, err := ctx.FixtureManager.LoadActionFixture(testCase.Fixture)
if err != nil {
result.Error = fmt.Errorf("failed to load fixture %s: %w", testCase.Fixture, err)
return result
}
// Create temporary action file
actionPath = filepath.Join(ctx.TempDir, "action.yml")
testutil.WriteTestFile(t, actionPath, fixture.Content)
}
// If we don't have an action file to test, just return success
if actionPath == "" {
result.Success = true
return result
}
// Create generator configuration from test config
config := createGeneratorConfigFromTestConfig(ctx.Config, ctx.TempDir)
// Save current working directory and change to project root for template resolution
originalWd, err := os.Getwd()
if err != nil {
result.Error = fmt.Errorf("failed to get working directory: %w", err)
return result
}
// Use runtime.Caller to find project root relative to this file
_, currentFile, _, ok := runtime.Caller(0)
if !ok {
result.Error = fmt.Errorf("failed to get current file path")
return result
}
// Get the project root (go up from internal/generator_comprehensive_test.go to project root)
projectRoot := filepath.Dir(filepath.Dir(currentFile))
if err := os.Chdir(projectRoot); err != nil {
result.Error = fmt.Errorf("failed to change to project root %s: %w", projectRoot, err)
return result
}
// Debug: Log the working directory and template path
currentWd, _ := os.Getwd()
t.Logf("Test working directory: %s, template path: %s", currentWd, config.Template)
// Restore working directory after test
defer func() {
if err := os.Chdir(originalWd); err != nil {
// Log error but don't fail the test
t.Logf("Failed to restore working directory: %v", err)
}
}()
// Create and run generator
generator := NewGenerator(config)
err = generator.GenerateFromFile(actionPath)
if err != nil {
result.Error = err
result.Success = false
} else {
result.Success = true
// Detect generated files
result.Files = testutil.DetectGeneratedFiles(ctx.TempDir, config.OutputFormat)
}
return result
}
}
// createGeneratorConfigFromTestConfig converts TestConfig to AppConfig.
func createGeneratorConfigFromTestConfig(testConfig *testutil.TestConfig, outputDir string) *AppConfig {
config := &AppConfig{
Theme: "default",
OutputFormat: "md",
OutputDir: outputDir,
Template: "templates/readme.tmpl",
Schema: "schemas/schema.json",
Verbose: false,
Quiet: true, // Default to quiet for tests
GitHubToken: "",
}
// Override with test-specific settings
if testConfig != nil {
if testConfig.Theme != "" {
config.Theme = testConfig.Theme
}
if testConfig.OutputFormat != "" {
config.OutputFormat = testConfig.OutputFormat
}
if testConfig.OutputDir != "" {
config.OutputDir = testConfig.OutputDir
}
config.Verbose = testConfig.Verbose
config.Quiet = testConfig.Quiet
}
// Set appropriate template path based on theme and output format
config.Template = resolveTemplatePathForTest(config.Theme, config.OutputFormat)
return config
}
// resolveTemplatePathForTest resolves the correct template path for testing.
func resolveTemplatePathForTest(theme, _ string) string {
switch theme {
case "github":
return "templates/themes/github/readme.tmpl"
case "gitlab":
return "templates/themes/gitlab/readme.tmpl"
case "minimal":
return "templates/themes/minimal/readme.tmpl"
case "professional":
return "templates/themes/professional/readme.tmpl"
default:
return "templates/readme.tmpl"
}
}