mirror of
https://github.com/ivuorinen/gh-action-readme.git
synced 2026-02-02 22:44:20 +00:00
* feat(deps): update go version, renovate config, tooling * chore(deps): update google/go-github to v74 * feat(deps): migrate from yaml.v3 to goccy/go-yaml * chore(deps): update goccy/go-yaml to v1.18.0 and address security concerns * feat: improve issue templates and project configuration - Update GitHub issue templates with CLI-specific fields for better bug reports - Add specialized templates for documentation, theme, and performance issues - Update pre-commit config to include comprehensive documentation linting - Remove outdated Snyk configuration and security references - Update Go version from 1.23+ to 1.24+ across project - Streamline README.md organization and improve clarity - Update CHANGELOG.md and CLAUDE.md formatting - Create comprehensive CONTRIBUTING.md with development guidelines - Remove TODO.md (replaced by docs/roadmap.md) - Move SECURITY.md to docs/security.md * docs: fix markdown linting violations across documentation * fix: resolve template placeholder issues and improve uses statement generation * fix: remove trailing whitespace from GitHub issue template
1065 lines
26 KiB
Go
1065 lines
26 KiB
Go
// Package testutil provides table-driven testing infrastructure and test helpers for gh-action-readme.
|
|
package testutil
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/google/go-github/v74/github"
|
|
)
|
|
|
|
// File constants.
|
|
const (
|
|
readmeFilename = "README.md"
|
|
)
|
|
|
|
// TestExecutor is a function type for executing specific types of tests.
|
|
type TestExecutor func(t *testing.T, testCase TestCase, ctx *TestContext) *TestResult
|
|
|
|
// TestCase represents a parameterized test case.
|
|
type TestCase struct {
|
|
Name string
|
|
Description string
|
|
Fixture string
|
|
Config *TestConfig
|
|
Mocks *MockConfig
|
|
Expected *ExpectedResult
|
|
SetupFunc func(*testing.T, *TestContext) error
|
|
CleanupFunc func(*testing.T, *TestContext) error
|
|
SkipReason string
|
|
Executor TestExecutor // Custom test execution logic
|
|
}
|
|
|
|
// TestSuite represents a collection of related test cases.
|
|
type TestSuite struct {
|
|
Name string
|
|
Description string
|
|
Cases []TestCase
|
|
GlobalSetup func(*testing.T) (*TestContext, error)
|
|
GlobalCleanup func(*testing.T, *TestContext) error
|
|
Parallel bool
|
|
}
|
|
|
|
// TestConfig holds configuration options for a test case.
|
|
type TestConfig struct {
|
|
Theme string
|
|
OutputFormat string
|
|
OutputDir string
|
|
Verbose bool
|
|
Quiet bool
|
|
Recursive bool
|
|
Validate bool
|
|
ExtraFlags map[string]string
|
|
}
|
|
|
|
// MockConfig specifies which mocks to set up for a test case.
|
|
type MockConfig struct {
|
|
GitHubClient bool
|
|
GitHubResponses map[string]string
|
|
ColoredOutput bool
|
|
FileSystem bool
|
|
Environment map[string]string
|
|
TempDir bool
|
|
}
|
|
|
|
// ExpectedResult defines what a test case should produce.
|
|
type ExpectedResult struct {
|
|
ShouldSucceed bool
|
|
ShouldFail bool
|
|
ExpectedError string
|
|
ExpectedOutput []string
|
|
ExpectedFiles []string
|
|
ExpectedExitCode int
|
|
CustomValidation func(*testing.T, *TestResult) error
|
|
}
|
|
|
|
// TestContext holds the context for a running test.
|
|
type TestContext struct {
|
|
TempDir string
|
|
FixtureManager *FixtureManager
|
|
Mocks *MockSuite
|
|
Config *TestConfig
|
|
Cleanup []func() error
|
|
}
|
|
|
|
// TestResult holds the results of a test execution.
|
|
type TestResult struct {
|
|
Success bool
|
|
Error error
|
|
Output string
|
|
Files []string
|
|
ExitCode int
|
|
Duration int64
|
|
Context *TestContext
|
|
}
|
|
|
|
// MockSuite holds all configured mocks for a test.
|
|
type MockSuite struct {
|
|
GitHubClient *github.Client
|
|
ColoredOutput *MockColoredOutput
|
|
HTTPClient *MockHTTPClient
|
|
Environment map[string]string
|
|
TempDirs []string
|
|
}
|
|
|
|
// ActionTestCase represents a test case specifically for action-related testing.
|
|
type ActionTestCase struct {
|
|
TestCase
|
|
ActionType ActionType
|
|
ExpectValid bool
|
|
ExpectAnalysis bool
|
|
ExpectDeps int
|
|
}
|
|
|
|
// GeneratorTestCase represents a test case for generator testing.
|
|
type GeneratorTestCase struct {
|
|
TestCase
|
|
Theme string
|
|
OutputFormat string
|
|
ExpectFiles []string
|
|
ExpectContent map[string][]string
|
|
}
|
|
|
|
// ValidationTestCase represents a test case for validation testing.
|
|
type ValidationTestCase struct {
|
|
TestCase
|
|
ExpectValid bool
|
|
ExpectedErrors []string
|
|
ValidationLevel string
|
|
}
|
|
|
|
// RunTestSuite executes a complete test suite with common setup/teardown.
|
|
func RunTestSuite(t *testing.T, suite TestSuite) {
|
|
t.Helper()
|
|
|
|
if suite.Name == "" {
|
|
t.Fatal("test suite must have a name")
|
|
}
|
|
|
|
t.Run(suite.Name, func(t *testing.T) {
|
|
if suite.Parallel {
|
|
t.Parallel()
|
|
}
|
|
|
|
globalContext := setupGlobalContext(t, suite)
|
|
runAllTestCases(t, suite, globalContext)
|
|
})
|
|
}
|
|
|
|
// setupGlobalContext handles global setup and cleanup for test suite.
|
|
func setupGlobalContext(t *testing.T, suite TestSuite) *TestContext {
|
|
t.Helper()
|
|
|
|
var globalContext *TestContext
|
|
if suite.GlobalSetup != nil {
|
|
var err error
|
|
globalContext, err = suite.GlobalSetup(t)
|
|
if err != nil {
|
|
t.Fatalf("global setup failed: %v", err)
|
|
}
|
|
}
|
|
|
|
// Set up global cleanup
|
|
if suite.GlobalCleanup != nil && globalContext != nil {
|
|
t.Cleanup(func() {
|
|
if err := suite.GlobalCleanup(t, globalContext); err != nil {
|
|
t.Errorf("global cleanup failed: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
return globalContext
|
|
}
|
|
|
|
// runAllTestCases executes all test cases in the suite.
|
|
func runAllTestCases(t *testing.T, suite TestSuite, globalContext *TestContext) {
|
|
t.Helper()
|
|
|
|
for _, testCase := range suite.Cases {
|
|
testCase := testCase // capture loop variable
|
|
|
|
if testCase.SkipReason != "" {
|
|
runSkippedTest(t, testCase)
|
|
|
|
continue
|
|
}
|
|
|
|
runIndividualTest(t, suite, testCase, globalContext)
|
|
}
|
|
}
|
|
|
|
// runSkippedTest handles skipped test cases.
|
|
func runSkippedTest(t *testing.T, testCase TestCase) {
|
|
t.Helper()
|
|
|
|
t.Run(testCase.Name, func(t *testing.T) {
|
|
t.Skip(testCase.SkipReason)
|
|
})
|
|
}
|
|
|
|
// runIndividualTest executes a single test case.
|
|
func runIndividualTest(t *testing.T, suite TestSuite, testCase TestCase, globalContext *TestContext) {
|
|
t.Helper()
|
|
|
|
t.Run(testCase.Name, func(t *testing.T) {
|
|
if suite.Parallel {
|
|
t.Parallel()
|
|
}
|
|
|
|
runTestCase(t, testCase, globalContext)
|
|
})
|
|
}
|
|
|
|
// runTestCase executes a single test case.
|
|
func runTestCase(t *testing.T, testCase TestCase, globalContext *TestContext) {
|
|
t.Helper()
|
|
|
|
// Create test context
|
|
ctx := createTestContext(t, testCase, globalContext)
|
|
|
|
// Setup test-specific cleanup
|
|
defer func() {
|
|
for i := len(ctx.Cleanup) - 1; i >= 0; i-- {
|
|
if err := ctx.Cleanup[i](); err != nil {
|
|
t.Errorf("cleanup failed: %v", err)
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Run test case setup
|
|
if testCase.SetupFunc != nil {
|
|
if err := testCase.SetupFunc(t, ctx); err != nil {
|
|
t.Fatalf("test setup failed: %v", err)
|
|
}
|
|
}
|
|
|
|
// Execute the test
|
|
result := executeTest(t, testCase, ctx)
|
|
|
|
// Validate results
|
|
validateTestResult(t, testCase, result)
|
|
|
|
// Run test case cleanup
|
|
if testCase.CleanupFunc != nil {
|
|
if err := testCase.CleanupFunc(t, ctx); err != nil {
|
|
t.Errorf("test cleanup failed: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// createTestContext creates a test context for a test case.
|
|
func createTestContext(t *testing.T, testCase TestCase, globalContext *TestContext) *TestContext {
|
|
t.Helper()
|
|
|
|
ctx := &TestContext{
|
|
FixtureManager: GetFixtureManager(),
|
|
Config: testCase.Config,
|
|
Cleanup: make([]func() error, 0),
|
|
}
|
|
|
|
// Inherit from global context if available
|
|
if globalContext != nil {
|
|
if ctx.Config == nil {
|
|
ctx.Config = globalContext.Config
|
|
}
|
|
if globalContext.TempDir != "" {
|
|
ctx.TempDir = globalContext.TempDir
|
|
}
|
|
}
|
|
|
|
// Set up temporary directory if needed
|
|
if testCase.Mocks != nil && testCase.Mocks.TempDir {
|
|
tempDir, cleanup := TempDir(t)
|
|
ctx.TempDir = tempDir
|
|
ctx.Cleanup = append(ctx.Cleanup, func() error {
|
|
cleanup()
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// Set up mocks
|
|
if testCase.Mocks != nil {
|
|
ctx.Mocks = createMockSuite(t, testCase.Mocks)
|
|
}
|
|
|
|
return ctx
|
|
}
|
|
|
|
// createMockSuite creates a mock suite based on configuration.
|
|
func createMockSuite(t *testing.T, config *MockConfig) *MockSuite {
|
|
t.Helper()
|
|
|
|
suite := &MockSuite{
|
|
Environment: make(map[string]string),
|
|
TempDirs: make([]string, 0),
|
|
}
|
|
|
|
// Set up GitHub client mock
|
|
if config.GitHubClient {
|
|
responses := config.GitHubResponses
|
|
if responses == nil {
|
|
responses = MockGitHubResponses()
|
|
}
|
|
suite.GitHubClient = MockGitHubClient(responses)
|
|
}
|
|
|
|
// Set up colored output mock
|
|
if config.ColoredOutput {
|
|
suite.ColoredOutput = &MockColoredOutput{
|
|
Messages: make([]string, 0),
|
|
}
|
|
}
|
|
|
|
// Set up HTTP client mock
|
|
if config.GitHubClient {
|
|
suite.HTTPClient = &MockHTTPClient{
|
|
Responses: make(map[string]*http.Response),
|
|
Requests: make([]*http.Request, 0),
|
|
}
|
|
}
|
|
|
|
// Set up environment variables
|
|
for key, value := range config.Environment {
|
|
suite.Environment[key] = value
|
|
}
|
|
|
|
return suite
|
|
}
|
|
|
|
// executeTest runs the actual test logic.
|
|
func executeTest(t *testing.T, testCase TestCase, ctx *TestContext) *TestResult {
|
|
t.Helper()
|
|
|
|
// Use custom executor if provided
|
|
if testCase.Executor != nil {
|
|
return testCase.Executor(t, testCase, ctx)
|
|
}
|
|
|
|
// Default execution: just create fixture and return success
|
|
result := &TestResult{
|
|
Context: ctx,
|
|
}
|
|
|
|
// 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")
|
|
WriteTestFile(t, actionPath, fixture.Content)
|
|
}
|
|
|
|
// Default success for non-generator tests
|
|
result.Success = true
|
|
|
|
return result
|
|
}
|
|
|
|
// validateTestResult validates the test results against expectations.
|
|
func validateTestResult(t *testing.T, testCase TestCase, result *TestResult) {
|
|
t.Helper()
|
|
|
|
if testCase.Expected == nil {
|
|
return
|
|
}
|
|
|
|
expected := testCase.Expected
|
|
validateSuccessFailure(t, expected, result)
|
|
validateError(t, expected, result)
|
|
validateOutput(t, expected, result)
|
|
validateFiles(t, expected, result)
|
|
validateExitCode(t, expected, result)
|
|
validateCustom(t, expected, result)
|
|
}
|
|
|
|
// validateSuccessFailure checks success/failure expectations.
|
|
func validateSuccessFailure(t *testing.T, expected *ExpectedResult, result *TestResult) {
|
|
t.Helper()
|
|
|
|
if expected.ShouldSucceed && !result.Success {
|
|
t.Errorf("expected test to succeed, but it failed: %v", result.Error)
|
|
}
|
|
|
|
if expected.ShouldFail && result.Success {
|
|
t.Error("expected test to fail, but it succeeded")
|
|
}
|
|
}
|
|
|
|
// validateError checks expected error conditions.
|
|
func validateError(t *testing.T, expected *ExpectedResult, result *TestResult) {
|
|
t.Helper()
|
|
|
|
if expected.ExpectedError == "" {
|
|
return
|
|
}
|
|
|
|
if result.Error == nil {
|
|
t.Errorf("expected error %q, but got no error", expected.ExpectedError)
|
|
|
|
return
|
|
}
|
|
|
|
if result.Error.Error() != expected.ExpectedError {
|
|
t.Errorf("expected error %q, but got %q", expected.ExpectedError, result.Error.Error())
|
|
}
|
|
}
|
|
|
|
// validateOutput checks expected output conditions.
|
|
func validateOutput(t *testing.T, expected *ExpectedResult, result *TestResult) {
|
|
t.Helper()
|
|
|
|
for _, expectedOutput := range expected.ExpectedOutput {
|
|
if !containsString(result.Output, expectedOutput) {
|
|
t.Errorf("expected output to contain %q, but it didn't. Output: %s", expectedOutput, result.Output)
|
|
}
|
|
}
|
|
}
|
|
|
|
// validateFiles checks expected file conditions.
|
|
func validateFiles(t *testing.T, expected *ExpectedResult, result *TestResult) {
|
|
t.Helper()
|
|
|
|
for _, expectedFile := range expected.ExpectedFiles {
|
|
if strings.Contains(expectedFile, "*") {
|
|
// Handle wildcard patterns like "*.html"
|
|
found := false
|
|
pattern := strings.TrimPrefix(expectedFile, "*")
|
|
for _, actualFile := range result.Files {
|
|
if strings.HasSuffix(actualFile, pattern) {
|
|
found = true
|
|
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf(
|
|
"expected file matching pattern %q to be created, but none found. Files: %v",
|
|
expectedFile,
|
|
result.Files,
|
|
)
|
|
}
|
|
} else if !containsString(result.Files, expectedFile) {
|
|
// Handle exact filename matches
|
|
t.Errorf("expected file %q to be created, but it wasn't. Files: %v", expectedFile, result.Files)
|
|
}
|
|
}
|
|
}
|
|
|
|
// validateExitCode checks expected exit code.
|
|
func validateExitCode(t *testing.T, expected *ExpectedResult, result *TestResult) {
|
|
t.Helper()
|
|
|
|
if expected.ExpectedExitCode != 0 && result.ExitCode != expected.ExpectedExitCode {
|
|
t.Errorf("expected exit code %d, but got %d", expected.ExpectedExitCode, result.ExitCode)
|
|
}
|
|
}
|
|
|
|
// validateCustom runs custom validation if provided.
|
|
func validateCustom(t *testing.T, expected *ExpectedResult, result *TestResult) {
|
|
t.Helper()
|
|
|
|
if expected.CustomValidation == nil {
|
|
return
|
|
}
|
|
|
|
if err := expected.CustomValidation(t, result); err != nil {
|
|
t.Errorf("custom validation failed: %v", err)
|
|
}
|
|
}
|
|
|
|
// Helper functions for specific test types
|
|
|
|
// RunActionTests executes action-related test cases.
|
|
func RunActionTests(t *testing.T, cases []ActionTestCase) {
|
|
t.Helper()
|
|
|
|
testCases := make([]TestCase, len(cases))
|
|
for i, actionCase := range cases {
|
|
testCases[i] = actionCase.TestCase
|
|
}
|
|
|
|
suite := TestSuite{
|
|
Name: "Action Tests",
|
|
Cases: testCases,
|
|
Parallel: true,
|
|
}
|
|
|
|
RunTestSuite(t, suite)
|
|
}
|
|
|
|
// RunGeneratorTests executes generator test cases.
|
|
func RunGeneratorTests(t *testing.T, cases []GeneratorTestCase) {
|
|
t.Helper()
|
|
|
|
testCases := make([]TestCase, len(cases))
|
|
for i, genCase := range cases {
|
|
testCases[i] = genCase.TestCase
|
|
}
|
|
|
|
suite := TestSuite{
|
|
Name: "Generator Tests",
|
|
Cases: testCases,
|
|
Parallel: true,
|
|
}
|
|
|
|
RunTestSuite(t, suite)
|
|
}
|
|
|
|
// RunValidationTests executes validation test cases.
|
|
func RunValidationTests(t *testing.T, cases []ValidationTestCase) {
|
|
t.Helper()
|
|
|
|
testCases := make([]TestCase, len(cases))
|
|
for i, valCase := range cases {
|
|
testCases[i] = valCase.TestCase
|
|
}
|
|
|
|
suite := TestSuite{
|
|
Name: "Validation Tests",
|
|
Cases: testCases,
|
|
Parallel: true,
|
|
}
|
|
|
|
RunTestSuite(t, suite)
|
|
}
|
|
|
|
// Utility functions
|
|
|
|
// containsString checks if a slice contains a string.
|
|
func containsString(slice any, item string) bool {
|
|
switch s := slice.(type) {
|
|
case []string:
|
|
for _, v := range s {
|
|
if v == item {
|
|
return true
|
|
}
|
|
}
|
|
case string:
|
|
return len(s) > 0 && s == item
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// DetectGeneratedFiles finds files that were generated in the output directory.
|
|
// This is exported so tests in other packages can use it.
|
|
func DetectGeneratedFiles(outputDir string, outputFormat string) []string {
|
|
var files []string
|
|
|
|
// Different output formats create different files:
|
|
// - md: README.md
|
|
// - html: <action-name>.html (name varies)
|
|
// - json: action-docs.json
|
|
// - asciidoc: README.adoc
|
|
|
|
entries, err := os.ReadDir(outputDir)
|
|
if err != nil {
|
|
return files
|
|
}
|
|
|
|
for _, entry := range entries {
|
|
if !entry.IsDir() {
|
|
name := entry.Name()
|
|
// Skip the action.yml we created for testing
|
|
if name == "action.yml" {
|
|
continue
|
|
}
|
|
|
|
// Check if this file matches the expected output format
|
|
isGenerated := false
|
|
switch outputFormat {
|
|
case "md":
|
|
isGenerated = name == readmeFilename
|
|
case "html":
|
|
isGenerated = strings.HasSuffix(name, ".html")
|
|
case "json":
|
|
isGenerated = name == "action-docs.json"
|
|
case "asciidoc":
|
|
isGenerated = name == "README.adoc"
|
|
default:
|
|
isGenerated = name == readmeFilename
|
|
}
|
|
|
|
if isGenerated {
|
|
files = append(files, name)
|
|
}
|
|
}
|
|
}
|
|
|
|
return files
|
|
}
|
|
|
|
// DefaultTestConfig returns a default test configuration.
|
|
func DefaultTestConfig() *TestConfig {
|
|
return &TestConfig{
|
|
Theme: "default",
|
|
OutputFormat: "md",
|
|
OutputDir: ".",
|
|
Verbose: false,
|
|
Quiet: false,
|
|
Recursive: false,
|
|
Validate: true,
|
|
ExtraFlags: make(map[string]string),
|
|
}
|
|
}
|
|
|
|
// DefaultMockConfig returns a default mock configuration.
|
|
func DefaultMockConfig() *MockConfig {
|
|
return &MockConfig{
|
|
GitHubClient: true,
|
|
GitHubResponses: MockGitHubResponses(),
|
|
ColoredOutput: true,
|
|
FileSystem: false,
|
|
Environment: make(map[string]string),
|
|
TempDir: true,
|
|
}
|
|
}
|
|
|
|
// Advanced test environment and helper functions (consolidated from helpers.go)
|
|
|
|
// EnvironmentConfig configures a test environment.
|
|
type EnvironmentConfig struct {
|
|
ActionFixtures []string
|
|
ConfigFixture string
|
|
WithMocks bool
|
|
ExtraFiles map[string]string
|
|
}
|
|
|
|
// TestEnvironment represents a complete test environment.
|
|
type TestEnvironment struct {
|
|
TempDir string
|
|
ActionPaths []string
|
|
ConfigPath string
|
|
FixtureManager *FixtureManager
|
|
Mocks *MockSuite
|
|
Cleanup []func() error
|
|
}
|
|
|
|
// CreateTemporaryAction creates a temporary action file for testing.
|
|
func CreateTemporaryAction(t *testing.T, fixture string) string {
|
|
t.Helper()
|
|
|
|
// Load the fixture
|
|
actionFixture, err := LoadActionFixture(fixture)
|
|
if err != nil {
|
|
t.Fatalf("failed to load action fixture %s: %v", fixture, err)
|
|
}
|
|
|
|
// Create temporary directory
|
|
tempDir, cleanup := TempDir(t)
|
|
t.Cleanup(cleanup)
|
|
|
|
// Write action file
|
|
actionPath := filepath.Join(tempDir, "action.yml")
|
|
WriteTestFile(t, actionPath, actionFixture.Content)
|
|
|
|
return actionPath
|
|
}
|
|
|
|
// CreateTemporaryActionDir creates a temporary directory with an action file.
|
|
func CreateTemporaryActionDir(t *testing.T, fixture string) string {
|
|
t.Helper()
|
|
|
|
// Load the fixture
|
|
actionFixture, err := LoadActionFixture(fixture)
|
|
if err != nil {
|
|
t.Fatalf("failed to load action fixture %s: %v", fixture, err)
|
|
}
|
|
|
|
// Create temporary directory
|
|
tempDir, cleanup := TempDir(t)
|
|
t.Cleanup(cleanup)
|
|
|
|
// Write action file
|
|
actionPath := filepath.Join(tempDir, "action.yml")
|
|
WriteTestFile(t, actionPath, actionFixture.Content)
|
|
|
|
return tempDir
|
|
}
|
|
|
|
// CreateTestEnvironment sets up a complete test environment.
|
|
func CreateTestEnvironment(t *testing.T, config *EnvironmentConfig) *TestEnvironment {
|
|
t.Helper()
|
|
|
|
if config == nil {
|
|
config = &EnvironmentConfig{}
|
|
}
|
|
|
|
env := &TestEnvironment{
|
|
TempDir: "",
|
|
ActionPaths: make([]string, 0),
|
|
ConfigPath: "",
|
|
FixtureManager: GetFixtureManager(),
|
|
Cleanup: make([]func() error, 0),
|
|
}
|
|
|
|
// Create temporary directory
|
|
tempDir, cleanup := TempDir(t)
|
|
env.TempDir = tempDir
|
|
env.Cleanup = append(env.Cleanup, func() error {
|
|
cleanup()
|
|
|
|
return nil
|
|
})
|
|
|
|
// Create action files from fixtures
|
|
for _, fixture := range config.ActionFixtures {
|
|
actionPath := CreateTemporaryAction(t, fixture)
|
|
env.ActionPaths = append(env.ActionPaths, actionPath)
|
|
}
|
|
|
|
// Create config file if specified
|
|
if config.ConfigFixture != "" {
|
|
configFixture, err := LoadConfigFixture(config.ConfigFixture)
|
|
if err != nil {
|
|
t.Fatalf("failed to load config fixture %s: %v", config.ConfigFixture, err)
|
|
}
|
|
|
|
configPath := filepath.Join(tempDir, "config.yml")
|
|
WriteTestFile(t, configPath, configFixture.Content)
|
|
env.ConfigPath = configPath
|
|
}
|
|
|
|
// Set up mocks if requested
|
|
if config.WithMocks {
|
|
env.Mocks = CreateMockSuite(DefaultMockConfig())
|
|
}
|
|
|
|
return env
|
|
}
|
|
|
|
// CreateMockSuite creates a complete mock environment (enhanced from helpers.go).
|
|
func CreateMockSuite(config *MockConfig) *MockSuite {
|
|
if config == nil {
|
|
config = DefaultMockConfig()
|
|
}
|
|
|
|
suite := &MockSuite{
|
|
Environment: make(map[string]string),
|
|
TempDirs: make([]string, 0),
|
|
}
|
|
|
|
// Set up GitHub client mock
|
|
if config.GitHubClient {
|
|
responses := config.GitHubResponses
|
|
if responses == nil {
|
|
responses = MockGitHubResponses()
|
|
}
|
|
suite.GitHubClient = MockGitHubClient(responses)
|
|
}
|
|
|
|
// Set up colored output mock
|
|
if config.ColoredOutput {
|
|
suite.ColoredOutput = &MockColoredOutput{
|
|
Messages: make([]string, 0),
|
|
}
|
|
}
|
|
|
|
// Set up HTTP client mock
|
|
if config.GitHubClient {
|
|
suite.HTTPClient = &MockHTTPClient{
|
|
Responses: make(map[string]*http.Response),
|
|
Requests: make([]*http.Request, 0),
|
|
}
|
|
}
|
|
|
|
// Set up environment variables
|
|
for key, value := range config.Environment {
|
|
suite.Environment[key] = value
|
|
}
|
|
|
|
return suite
|
|
}
|
|
|
|
// SetupGitHubMocks configures GitHub API mocks for test scenarios.
|
|
func SetupGitHubMocks(scenarios []string) map[string]string {
|
|
responses := MockGitHubResponses()
|
|
|
|
// Add scenario-specific responses
|
|
for _, scenario := range scenarios {
|
|
switch scenario {
|
|
case "rate-limit":
|
|
responses["GET https://api.github.com/rate_limit"] = GitHubRateLimitResponse
|
|
case "not-found":
|
|
responses["GET https://api.github.com/repos/nonexistent/repo"] = GitHubErrorResponse
|
|
case "latest-release":
|
|
responses["GET https://api.github.com/repos/actions/checkout/releases/latest"] = GitHubReleaseResponse
|
|
}
|
|
}
|
|
|
|
return responses
|
|
}
|
|
|
|
// ValidateActionFixture validates that a fixture contains expected content.
|
|
func ValidateActionFixture(t *testing.T, fixture *ActionFixture) {
|
|
t.Helper()
|
|
|
|
if fixture == nil {
|
|
t.Fatal("fixture is nil")
|
|
}
|
|
|
|
if fixture.Content == "" {
|
|
t.Error("fixture content is empty")
|
|
}
|
|
|
|
if fixture.Name == "" {
|
|
t.Error("fixture name is empty")
|
|
}
|
|
|
|
// Basic YAML validation is done in the fixture loader
|
|
if !fixture.IsValid && fixture.Scenario != nil && fixture.Scenario.ExpectValid {
|
|
t.Errorf("fixture %s should be valid according to scenario but validation failed", fixture.Name)
|
|
}
|
|
}
|
|
|
|
// TestAllThemes runs a test with all available themes.
|
|
func TestAllThemes(t *testing.T, testFunc func(*testing.T, string)) {
|
|
t.Helper()
|
|
|
|
themes := []string{"default", "github", "minimal", "professional"}
|
|
|
|
for _, theme := range themes {
|
|
theme := theme // capture loop variable
|
|
t.Run("theme_"+theme, func(t *testing.T) {
|
|
t.Parallel()
|
|
testFunc(t, theme)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestAllFormats runs a test with all available output formats.
|
|
func TestAllFormats(t *testing.T, testFunc func(*testing.T, string)) {
|
|
t.Helper()
|
|
|
|
formats := []string{"md", "html", "json", "asciidoc"}
|
|
|
|
for _, format := range formats {
|
|
format := format // capture loop variable
|
|
t.Run("format_"+format, func(t *testing.T) {
|
|
t.Parallel()
|
|
testFunc(t, format)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestValidationScenarios runs validation tests for all invalid fixtures.
|
|
func TestValidationScenarios(t *testing.T, validatorFunc func(*testing.T, string) error) {
|
|
t.Helper()
|
|
|
|
invalidFixtures := GetInvalidFixtures()
|
|
|
|
for _, fixture := range invalidFixtures {
|
|
fixture := fixture // capture loop variable
|
|
t.Run("invalid_"+strings.ReplaceAll(fixture, "/", "_"), func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
err := validatorFunc(t, fixture)
|
|
if err == nil {
|
|
t.Errorf("expected validation error for fixture %s, but got none", fixture)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// CreateGitHubMockSuite creates a GitHub API mock suite.
|
|
func CreateGitHubMockSuite(scenarios []string) *MockSuite {
|
|
config := &MockConfig{
|
|
GitHubClient: true,
|
|
GitHubResponses: SetupGitHubMocks(scenarios),
|
|
ColoredOutput: true,
|
|
Environment: make(map[string]string),
|
|
TempDir: true,
|
|
}
|
|
|
|
return CreateMockSuite(config)
|
|
}
|
|
|
|
// AssertFixtureValid asserts that a fixture should be valid.
|
|
func AssertFixtureValid(t *testing.T, fixtureName string) {
|
|
t.Helper()
|
|
|
|
fixture, err := LoadActionFixture(fixtureName)
|
|
AssertNoError(t, err)
|
|
|
|
if !fixture.IsValid {
|
|
t.Errorf("fixture %s should be valid but failed validation", fixtureName)
|
|
}
|
|
|
|
ValidateActionFixture(t, fixture)
|
|
}
|
|
|
|
// AssertFixtureInvalid asserts that a fixture should be invalid.
|
|
func AssertFixtureInvalid(t *testing.T, fixtureName string) {
|
|
t.Helper()
|
|
|
|
fixture, err := LoadActionFixture(fixtureName)
|
|
|
|
// The fixture might load but be marked as invalid
|
|
if err == nil && fixture.IsValid {
|
|
t.Errorf("fixture %s should be invalid but passed validation", fixtureName)
|
|
}
|
|
}
|
|
|
|
// CreateActionTestCases creates test cases for action-related testing.
|
|
func CreateActionTestCases() []ActionTestCase {
|
|
validFixtures := GetValidFixtures()
|
|
invalidFixtures := GetInvalidFixtures()
|
|
|
|
cases := make([]ActionTestCase, 0, len(validFixtures)+len(invalidFixtures))
|
|
|
|
// Add valid fixture test cases
|
|
for _, fixture := range validFixtures {
|
|
actionFixture, err := LoadActionFixture(fixture)
|
|
if err != nil {
|
|
continue // Skip fixtures that can't be loaded
|
|
}
|
|
|
|
cases = append(cases, ActionTestCase{
|
|
TestCase: TestCase{
|
|
Name: "valid_" + strings.ReplaceAll(fixture, "/", "_"),
|
|
Description: "Test valid action fixture: " + fixture,
|
|
Fixture: fixture,
|
|
Config: DefaultTestConfig(),
|
|
Mocks: DefaultMockConfig(),
|
|
Expected: &ExpectedResult{
|
|
ShouldSucceed: true,
|
|
ShouldFail: false,
|
|
},
|
|
},
|
|
ActionType: actionFixture.ActionType,
|
|
ExpectValid: true,
|
|
ExpectAnalysis: true,
|
|
})
|
|
}
|
|
|
|
// Add invalid fixture test cases
|
|
for _, fixture := range invalidFixtures {
|
|
actionFixture, _ := LoadActionFixture(fixture) // Might fail, that's ok
|
|
actionType := ActionTypeInvalid
|
|
if actionFixture != nil {
|
|
actionType = actionFixture.ActionType
|
|
}
|
|
|
|
cases = append(cases, ActionTestCase{
|
|
TestCase: TestCase{
|
|
Name: "invalid_" + strings.ReplaceAll(fixture, "/", "_"),
|
|
Description: "Test invalid action fixture: " + fixture,
|
|
Fixture: fixture,
|
|
Config: DefaultTestConfig(),
|
|
Mocks: DefaultMockConfig(),
|
|
Expected: &ExpectedResult{
|
|
ShouldSucceed: false,
|
|
ShouldFail: true,
|
|
},
|
|
},
|
|
ActionType: actionType,
|
|
ExpectValid: false,
|
|
ExpectAnalysis: false,
|
|
})
|
|
}
|
|
|
|
return cases
|
|
}
|
|
|
|
// getExpectedFilename returns the expected filename for a given output format.
|
|
func getExpectedFilename(outputFormat string) string {
|
|
switch outputFormat {
|
|
case "md":
|
|
return "README.md"
|
|
case "html":
|
|
// HTML files have variable names based on action name, so we'll use a pattern
|
|
// The DetectGeneratedFiles function will find any .html file
|
|
return "*.html"
|
|
case "json":
|
|
return "action-docs.json"
|
|
case "asciidoc":
|
|
return "README.adoc"
|
|
default:
|
|
return "README.md"
|
|
}
|
|
}
|
|
|
|
// CreateGeneratorTestCases creates test cases for generator testing.
|
|
func CreateGeneratorTestCases() []GeneratorTestCase {
|
|
validFixtures := GetValidFixtures()
|
|
themes := []string{"default", "github", "minimal", "professional"}
|
|
formats := []string{"md", "html", "json", "asciidoc"}
|
|
|
|
cases := make([]GeneratorTestCase, 0)
|
|
|
|
// Create test cases for each valid fixture with each theme/format combination
|
|
for _, fixture := range validFixtures {
|
|
for _, theme := range themes {
|
|
for _, format := range formats {
|
|
expectedFilename := getExpectedFilename(format)
|
|
|
|
cases = append(cases, GeneratorTestCase{
|
|
TestCase: TestCase{
|
|
Name: fmt.Sprintf("%s_%s_%s", strings.ReplaceAll(fixture, "/", "_"), theme, format),
|
|
Description: fmt.Sprintf(
|
|
"Generate %s documentation for %s using %s theme",
|
|
format,
|
|
fixture,
|
|
theme,
|
|
),
|
|
Fixture: fixture,
|
|
Config: &TestConfig{
|
|
Theme: theme,
|
|
OutputFormat: format,
|
|
OutputDir: ".",
|
|
Verbose: false,
|
|
Quiet: false,
|
|
},
|
|
Mocks: DefaultMockConfig(),
|
|
Expected: &ExpectedResult{
|
|
ShouldSucceed: true,
|
|
ExpectedFiles: []string{expectedFilename},
|
|
},
|
|
},
|
|
Theme: theme,
|
|
OutputFormat: format,
|
|
ExpectFiles: []string{expectedFilename},
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
return cases
|
|
}
|
|
|
|
// CreateValidationTestCases creates test cases for validation testing.
|
|
func CreateValidationTestCases() []ValidationTestCase {
|
|
fm := GetFixtureManager()
|
|
cases := make([]ValidationTestCase, 0)
|
|
|
|
// Add test cases for all scenarios
|
|
for _, scenario := range fm.scenarios {
|
|
cases = append(cases, ValidationTestCase{
|
|
TestCase: TestCase{
|
|
Name: "validate_" + scenario.ID,
|
|
Description: scenario.Description,
|
|
Fixture: scenario.Fixture,
|
|
Config: DefaultTestConfig(),
|
|
Expected: &ExpectedResult{
|
|
ShouldSucceed: scenario.ExpectValid,
|
|
ShouldFail: !scenario.ExpectValid,
|
|
},
|
|
},
|
|
ExpectValid: scenario.ExpectValid,
|
|
ValidationLevel: "standard",
|
|
})
|
|
}
|
|
|
|
return cases
|
|
}
|