Initial commit

This commit is contained in:
2025-07-30 19:12:53 +03:00
commit 74cbe1e469
83 changed files with 12567 additions and 0 deletions

526
integration_test.go Normal file
View File

@@ -0,0 +1,526 @@
package main
import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"github.com/ivuorinen/gh-action-readme/testutil"
)
// buildTestBinary builds the test binary for integration testing.
func buildTestBinary(t *testing.T) string {
t.Helper()
tmpDir, err := os.MkdirTemp("", "gh-action-readme-binary-*")
if err != nil {
t.Fatalf("failed to create temp dir for binary: %v", err)
}
binaryPath := filepath.Join(tmpDir, "gh-action-readme")
cmd := exec.Command("go", "build", "-o", binaryPath, ".")
var stderr strings.Builder
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
t.Fatalf("failed to build test binary: %v\nstderr: %s", err, stderr.String())
}
return binaryPath
}
// setupCompleteWorkflow creates a realistic project structure for testing.
func setupCompleteWorkflow(t *testing.T, tmpDir string) {
testutil.WriteTestFile(t, filepath.Join(tmpDir, "action.yml"), testutil.CompositeActionYML)
testutil.WriteTestFile(t, filepath.Join(tmpDir, "README.md"), "# Old README")
testutil.WriteTestFile(t, filepath.Join(tmpDir, ".gitignore"), testutil.GitIgnoreContent)
testutil.WriteTestFile(t, filepath.Join(tmpDir, "package.json"), testutil.PackageJSONContent)
}
// setupMultiActionWorkflow creates a project with multiple actions.
func setupMultiActionWorkflow(t *testing.T, tmpDir string) {
testutil.WriteTestFile(t, filepath.Join(tmpDir, "action.yml"), testutil.SimpleActionYML)
subDir := filepath.Join(tmpDir, "actions", "deploy")
_ = os.MkdirAll(subDir, 0755)
testutil.WriteTestFile(t, filepath.Join(subDir, "action.yml"), testutil.DockerActionYML)
subDir2 := filepath.Join(tmpDir, "actions", "test")
_ = os.MkdirAll(subDir2, 0755)
testutil.WriteTestFile(t, filepath.Join(subDir2, "action.yml"), testutil.CompositeActionYML)
}
// setupConfigWorkflow creates a simple action for config testing.
func setupConfigWorkflow(t *testing.T, tmpDir string) {
testutil.WriteTestFile(t, filepath.Join(tmpDir, "action.yml"), testutil.SimpleActionYML)
}
// setupErrorWorkflow creates an invalid action file for error testing.
func setupErrorWorkflow(t *testing.T, tmpDir string) {
testutil.WriteTestFile(t, filepath.Join(tmpDir, "action.yml"), testutil.InvalidActionYML)
}
// checkStepExitCode validates command exit code expectations.
func checkStepExitCode(t *testing.T, step workflowStep, exitCode int, stdout, stderr strings.Builder) {
if step.expectSuccess && exitCode != 0 {
t.Errorf("expected success but got exit code %d", exitCode)
t.Logf("stdout: %s", stdout.String())
t.Logf("stderr: %s", stderr.String())
} else if !step.expectSuccess && exitCode == 0 {
t.Error("expected failure but command succeeded")
}
}
// checkStepOutput validates command output expectations.
func checkStepOutput(t *testing.T, step workflowStep, output string) {
if step.expectOutput != "" && !strings.Contains(output, step.expectOutput) {
t.Errorf("expected output to contain %q, got: %s", step.expectOutput, output)
}
if step.expectError != "" && !strings.Contains(output, step.expectError) {
t.Errorf("expected error to contain %q, got: %s", step.expectError, output)
}
}
// executeWorkflowStep runs a single workflow step.
func executeWorkflowStep(t *testing.T, binaryPath, tmpDir string, step workflowStep) {
t.Run(step.name, func(t *testing.T) {
cmd := exec.Command(binaryPath, step.cmd...)
cmd.Dir = tmpDir
var stdout, stderr strings.Builder
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
exitCode := 0
if err != nil {
if exitError, ok := err.(*exec.ExitError); ok {
exitCode = exitError.ExitCode()
}
}
checkStepExitCode(t, step, exitCode, stdout, stderr)
checkStepOutput(t, step, stdout.String()+stderr.String())
})
}
// TestEndToEndWorkflows tests complete workflows from start to finish.
func TestEndToEndWorkflows(t *testing.T) {
binaryPath := buildTestBinary(t)
defer func() { _ = os.Remove(binaryPath) }()
tests := []struct {
name string
setupFunc func(t *testing.T, tmpDir string)
workflow []workflowStep
}{
{
name: "Complete documentation generation workflow",
setupFunc: setupCompleteWorkflow,
workflow: []workflowStep{
{
name: "validate action file",
cmd: []string{"validate"},
expectSuccess: true,
expectOutput: "All validations passed",
},
{
name: "generate with default theme",
cmd: []string{"gen", "--theme", "default"},
expectSuccess: true,
},
{
name: "generate with github theme",
cmd: []string{"gen", "--theme", "github", "--output-format", "html"},
expectSuccess: true,
},
{
name: "list dependencies",
cmd: []string{"deps", "list"},
expectSuccess: true,
},
{
name: "check cache statistics",
cmd: []string{"cache", "stats"},
expectSuccess: true,
expectOutput: "Cache Statistics",
},
},
},
{
name: "Multi-action project workflow",
setupFunc: setupMultiActionWorkflow,
workflow: []workflowStep{
{
name: "validate all actions recursively",
cmd: []string{"validate"},
expectSuccess: true,
},
{
name: "generate docs for all actions",
cmd: []string{"gen", "--recursive", "--theme", "professional"},
expectSuccess: true,
},
{
name: "check all dependencies",
cmd: []string{"deps", "list"},
expectSuccess: true,
},
},
},
{
name: "Configuration management workflow",
setupFunc: setupConfigWorkflow,
workflow: []workflowStep{
{
name: "show current config",
cmd: []string{"config", "show"},
expectSuccess: true,
expectOutput: "Current Configuration",
},
{
name: "list available themes",
cmd: []string{"config", "themes"},
expectSuccess: true,
expectOutput: "Available Themes",
},
{
name: "generate with custom theme",
cmd: []string{"gen", "--theme", "minimal"},
expectSuccess: true,
},
},
},
{
name: "Error handling and recovery workflow",
setupFunc: setupErrorWorkflow,
workflow: []workflowStep{
{
name: "validate invalid action",
cmd: []string{"validate"},
expectSuccess: false,
expectError: "Missing required field",
},
{
name: "attempt generation with invalid action",
cmd: []string{"gen"},
expectSuccess: false,
},
{
name: "show schema for reference",
cmd: []string{"schema"},
expectSuccess: true,
expectOutput: "schema",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tmpDir, cleanup := testutil.TempDir(t)
defer cleanup()
// Setup the test environment
tt.setupFunc(t, tmpDir)
// Execute workflow steps
for _, step := range tt.workflow {
executeWorkflowStep(t, binaryPath, tmpDir, step)
}
})
}
}
type workflowStep struct {
name string
cmd []string
expectSuccess bool
expectOutput string
expectError string
}
// testProjectSetup tests basic project validation.
func testProjectSetup(t *testing.T, binaryPath, tmpDir string) {
// Create a new GitHub Action project
testutil.WriteTestFile(t, filepath.Join(tmpDir, "action.yml"), `
name: 'My New Action'
description: 'A brand new GitHub Action'
inputs:
message:
description: 'Message to display'
required: true
runs:
using: 'node20'
main: 'index.js'
`)
// Validate the action
cmd := exec.Command(binaryPath, "validate")
cmd.Dir = tmpDir
err := cmd.Run()
testutil.AssertNoError(t, err)
}
// testDocumentationGeneration tests generation with different themes.
func testDocumentationGeneration(t *testing.T, binaryPath, tmpDir string) {
themes := []string{"default", "github", "minimal"}
for _, theme := range themes {
cmd := exec.Command(binaryPath, "gen", "--theme", theme)
cmd.Dir = tmpDir
err := cmd.Run()
testutil.AssertNoError(t, err)
// Verify README was created
readmeFiles, _ := filepath.Glob(filepath.Join(tmpDir, "README*.md"))
if len(readmeFiles) == 0 {
t.Errorf("no README generated for theme %s", theme)
}
// Clean up for next iteration
for _, file := range readmeFiles {
_ = os.Remove(file)
}
}
}
// testDependencyManagement tests dependency listing functionality.
func testDependencyManagement(t *testing.T, binaryPath, tmpDir string) {
// Update action to be composite with dependencies
testutil.WriteTestFile(t, filepath.Join(tmpDir, "action.yml"), testutil.CompositeActionYML)
// List dependencies
cmd := exec.Command(binaryPath, "deps", "list")
cmd.Dir = tmpDir
var stdout strings.Builder
cmd.Stdout = &stdout
err := cmd.Run()
testutil.AssertNoError(t, err)
output := stdout.String()
if !strings.Contains(output, "Dependencies found") {
t.Error("expected dependency listing output")
}
}
// testOutputFormats tests generation with different output formats.
func testOutputFormats(t *testing.T, binaryPath, tmpDir string) {
formats := []string{"md", "html", "json"}
for _, format := range formats {
cmd := exec.Command(binaryPath, "gen", "--output-format", format)
cmd.Dir = tmpDir
err := cmd.Run()
testutil.AssertNoError(t, err)
// Verify output was created
var pattern string
switch format {
case "md":
pattern = "README*.md"
case "html":
pattern = "README*.html"
case "json":
pattern = "README*.json"
}
files, _ := filepath.Glob(filepath.Join(tmpDir, pattern))
if len(files) == 0 {
t.Errorf("no output generated for format %s", format)
}
// Clean up
for _, file := range files {
_ = os.Remove(file)
}
}
}
// testCacheManagement tests cache-related commands.
func testCacheManagement(t *testing.T, binaryPath, tmpDir string) {
// Check cache stats
cmd := exec.Command(binaryPath, "cache", "stats")
cmd.Dir = tmpDir
err := cmd.Run()
testutil.AssertNoError(t, err)
// Clear cache
cmd = exec.Command(binaryPath, "cache", "clear")
cmd.Dir = tmpDir
err = cmd.Run()
testutil.AssertNoError(t, err)
// Check path
cmd = exec.Command(binaryPath, "cache", "path")
cmd.Dir = tmpDir
err = cmd.Run()
testutil.AssertNoError(t, err)
}
func TestCompleteProjectLifecycle(t *testing.T) {
binaryPath := buildTestBinary(t)
defer func() { _ = os.Remove(binaryPath) }()
tmpDir, cleanup := testutil.TempDir(t)
defer cleanup()
// Phase 1: Project setup
t.Run("Phase 1: Project Setup", func(t *testing.T) {
testProjectSetup(t, binaryPath, tmpDir)
})
// Phase 2: Documentation generation
t.Run("Phase 2: Documentation Generation", func(t *testing.T) {
testDocumentationGeneration(t, binaryPath, tmpDir)
})
// Phase 3: Add dependencies and test dependency features
t.Run("Phase 3: Dependency Management", func(t *testing.T) {
testDependencyManagement(t, binaryPath, tmpDir)
})
// Phase 4: Multiple output formats
t.Run("Phase 4: Multiple Output Formats", func(t *testing.T) {
testOutputFormats(t, binaryPath, tmpDir)
})
// Phase 5: Cache management
t.Run("Phase 5: Cache Management", func(t *testing.T) {
testCacheManagement(t, binaryPath, tmpDir)
})
}
func TestStressTestWorkflow(t *testing.T) {
binaryPath := buildTestBinary(t)
defer func() { _ = os.Remove(binaryPath) }()
tmpDir, cleanup := testutil.TempDir(t)
defer cleanup()
// Create many action files to test performance
const numActions = 20
for i := 0; i < numActions; i++ {
actionDir := filepath.Join(tmpDir, "action"+string(rune('A'+i)))
_ = os.MkdirAll(actionDir, 0755)
actionContent := strings.ReplaceAll(testutil.SimpleActionYML, "Simple Action", "Action "+string(rune('A'+i)))
testutil.WriteTestFile(t, filepath.Join(actionDir, "action.yml"), actionContent)
}
// Test recursive processing
cmd := exec.Command(binaryPath, "gen", "--recursive", "--theme", "github")
cmd.Dir = tmpDir
err := cmd.Run()
testutil.AssertNoError(t, err)
// Verify all READMEs were generated
readmeFiles, _ := filepath.Glob(filepath.Join(tmpDir, "**/README*.md"))
if len(readmeFiles) < numActions {
t.Errorf("expected at least %d README files, got %d", numActions, len(readmeFiles))
}
// Test validation of all files
cmd = exec.Command(binaryPath, "validate")
cmd.Dir = tmpDir
err = cmd.Run()
testutil.AssertNoError(t, err)
}
func TestErrorRecoveryWorkflow(t *testing.T) {
binaryPath := buildTestBinary(t)
defer func() { _ = os.Remove(binaryPath) }()
tmpDir, cleanup := testutil.TempDir(t)
defer cleanup()
// Create a project with mixed valid and invalid files
testutil.WriteTestFile(t, filepath.Join(tmpDir, "valid-action.yml"), testutil.SimpleActionYML)
testutil.WriteTestFile(t, filepath.Join(tmpDir, "invalid-action.yml"), testutil.InvalidActionYML)
subDir := filepath.Join(tmpDir, "subdir")
_ = os.MkdirAll(subDir, 0755)
testutil.WriteTestFile(t, filepath.Join(subDir, "another-valid.yml"), testutil.MinimalActionYML)
// Test that validation reports issues but doesn't crash
cmd := exec.Command(binaryPath, "validate")
cmd.Dir = tmpDir
var stderr strings.Builder
cmd.Stderr = &stderr
err := cmd.Run()
// Validation should fail due to invalid file
if err == nil {
t.Error("expected validation to fail with invalid files")
}
// But it should still report on valid files
output := stderr.String()
if !strings.Contains(output, "Missing required field") {
t.Error("expected validation error message")
}
// Test generation with mixed files - should generate docs for valid ones
cmd = exec.Command(binaryPath, "gen", "--recursive")
cmd.Dir = tmpDir
cmd.Stderr = &stderr
_ = cmd.Run()
// Generation might fail due to invalid files, but check what was generated
readmeFiles, _ := filepath.Glob(filepath.Join(tmpDir, "**/README*.md"))
// Should have generated at least some READMEs for valid files
if len(readmeFiles) == 0 {
t.Log("No READMEs generated, which might be expected with invalid files")
}
}
func TestConfigurationWorkflow(t *testing.T) {
binaryPath := buildTestBinary(t)
defer func() { _ = os.Remove(binaryPath) }()
tmpDir, cleanup := testutil.TempDir(t)
defer cleanup()
// Set up XDG config environment
configHome := filepath.Join(tmpDir, "config")
_ = os.Setenv("XDG_CONFIG_HOME", configHome)
defer func() { _ = os.Unsetenv("XDG_CONFIG_HOME") }()
testutil.WriteTestFile(t, filepath.Join(tmpDir, "action.yml"), testutil.SimpleActionYML)
var err error
// Test configuration initialization
cmd := exec.Command(binaryPath, "config", "init")
cmd.Dir = tmpDir
_ = cmd.Run()
// This might fail if config already exists, which is fine
// Test showing configuration
cmd = exec.Command(binaryPath, "config", "show")
cmd.Dir = tmpDir
var stdout strings.Builder
cmd.Stdout = &stdout
err = cmd.Run()
testutil.AssertNoError(t, err)
if !strings.Contains(stdout.String(), "Current Configuration") {
t.Error("expected configuration output")
}
// Test with different configuration options
cmd = exec.Command(binaryPath, "--verbose", "gen")
cmd.Dir = tmpDir
err = cmd.Run()
testutil.AssertNoError(t, err)
cmd = exec.Command(binaryPath, "--quiet", "gen")
cmd.Dir = tmpDir
err = cmd.Run()
testutil.AssertNoError(t, err)
}