mirror of
https://github.com/ivuorinen/gh-action-readme.git
synced 2026-02-06 18:46:28 +00:00
feat(lint): add many linters, make all the tests run fast! (#23)
* 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.
This commit is contained in:
@@ -12,13 +12,40 @@ import (
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// fixtureCache provides thread-safe caching of fixture content.
|
||||
var fixtureCache = struct {
|
||||
mu sync.RWMutex
|
||||
cache map[string]string
|
||||
}{
|
||||
cache: make(map[string]string),
|
||||
}
|
||||
|
||||
// MustReadFixture reads a YAML fixture file from testdata/yaml-fixtures.
|
||||
func MustReadFixture(filename string) string {
|
||||
return mustReadFixture(filename)
|
||||
}
|
||||
|
||||
// mustReadFixture reads a YAML fixture file from testdata/yaml-fixtures.
|
||||
// mustReadFixture reads a YAML fixture file from testdata/yaml-fixtures with caching.
|
||||
func mustReadFixture(filename string) string {
|
||||
// Try to get from cache first (read lock)
|
||||
fixtureCache.mu.RLock()
|
||||
if content, exists := fixtureCache.cache[filename]; exists {
|
||||
fixtureCache.mu.RUnlock()
|
||||
|
||||
return content
|
||||
}
|
||||
fixtureCache.mu.RUnlock()
|
||||
|
||||
// Not in cache, acquire write lock and read from disk
|
||||
fixtureCache.mu.Lock()
|
||||
defer fixtureCache.mu.Unlock()
|
||||
|
||||
// Double-check in case another goroutine loaded it while we were waiting
|
||||
if content, exists := fixtureCache.cache[filename]; exists {
|
||||
return content
|
||||
}
|
||||
|
||||
// Load from disk
|
||||
_, currentFile, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
panic("failed to get current file path")
|
||||
@@ -28,12 +55,17 @@ func mustReadFixture(filename string) string {
|
||||
projectRoot := filepath.Dir(filepath.Dir(currentFile))
|
||||
fixturePath := filepath.Join(projectRoot, "testdata", "yaml-fixtures", filename)
|
||||
|
||||
content, err := os.ReadFile(fixturePath) // #nosec G304 -- test fixture path from project structure
|
||||
contentBytes, err := os.ReadFile(fixturePath) // #nosec G304 -- test fixture path from project structure
|
||||
if err != nil {
|
||||
panic("failed to read fixture " + filename + ": " + err.Error())
|
||||
}
|
||||
|
||||
return string(content)
|
||||
content := string(contentBytes)
|
||||
|
||||
// Store in cache
|
||||
fixtureCache.cache[filename] = content
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
// Constants for fixture management.
|
||||
@@ -316,6 +348,7 @@ var PackageJSONContent = func() string {
|
||||
result += " \"webpack\": \"^5.0.0\"\n"
|
||||
result += " }\n"
|
||||
result += "}\n"
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
@@ -373,6 +406,7 @@ func (fm *FixtureManager) LoadActionFixture(name string) (*ActionFixture, error)
|
||||
fm.mu.RLock()
|
||||
if fixture, exists := fm.cache[name]; exists {
|
||||
fm.mu.RUnlock()
|
||||
|
||||
return fixture, nil
|
||||
}
|
||||
fm.mu.RUnlock()
|
||||
@@ -403,6 +437,7 @@ func (fm *FixtureManager) LoadActionFixture(name string) (*ActionFixture, error)
|
||||
// Double-check cache in case another goroutine cached it while we were loading
|
||||
if cachedFixture, exists := fm.cache[name]; exists {
|
||||
fm.mu.Unlock()
|
||||
|
||||
return cachedFixture, nil
|
||||
}
|
||||
fm.cache[name] = fixture
|
||||
@@ -505,6 +540,7 @@ func (fm *FixtureManager) ensureYamlExtension(path string) string {
|
||||
if !strings.HasSuffix(path, YmlExtension) && !strings.HasSuffix(path, YamlExtension) {
|
||||
path += YmlExtension
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
@@ -524,6 +560,7 @@ func (fm *FixtureManager) searchInDirectories(name string) string {
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -535,6 +572,7 @@ func (fm *FixtureManager) buildSearchPath(dir, name string) string {
|
||||
} else {
|
||||
path = filepath.Join(fm.basePath, dir, name)
|
||||
}
|
||||
|
||||
return fm.ensureYamlExtension(path)
|
||||
}
|
||||
|
||||
@@ -566,6 +604,7 @@ func (fm *FixtureManager) determineActionTypeByName(name string) ActionType {
|
||||
if strings.Contains(name, "minimal") {
|
||||
return ActionTypeMinimal
|
||||
}
|
||||
|
||||
return ActionTypeMinimal
|
||||
}
|
||||
|
||||
@@ -580,6 +619,7 @@ func (fm *FixtureManager) determineActionTypeByContent(content string) ActionTyp
|
||||
if strings.Contains(content, `using: 'node`) {
|
||||
return ActionTypeJavaScript
|
||||
}
|
||||
|
||||
return ActionTypeMinimal
|
||||
}
|
||||
|
||||
@@ -594,6 +634,7 @@ func (fm *FixtureManager) determineConfigType(name string) string {
|
||||
if strings.Contains(name, "user") {
|
||||
return "user-specific"
|
||||
}
|
||||
|
||||
return "generic"
|
||||
}
|
||||
|
||||
@@ -658,12 +699,14 @@ func isValidRuntime(runtime string) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// validateConfigContent validates configuration fixture content.
|
||||
func (fm *FixtureManager) validateConfigContent(content string) bool {
|
||||
var data map[string]any
|
||||
|
||||
return yaml.Unmarshal([]byte(content), &data) == nil
|
||||
}
|
||||
|
||||
@@ -762,6 +805,7 @@ func GetFixtureManager() *FixtureManager {
|
||||
panic(fmt.Sprintf("failed to load test scenarios: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
return defaultFixtureManager
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
const testVersion = "v4.1.1"
|
||||
|
||||
func TestMustReadFixture(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
filename string
|
||||
@@ -32,6 +33,7 @@ func TestMustReadFixture(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if tt.wantErr {
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
@@ -56,7 +58,9 @@ func TestMustReadFixture(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMustReadFixture_Panic(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("missing file panics", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Error("expected panic but got none")
|
||||
@@ -64,6 +68,7 @@ func TestMustReadFixture_Panic(t *testing.T) {
|
||||
errStr, ok := r.(string)
|
||||
if !ok {
|
||||
t.Errorf("expected panic to contain string message, got: %T", r)
|
||||
|
||||
return
|
||||
}
|
||||
if !strings.Contains(errStr, "failed to read fixture") {
|
||||
@@ -77,28 +82,36 @@ func TestMustReadFixture_Panic(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGitHubAPIResponses(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("GitHubReleaseResponse", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testGitHubReleaseResponse(t)
|
||||
})
|
||||
t.Run("GitHubTagResponse", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testGitHubTagResponse(t)
|
||||
})
|
||||
t.Run("GitHubRepoResponse", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testGitHubRepoResponse(t)
|
||||
})
|
||||
t.Run("GitHubCommitResponse", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testGitHubCommitResponse(t)
|
||||
})
|
||||
t.Run("GitHubRateLimitResponse", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testGitHubRateLimitResponse(t)
|
||||
})
|
||||
t.Run("GitHubErrorResponse", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testGitHubErrorResponse(t)
|
||||
})
|
||||
}
|
||||
|
||||
// testGitHubReleaseResponse validates the GitHub release response format.
|
||||
func testGitHubReleaseResponse(t *testing.T) {
|
||||
t.Helper()
|
||||
data := parseJSONResponse(t, GitHubReleaseResponse)
|
||||
|
||||
if data["id"] == nil {
|
||||
@@ -114,6 +127,7 @@ func testGitHubReleaseResponse(t *testing.T) {
|
||||
|
||||
// testGitHubTagResponse validates the GitHub tag response format.
|
||||
func testGitHubTagResponse(t *testing.T) {
|
||||
t.Helper()
|
||||
data := parseJSONResponse(t, GitHubTagResponse)
|
||||
|
||||
if data["name"] != testVersion {
|
||||
@@ -126,6 +140,7 @@ func testGitHubTagResponse(t *testing.T) {
|
||||
|
||||
// testGitHubRepoResponse validates the GitHub repository response format.
|
||||
func testGitHubRepoResponse(t *testing.T) {
|
||||
t.Helper()
|
||||
data := parseJSONResponse(t, GitHubRepoResponse)
|
||||
|
||||
if data["name"] != "checkout" {
|
||||
@@ -138,6 +153,7 @@ func testGitHubRepoResponse(t *testing.T) {
|
||||
|
||||
// testGitHubCommitResponse validates the GitHub commit response format.
|
||||
func testGitHubCommitResponse(t *testing.T) {
|
||||
t.Helper()
|
||||
data := parseJSONResponse(t, GitHubCommitResponse)
|
||||
|
||||
if data["sha"] == nil {
|
||||
@@ -150,6 +166,7 @@ func testGitHubCommitResponse(t *testing.T) {
|
||||
|
||||
// testGitHubRateLimitResponse validates the GitHub rate limit response format.
|
||||
func testGitHubRateLimitResponse(t *testing.T) {
|
||||
t.Helper()
|
||||
data := parseJSONResponse(t, GitHubRateLimitResponse)
|
||||
|
||||
if data["resources"] == nil {
|
||||
@@ -162,6 +179,7 @@ func testGitHubRateLimitResponse(t *testing.T) {
|
||||
|
||||
// testGitHubErrorResponse validates the GitHub error response format.
|
||||
func testGitHubErrorResponse(t *testing.T) {
|
||||
t.Helper()
|
||||
data := parseJSONResponse(t, GitHubErrorResponse)
|
||||
|
||||
if data["message"] != "Not Found" {
|
||||
@@ -171,14 +189,17 @@ func testGitHubErrorResponse(t *testing.T) {
|
||||
|
||||
// parseJSONResponse parses a JSON response string and returns the data map.
|
||||
func parseJSONResponse(t *testing.T, response string) map[string]any {
|
||||
t.Helper()
|
||||
var data map[string]any
|
||||
if err := json.Unmarshal([]byte(response), &data); err != nil {
|
||||
t.Fatalf("failed to parse JSON response: %v", err)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
func TestSimpleTemplate(t *testing.T) {
|
||||
t.Parallel()
|
||||
template := SimpleTemplate
|
||||
|
||||
// Check that template contains expected sections
|
||||
@@ -208,6 +229,7 @@ func TestSimpleTemplate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMockGitHubResponses(t *testing.T) {
|
||||
t.Parallel()
|
||||
responses := MockGitHubResponses()
|
||||
|
||||
// Test that all expected endpoints are present
|
||||
@@ -236,6 +258,7 @@ func TestMockGitHubResponses(t *testing.T) {
|
||||
|
||||
// Test specific response structures
|
||||
t.Run("checkout releases response", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
response := responses["GET https://api.github.com/repos/actions/checkout/releases/latest"]
|
||||
var release map[string]any
|
||||
if err := json.Unmarshal([]byte(response), &release); err != nil {
|
||||
@@ -249,6 +272,7 @@ func TestMockGitHubResponses(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFixtureConstants(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Test that all fixture variables are properly loaded
|
||||
fixtures := map[string]string{
|
||||
"SimpleActionYML": MustReadFixture("actions/javascript/simple.yml"),
|
||||
@@ -263,6 +287,7 @@ func TestFixtureConstants(t *testing.T) {
|
||||
|
||||
for name, content := range fixtures {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if content == "" {
|
||||
t.Errorf("%s is empty", name)
|
||||
}
|
||||
@@ -289,6 +314,7 @@ func TestFixtureConstants(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGitIgnoreContent(t *testing.T) {
|
||||
t.Parallel()
|
||||
content := GitIgnoreContent
|
||||
|
||||
expectedPatterns := []string{
|
||||
@@ -314,6 +340,7 @@ func TestGitIgnoreContent(t *testing.T) {
|
||||
|
||||
// Test helper functions that interact with the filesystem.
|
||||
func TestFixtureFileSystem(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Verify that the fixture files actually exist
|
||||
fixtureFiles := []string{
|
||||
"simple-action.yml",
|
||||
@@ -334,6 +361,7 @@ func TestFixtureFileSystem(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get working directory: %v", err)
|
||||
}
|
||||
|
||||
return filepath.Dir(wd) // Go up from testutil to project root
|
||||
}()
|
||||
|
||||
@@ -341,6 +369,7 @@ func TestFixtureFileSystem(t *testing.T) {
|
||||
|
||||
for _, filename := range fixtureFiles {
|
||||
t.Run(filename, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
path := filepath.Join(fixturesDir, filename)
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
t.Errorf("fixture file does not exist: %s", path)
|
||||
@@ -352,6 +381,7 @@ func TestFixtureFileSystem(t *testing.T) {
|
||||
// Tests for FixtureManager functionality (consolidated from scenarios.go tests)
|
||||
|
||||
func TestNewFixtureManager(t *testing.T) {
|
||||
t.Parallel()
|
||||
fm := NewFixtureManager()
|
||||
if fm == nil {
|
||||
t.Fatal("expected fixture manager to be created")
|
||||
@@ -371,6 +401,7 @@ func TestNewFixtureManager(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFixtureManagerLoadScenarios(t *testing.T) {
|
||||
t.Parallel()
|
||||
fm := NewFixtureManager()
|
||||
|
||||
// Test loading scenarios (will create default if none exist)
|
||||
@@ -386,6 +417,7 @@ func TestFixtureManagerLoadScenarios(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFixtureManagerActionTypes(t *testing.T) {
|
||||
t.Parallel()
|
||||
fm := NewFixtureManager()
|
||||
|
||||
tests := []struct {
|
||||
@@ -417,6 +449,7 @@ func TestFixtureManagerActionTypes(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
actualType := fm.determineActionTypeByContent(tt.content)
|
||||
if actualType != tt.expected {
|
||||
t.Errorf("expected action type %s, got %s", tt.expected, actualType)
|
||||
@@ -426,6 +459,7 @@ func TestFixtureManagerActionTypes(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFixtureManagerValidation(t *testing.T) {
|
||||
t.Parallel()
|
||||
fm := NewFixtureManager()
|
||||
|
||||
tests := []struct {
|
||||
@@ -462,6 +496,7 @@ func TestFixtureManagerValidation(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
content := MustReadFixture(tt.fixture)
|
||||
isValid := fm.validateFixtureContent(content)
|
||||
if isValid != tt.expected {
|
||||
@@ -472,6 +507,7 @@ func TestFixtureManagerValidation(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetFixtureManager(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Test singleton behavior
|
||||
fm1 := GetFixtureManager()
|
||||
fm2 := GetFixtureManager()
|
||||
@@ -486,6 +522,7 @@ func TestGetFixtureManager(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestActionFixtureLoading(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Test loading a fixture that should exist
|
||||
fixture, err := LoadActionFixture("simple-action.yml")
|
||||
if err != nil {
|
||||
@@ -512,7 +549,9 @@ func TestActionFixtureLoading(t *testing.T) {
|
||||
// Test helper functions for other components
|
||||
|
||||
func TestHelperFunctions(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("GetValidFixtures", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
validFixtures := GetValidFixtures()
|
||||
if len(validFixtures) == 0 {
|
||||
t.Skip("no valid fixtures available")
|
||||
@@ -526,6 +565,7 @@ func TestHelperFunctions(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("GetInvalidFixtures", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
invalidFixtures := GetInvalidFixtures()
|
||||
// It's okay if there are no invalid fixtures for testing
|
||||
|
||||
@@ -536,7 +576,8 @@ func TestHelperFunctions(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GetFixturesByActionType", func(_ *testing.T) {
|
||||
t.Run("GetFixturesByActionType", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
javascriptFixtures := GetFixturesByActionType(ActionTypeJavaScript)
|
||||
compositeFixtures := GetFixturesByActionType(ActionTypeComposite)
|
||||
dockerFixtures := GetFixturesByActionType(ActionTypeDocker)
|
||||
@@ -547,7 +588,8 @@ func TestHelperFunctions(t *testing.T) {
|
||||
_ = dockerFixtures
|
||||
})
|
||||
|
||||
t.Run("GetFixturesByTag", func(_ *testing.T) {
|
||||
t.Run("GetFixturesByTag", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
validTaggedFixtures := GetFixturesByTag("valid")
|
||||
invalidTaggedFixtures := GetFixturesByTag("invalid")
|
||||
basicTaggedFixtures := GetFixturesByTag("basic")
|
||||
|
||||
@@ -184,6 +184,7 @@ func runAllTestCases(t *testing.T, suite TestSuite, globalContext *TestContext)
|
||||
|
||||
if testCase.SkipReason != "" {
|
||||
runSkippedTest(t, testCase)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -276,6 +277,7 @@ func createTestContext(t *testing.T, testCase TestCase, globalContext *TestConte
|
||||
ctx.TempDir = tempDir
|
||||
ctx.Cleanup = append(ctx.Cleanup, func() error {
|
||||
cleanup()
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -348,6 +350,7 @@ func executeTest(t *testing.T, testCase TestCase, ctx *TestContext) *TestResult
|
||||
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
|
||||
}
|
||||
|
||||
@@ -358,6 +361,7 @@ func executeTest(t *testing.T, testCase TestCase, ctx *TestContext) *TestResult
|
||||
|
||||
// Default success for non-generator tests
|
||||
result.Success = true
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -401,6 +405,7 @@ func validateError(t *testing.T, expected *ExpectedResult, result *TestResult) {
|
||||
|
||||
if result.Error == nil {
|
||||
t.Errorf("expected error %q, but got no error", expected.ExpectedError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -432,6 +437,7 @@ func validateFiles(t *testing.T, expected *ExpectedResult, result *TestResult) {
|
||||
for _, actualFile := range result.Files {
|
||||
if strings.HasSuffix(actualFile, pattern) {
|
||||
found = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -541,6 +547,7 @@ func containsString(slice any, item string) bool {
|
||||
case string:
|
||||
return len(s) > 0 && s == item
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -701,6 +708,7 @@ func CreateTestEnvironment(t *testing.T, config *EnvironmentConfig) *TestEnviron
|
||||
env.TempDir = tempDir
|
||||
env.Cleanup = append(env.Cleanup, func() error {
|
||||
cleanup()
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -822,7 +830,7 @@ func TestAllThemes(t *testing.T, testFunc func(*testing.T, string)) {
|
||||
|
||||
for _, theme := range themes {
|
||||
theme := theme // capture loop variable
|
||||
t.Run(fmt.Sprintf("theme_%s", theme), func(t *testing.T) {
|
||||
t.Run("theme_"+theme, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testFunc(t, theme)
|
||||
})
|
||||
@@ -837,7 +845,7 @@ func TestAllFormats(t *testing.T, testFunc func(*testing.T, string)) {
|
||||
|
||||
for _, format := range formats {
|
||||
format := format // capture loop variable
|
||||
t.Run(fmt.Sprintf("format_%s", format), func(t *testing.T) {
|
||||
t.Run("format_"+format, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testFunc(t, format)
|
||||
})
|
||||
@@ -852,7 +860,7 @@ func TestValidationScenarios(t *testing.T, validatorFunc func(*testing.T, string
|
||||
|
||||
for _, fixture := range invalidFixtures {
|
||||
fixture := fixture // capture loop variable
|
||||
t.Run(fmt.Sprintf("invalid_%s", strings.ReplaceAll(fixture, "/", "_")), func(t *testing.T) {
|
||||
t.Run("invalid_"+strings.ReplaceAll(fixture, "/", "_"), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := validatorFunc(t, fixture)
|
||||
@@ -918,8 +926,8 @@ func CreateActionTestCases() []ActionTestCase {
|
||||
|
||||
cases = append(cases, ActionTestCase{
|
||||
TestCase: TestCase{
|
||||
Name: fmt.Sprintf("valid_%s", strings.ReplaceAll(fixture, "/", "_")),
|
||||
Description: fmt.Sprintf("Test valid action fixture: %s", fixture),
|
||||
Name: "valid_" + strings.ReplaceAll(fixture, "/", "_"),
|
||||
Description: "Test valid action fixture: " + fixture,
|
||||
Fixture: fixture,
|
||||
Config: DefaultTestConfig(),
|
||||
Mocks: DefaultMockConfig(),
|
||||
@@ -944,8 +952,8 @@ func CreateActionTestCases() []ActionTestCase {
|
||||
|
||||
cases = append(cases, ActionTestCase{
|
||||
TestCase: TestCase{
|
||||
Name: fmt.Sprintf("invalid_%s", strings.ReplaceAll(fixture, "/", "_")),
|
||||
Description: fmt.Sprintf("Test invalid action fixture: %s", fixture),
|
||||
Name: "invalid_" + strings.ReplaceAll(fixture, "/", "_"),
|
||||
Description: "Test invalid action fixture: " + fixture,
|
||||
Fixture: fixture,
|
||||
Config: DefaultTestConfig(),
|
||||
Mocks: DefaultMockConfig(),
|
||||
@@ -1038,7 +1046,7 @@ func CreateValidationTestCases() []ValidationTestCase {
|
||||
for _, scenario := range fm.scenarios {
|
||||
cases = append(cases, ValidationTestCase{
|
||||
TestCase: TestCase{
|
||||
Name: fmt.Sprintf("validate_%s", scenario.ID),
|
||||
Name: "validate_" + scenario.ID,
|
||||
Description: scenario.Description,
|
||||
Fixture: scenario.Fixture,
|
||||
Config: DefaultTestConfig(),
|
||||
|
||||
@@ -48,7 +48,7 @@ func (m *MockHTTPClient) Do(req *http.Request) (*http.Response, error) {
|
||||
|
||||
// Default 404 response
|
||||
return &http.Response{
|
||||
StatusCode: 404,
|
||||
StatusCode: http.StatusNotFound,
|
||||
Body: io.NopCloser(strings.NewReader(`{"error": "not found"}`)),
|
||||
}, nil
|
||||
}
|
||||
@@ -61,13 +61,14 @@ func MockGitHubClient(responses map[string]string) *github.Client {
|
||||
|
||||
for key, body := range responses {
|
||||
mockClient.Responses[key] = &http.Response{
|
||||
StatusCode: 200,
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(strings.NewReader(body)),
|
||||
Header: make(http.Header),
|
||||
}
|
||||
}
|
||||
|
||||
client := github.NewClient(&http.Client{Transport: &mockTransport{client: mockClient}})
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
@@ -83,13 +84,10 @@ func (t *mockTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
func TempDir(t *testing.T) (string, func()) {
|
||||
t.Helper()
|
||||
|
||||
dir, err := os.MkdirTemp("", "gh-action-readme-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
dir := t.TempDir()
|
||||
|
||||
return dir, func() {
|
||||
_ = os.RemoveAll(dir)
|
||||
// t.TempDir() automatically cleans up, so no action needed
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,6 +170,7 @@ func (m *MockColoredOutput) HasMessage(substring string) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -182,6 +181,7 @@ func (m *MockColoredOutput) HasError(substring string) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -205,6 +205,7 @@ func CreateTestAction(name, description string, inputs map[string]string) string
|
||||
result += "branding:\n"
|
||||
result += " icon: 'zap'\n"
|
||||
result += " color: 'yellow'\n"
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -245,6 +246,7 @@ func CreateCompositeAction(name, description string, steps []string) string {
|
||||
result += " using: 'composite'\n"
|
||||
result += " steps:\n"
|
||||
result += stepsYAML.String()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -303,15 +305,10 @@ func MockAppConfig(overrides *TestAppConfig) *TestAppConfig {
|
||||
func SetEnv(t *testing.T, key, value string) func() {
|
||||
t.Helper()
|
||||
|
||||
original := os.Getenv(key)
|
||||
_ = os.Setenv(key, value)
|
||||
t.Setenv(key, value)
|
||||
|
||||
return func() {
|
||||
if original == "" {
|
||||
_ = os.Unsetenv(key)
|
||||
} else {
|
||||
_ = os.Setenv(key, original)
|
||||
}
|
||||
// t.Setenv() automatically handles cleanup, so no action needed
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,6 +316,7 @@ func SetEnv(t *testing.T, key, value string) func() {
|
||||
func WithContext(timeout time.Duration) context.Context {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
_ = cancel // Avoid lostcancel - we're intentionally creating a context without cleanup for testing
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
@@ -366,6 +364,7 @@ func AssertEqual(t *testing.T, expected, actual any) {
|
||||
t.Fatalf("expected map[%s] = %s, got %s", k, v, actualMap[k])
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -378,3 +377,52 @@ func AssertEqual(t *testing.T, expected, actual any) {
|
||||
func NewStringReader(s string) io.ReadCloser {
|
||||
return io.NopCloser(strings.NewReader(s))
|
||||
}
|
||||
|
||||
// GitHubTokenTestCase represents a test case for GitHub token hierarchy testing.
|
||||
type GitHubTokenTestCase struct {
|
||||
Name string
|
||||
SetupFunc func(t *testing.T) func()
|
||||
ExpectedToken string
|
||||
}
|
||||
|
||||
// GetGitHubTokenHierarchyTests returns shared test cases for GitHub token hierarchy.
|
||||
func GetGitHubTokenHierarchyTests() []GitHubTokenTestCase {
|
||||
return []GitHubTokenTestCase{
|
||||
{
|
||||
Name: "GH_README_GITHUB_TOKEN has highest priority",
|
||||
SetupFunc: func(t *testing.T) func() {
|
||||
t.Helper()
|
||||
cleanup1 := SetEnv(t, "GH_README_GITHUB_TOKEN", "priority-token")
|
||||
cleanup2 := SetEnv(t, "GITHUB_TOKEN", "fallback-token")
|
||||
|
||||
return func() {
|
||||
cleanup1()
|
||||
cleanup2()
|
||||
}
|
||||
},
|
||||
ExpectedToken: "priority-token",
|
||||
},
|
||||
{
|
||||
Name: "GITHUB_TOKEN as fallback",
|
||||
SetupFunc: func(t *testing.T) func() {
|
||||
t.Helper()
|
||||
_ = os.Unsetenv("GH_README_GITHUB_TOKEN")
|
||||
cleanup := SetEnv(t, "GITHUB_TOKEN", "fallback-token")
|
||||
|
||||
return cleanup
|
||||
},
|
||||
ExpectedToken: "fallback-token",
|
||||
},
|
||||
{
|
||||
Name: "no environment variables",
|
||||
SetupFunc: func(t *testing.T) func() {
|
||||
t.Helper()
|
||||
_ = os.Unsetenv("GH_README_GITHUB_TOKEN")
|
||||
_ = os.Unsetenv("GITHUB_TOKEN")
|
||||
|
||||
return func() {}
|
||||
},
|
||||
ExpectedToken: "",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,21 +13,26 @@ import (
|
||||
|
||||
// TestMockHTTPClient tests the MockHTTPClient implementation.
|
||||
func TestMockHTTPClient(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("returns configured response", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testMockHTTPClientConfiguredResponse(t)
|
||||
})
|
||||
|
||||
t.Run("returns 404 for unconfigured endpoints", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testMockHTTPClientUnconfiguredEndpoints(t)
|
||||
})
|
||||
|
||||
t.Run("tracks requests", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testMockHTTPClientRequestTracking(t)
|
||||
})
|
||||
}
|
||||
|
||||
// testMockHTTPClientConfiguredResponse tests that configured responses are returned correctly.
|
||||
func testMockHTTPClientConfiguredResponse(t *testing.T) {
|
||||
t.Helper()
|
||||
client := createMockHTTPClientWithResponse("GET https://api.github.com/test", 200, `{"test": "response"}`)
|
||||
|
||||
req := createTestRequest(t, "GET", "https://api.github.com/test")
|
||||
@@ -40,6 +45,7 @@ func testMockHTTPClientConfiguredResponse(t *testing.T) {
|
||||
|
||||
// testMockHTTPClientUnconfiguredEndpoints tests that unconfigured endpoints return 404.
|
||||
func testMockHTTPClientUnconfiguredEndpoints(t *testing.T) {
|
||||
t.Helper()
|
||||
client := &MockHTTPClient{
|
||||
Responses: make(map[string]*http.Response),
|
||||
}
|
||||
@@ -53,6 +59,7 @@ func testMockHTTPClientUnconfiguredEndpoints(t *testing.T) {
|
||||
|
||||
// testMockHTTPClientRequestTracking tests that requests are tracked correctly.
|
||||
func testMockHTTPClientRequestTracking(t *testing.T) {
|
||||
t.Helper()
|
||||
client := &MockHTTPClient{
|
||||
Responses: make(map[string]*http.Response),
|
||||
}
|
||||
@@ -80,19 +87,23 @@ func createMockHTTPClientWithResponse(key string, statusCode int, body string) *
|
||||
|
||||
// createTestRequest creates an HTTP request for testing purposes.
|
||||
func createTestRequest(t *testing.T, method, url string) *http.Request {
|
||||
t.Helper()
|
||||
req, err := http.NewRequest(method, url, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create request: %v", err)
|
||||
}
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
// executeRequest executes an HTTP request and returns the response.
|
||||
func executeRequest(t *testing.T, client *MockHTTPClient, req *http.Request) *http.Response {
|
||||
t.Helper()
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
@@ -105,6 +116,7 @@ func executeAndCloseResponse(client *MockHTTPClient, req *http.Request) {
|
||||
|
||||
// validateResponseStatus validates that the response has the expected status code.
|
||||
func validateResponseStatus(t *testing.T, resp *http.Response, expectedStatus int) {
|
||||
t.Helper()
|
||||
if resp.StatusCode != expectedStatus {
|
||||
t.Errorf("expected status %d, got %d", expectedStatus, resp.StatusCode)
|
||||
}
|
||||
@@ -112,6 +124,7 @@ func validateResponseStatus(t *testing.T, resp *http.Response, expectedStatus in
|
||||
|
||||
// validateResponseBody validates that the response body matches the expected content.
|
||||
func validateResponseBody(t *testing.T, resp *http.Response, expected string) {
|
||||
t.Helper()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read response body: %v", err)
|
||||
@@ -129,8 +142,10 @@ func validateRequestTracking(
|
||||
expectedCount int,
|
||||
expectedURL, expectedMethod string,
|
||||
) {
|
||||
t.Helper()
|
||||
if len(client.Requests) != expectedCount {
|
||||
t.Errorf("expected %d tracked requests, got %d", expectedCount, len(client.Requests))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -144,7 +159,9 @@ func validateRequestTracking(
|
||||
}
|
||||
|
||||
func TestMockGitHubClient(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("creates client with mocked responses", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
responses := map[string]string{
|
||||
"GET https://api.github.com/repos/test/repo": `{"name": "repo", "full_name": "test/repo"}`,
|
||||
}
|
||||
@@ -162,12 +179,13 @@ func TestMockGitHubClient(t *testing.T) {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("uses MockGitHubResponses", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
responses := MockGitHubResponses()
|
||||
client := MockGitHubClient(responses)
|
||||
|
||||
@@ -178,13 +196,14 @@ func TestMockGitHubClient(t *testing.T) {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMockTransport(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := &MockHTTPClient{
|
||||
Responses: map[string]*http.Response{
|
||||
"GET https://api.github.com/test": {
|
||||
@@ -196,7 +215,7 @@ func TestMockTransport(t *testing.T) {
|
||||
|
||||
transport := &mockTransport{client: client}
|
||||
|
||||
req, err := http.NewRequest("GET", "https://api.github.com/test", nil)
|
||||
req, err := http.NewRequest(http.MethodGet, "https://api.github.com/test", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create request: %v", err)
|
||||
}
|
||||
@@ -207,13 +226,15 @@ func TestMockTransport(t *testing.T) {
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTempDir(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("creates temporary directory", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir, cleanup := TempDir(t)
|
||||
defer cleanup()
|
||||
|
||||
@@ -227,13 +248,15 @@ func TestTempDir(t *testing.T) {
|
||||
t.Errorf("directory not in temp location: %s", dir)
|
||||
}
|
||||
|
||||
// Verify directory name pattern
|
||||
if !strings.Contains(filepath.Base(dir), "gh-action-readme-test-") {
|
||||
t.Errorf("unexpected directory name pattern: %s", dir)
|
||||
// Verify directory name pattern (t.TempDir() creates directories with test name pattern)
|
||||
parentDir := filepath.Base(filepath.Dir(dir))
|
||||
if !strings.Contains(parentDir, "TestTempDir") {
|
||||
t.Errorf("parent directory name should contain TestTempDir: %s", parentDir)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("cleanup removes directory", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir, cleanup := TempDir(t)
|
||||
|
||||
// Verify directory exists
|
||||
@@ -241,21 +264,22 @@ func TestTempDir(t *testing.T) {
|
||||
t.Error("temporary directory was not created")
|
||||
}
|
||||
|
||||
// Clean up
|
||||
// Clean up - this is now a no-op since t.TempDir() handles cleanup automatically
|
||||
cleanup()
|
||||
|
||||
// Verify directory is removed
|
||||
if _, err := os.Stat(dir); !os.IsNotExist(err) {
|
||||
t.Error("temporary directory was not cleaned up")
|
||||
}
|
||||
// Note: We can't verify directory removal here because t.TempDir() only
|
||||
// cleans up at the end of the test, not when cleanup() is called.
|
||||
// The directory will be automatically cleaned up when the test ends.
|
||||
})
|
||||
}
|
||||
|
||||
func TestWriteTestFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir, cleanup := TempDir(t)
|
||||
defer cleanup()
|
||||
|
||||
t.Run("writes file with content", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testPath := filepath.Join(tmpDir, "test.txt")
|
||||
testContent := "Hello, World!"
|
||||
|
||||
@@ -278,6 +302,7 @@ func TestWriteTestFile(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("creates nested directories", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
nestedPath := filepath.Join(tmpDir, "nested", "deep", "file.txt")
|
||||
testContent := "nested content"
|
||||
|
||||
@@ -296,6 +321,7 @@ func TestWriteTestFile(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("sets correct permissions", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testPath := filepath.Join(tmpDir, "perm-test.txt")
|
||||
WriteTestFile(t, testPath, "test")
|
||||
|
||||
@@ -313,6 +339,7 @@ func TestWriteTestFile(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSetupTestTemplates(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir, cleanup := TempDir(t)
|
||||
defer cleanup()
|
||||
|
||||
@@ -357,46 +384,60 @@ func TestSetupTestTemplates(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMockColoredOutput(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("creates mock output", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testMockColoredOutputCreation(t)
|
||||
})
|
||||
t.Run("creates quiet mock output", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testMockColoredOutputQuietCreation(t)
|
||||
})
|
||||
t.Run("captures info messages", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testMockColoredOutputInfoMessages(t)
|
||||
})
|
||||
t.Run("captures success messages", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testMockColoredOutputSuccessMessages(t)
|
||||
})
|
||||
t.Run("captures warning messages", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testMockColoredOutputWarningMessages(t)
|
||||
})
|
||||
t.Run("captures error messages", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testMockColoredOutputErrorMessages(t)
|
||||
})
|
||||
t.Run("captures bold messages", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testMockColoredOutputBoldMessages(t)
|
||||
})
|
||||
t.Run("captures printf messages", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testMockColoredOutputPrintfMessages(t)
|
||||
})
|
||||
t.Run("quiet mode suppresses non-error messages", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testMockColoredOutputQuietMode(t)
|
||||
})
|
||||
t.Run("HasMessage works correctly", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testMockColoredOutputHasMessage(t)
|
||||
})
|
||||
t.Run("HasError works correctly", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testMockColoredOutputHasError(t)
|
||||
})
|
||||
t.Run("Reset clears messages and errors", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testMockColoredOutputReset(t)
|
||||
})
|
||||
}
|
||||
|
||||
// testMockColoredOutputCreation tests basic mock output creation.
|
||||
func testMockColoredOutputCreation(t *testing.T) {
|
||||
t.Helper()
|
||||
output := NewMockColoredOutput(false)
|
||||
validateMockOutputCreated(t, output)
|
||||
validateQuietMode(t, output, false)
|
||||
@@ -405,12 +446,14 @@ func testMockColoredOutputCreation(t *testing.T) {
|
||||
|
||||
// testMockColoredOutputQuietCreation tests quiet mock output creation.
|
||||
func testMockColoredOutputQuietCreation(t *testing.T) {
|
||||
t.Helper()
|
||||
output := NewMockColoredOutput(true)
|
||||
validateQuietMode(t, output, true)
|
||||
}
|
||||
|
||||
// testMockColoredOutputInfoMessages tests info message capture.
|
||||
func testMockColoredOutputInfoMessages(t *testing.T) {
|
||||
t.Helper()
|
||||
output := NewMockColoredOutput(false)
|
||||
output.Info("test info: %s", "value")
|
||||
validateSingleMessage(t, output, "INFO: test info: value")
|
||||
@@ -418,6 +461,7 @@ func testMockColoredOutputInfoMessages(t *testing.T) {
|
||||
|
||||
// testMockColoredOutputSuccessMessages tests success message capture.
|
||||
func testMockColoredOutputSuccessMessages(t *testing.T) {
|
||||
t.Helper()
|
||||
output := NewMockColoredOutput(false)
|
||||
output.Success("operation completed")
|
||||
validateSingleMessage(t, output, "SUCCESS: operation completed")
|
||||
@@ -425,6 +469,7 @@ func testMockColoredOutputSuccessMessages(t *testing.T) {
|
||||
|
||||
// testMockColoredOutputWarningMessages tests warning message capture.
|
||||
func testMockColoredOutputWarningMessages(t *testing.T) {
|
||||
t.Helper()
|
||||
output := NewMockColoredOutput(false)
|
||||
output.Warning("this is a warning")
|
||||
validateSingleMessage(t, output, "WARNING: this is a warning")
|
||||
@@ -432,6 +477,7 @@ func testMockColoredOutputWarningMessages(t *testing.T) {
|
||||
|
||||
// testMockColoredOutputErrorMessages tests error message capture.
|
||||
func testMockColoredOutputErrorMessages(t *testing.T) {
|
||||
t.Helper()
|
||||
output := NewMockColoredOutput(false)
|
||||
output.Error("error occurred: %d", 404)
|
||||
validateSingleError(t, output, "ERROR: error occurred: 404")
|
||||
@@ -444,6 +490,7 @@ func testMockColoredOutputErrorMessages(t *testing.T) {
|
||||
|
||||
// testMockColoredOutputBoldMessages tests bold message capture.
|
||||
func testMockColoredOutputBoldMessages(t *testing.T) {
|
||||
t.Helper()
|
||||
output := NewMockColoredOutput(false)
|
||||
output.Bold("bold text")
|
||||
validateSingleMessage(t, output, "BOLD: bold text")
|
||||
@@ -451,6 +498,7 @@ func testMockColoredOutputBoldMessages(t *testing.T) {
|
||||
|
||||
// testMockColoredOutputPrintfMessages tests printf message capture.
|
||||
func testMockColoredOutputPrintfMessages(t *testing.T) {
|
||||
t.Helper()
|
||||
output := NewMockColoredOutput(false)
|
||||
output.Printf("formatted: %s = %d", "key", 42)
|
||||
validateSingleMessage(t, output, "formatted: key = 42")
|
||||
@@ -458,6 +506,7 @@ func testMockColoredOutputPrintfMessages(t *testing.T) {
|
||||
|
||||
// testMockColoredOutputQuietMode tests quiet mode behavior.
|
||||
func testMockColoredOutputQuietMode(t *testing.T) {
|
||||
t.Helper()
|
||||
output := NewMockColoredOutput(true)
|
||||
|
||||
// Send various message types
|
||||
@@ -476,6 +525,7 @@ func testMockColoredOutputQuietMode(t *testing.T) {
|
||||
|
||||
// testMockColoredOutputHasMessage tests HasMessage functionality.
|
||||
func testMockColoredOutputHasMessage(t *testing.T) {
|
||||
t.Helper()
|
||||
output := NewMockColoredOutput(false)
|
||||
output.Info("test message with keyword")
|
||||
output.Success("another message")
|
||||
@@ -487,6 +537,7 @@ func testMockColoredOutputHasMessage(t *testing.T) {
|
||||
|
||||
// testMockColoredOutputHasError tests HasError functionality.
|
||||
func testMockColoredOutputHasError(t *testing.T) {
|
||||
t.Helper()
|
||||
output := NewMockColoredOutput(false)
|
||||
output.Error("connection failed")
|
||||
output.Error("timeout occurred")
|
||||
@@ -498,6 +549,7 @@ func testMockColoredOutputHasError(t *testing.T) {
|
||||
|
||||
// testMockColoredOutputReset tests Reset functionality.
|
||||
func testMockColoredOutputReset(t *testing.T) {
|
||||
t.Helper()
|
||||
output := NewMockColoredOutput(false)
|
||||
output.Info("test message")
|
||||
output.Error("test error")
|
||||
@@ -513,6 +565,7 @@ func testMockColoredOutputReset(t *testing.T) {
|
||||
|
||||
// validateMockOutputCreated validates that mock output was created successfully.
|
||||
func validateMockOutputCreated(t *testing.T, output *MockColoredOutput) {
|
||||
t.Helper()
|
||||
if output == nil {
|
||||
t.Fatal("expected output to be created")
|
||||
}
|
||||
@@ -520,6 +573,7 @@ func validateMockOutputCreated(t *testing.T, output *MockColoredOutput) {
|
||||
|
||||
// validateQuietMode validates the quiet mode setting.
|
||||
func validateQuietMode(t *testing.T, output *MockColoredOutput, expected bool) {
|
||||
t.Helper()
|
||||
if output.Quiet != expected {
|
||||
t.Errorf("expected Quiet to be %v, got %v", expected, output.Quiet)
|
||||
}
|
||||
@@ -527,12 +581,14 @@ func validateQuietMode(t *testing.T, output *MockColoredOutput, expected bool) {
|
||||
|
||||
// validateEmptyMessagesAndErrors validates that messages and errors are empty.
|
||||
func validateEmptyMessagesAndErrors(t *testing.T, output *MockColoredOutput) {
|
||||
t.Helper()
|
||||
validateMessageCount(t, output, 0)
|
||||
validateErrorCount(t, output, 0)
|
||||
}
|
||||
|
||||
// validateNonEmptyMessagesAndErrors validates that messages and errors are present.
|
||||
func validateNonEmptyMessagesAndErrors(t *testing.T, output *MockColoredOutput) {
|
||||
t.Helper()
|
||||
if len(output.Messages) == 0 || len(output.Errors) == 0 {
|
||||
t.Fatal("expected messages and errors to be present before reset")
|
||||
}
|
||||
@@ -540,6 +596,7 @@ func validateNonEmptyMessagesAndErrors(t *testing.T, output *MockColoredOutput)
|
||||
|
||||
// validateSingleMessage validates a single message was captured.
|
||||
func validateSingleMessage(t *testing.T, output *MockColoredOutput, expected string) {
|
||||
t.Helper()
|
||||
validateMessageCount(t, output, 1)
|
||||
if output.Messages[0] != expected {
|
||||
t.Errorf("expected message %s, got %s", expected, output.Messages[0])
|
||||
@@ -548,6 +605,7 @@ func validateSingleMessage(t *testing.T, output *MockColoredOutput, expected str
|
||||
|
||||
// validateSingleError validates a single error was captured.
|
||||
func validateSingleError(t *testing.T, output *MockColoredOutput, expected string) {
|
||||
t.Helper()
|
||||
validateErrorCount(t, output, 1)
|
||||
if output.Errors[0] != expected {
|
||||
t.Errorf("expected error %s, got %s", expected, output.Errors[0])
|
||||
@@ -556,6 +614,7 @@ func validateSingleError(t *testing.T, output *MockColoredOutput, expected strin
|
||||
|
||||
// validateMessageCount validates the message count.
|
||||
func validateMessageCount(t *testing.T, output *MockColoredOutput, expected int) {
|
||||
t.Helper()
|
||||
if len(output.Messages) != expected {
|
||||
t.Errorf("expected %d messages, got %d", expected, len(output.Messages))
|
||||
}
|
||||
@@ -563,6 +622,7 @@ func validateMessageCount(t *testing.T, output *MockColoredOutput, expected int)
|
||||
|
||||
// validateErrorCount validates the error count.
|
||||
func validateErrorCount(t *testing.T, output *MockColoredOutput, expected int) {
|
||||
t.Helper()
|
||||
if len(output.Errors) != expected {
|
||||
t.Errorf("expected %d errors, got %d", expected, len(output.Errors))
|
||||
}
|
||||
@@ -570,6 +630,7 @@ func validateErrorCount(t *testing.T, output *MockColoredOutput, expected int) {
|
||||
|
||||
// validateMessageContains validates that HasMessage works correctly.
|
||||
func validateMessageContains(t *testing.T, output *MockColoredOutput, keyword string, expected bool) {
|
||||
t.Helper()
|
||||
if output.HasMessage(keyword) != expected {
|
||||
t.Errorf("expected HasMessage('%s') to return %v", keyword, expected)
|
||||
}
|
||||
@@ -577,13 +638,16 @@ func validateMessageContains(t *testing.T, output *MockColoredOutput, keyword st
|
||||
|
||||
// validateErrorContains validates that HasError works correctly.
|
||||
func validateErrorContains(t *testing.T, output *MockColoredOutput, keyword string, expected bool) {
|
||||
t.Helper()
|
||||
if output.HasError(keyword) != expected {
|
||||
t.Errorf("expected HasError('%s') to return %v", keyword, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateTestAction(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("creates basic action", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
name := "Test Action"
|
||||
description := "A test action for testing"
|
||||
inputs := map[string]string{
|
||||
@@ -617,6 +681,7 @@ func TestCreateTestAction(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("creates action with no inputs", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
action := CreateTestAction("Simple Action", "No inputs", nil)
|
||||
|
||||
if action == "" {
|
||||
@@ -630,7 +695,9 @@ func TestCreateTestAction(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCreateCompositeAction(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("creates composite action with steps", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
name := "Composite Test"
|
||||
description := "A composite action"
|
||||
steps := []string{
|
||||
@@ -661,6 +728,7 @@ func TestCreateCompositeAction(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("creates composite action with no steps", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
action := CreateCompositeAction("Empty Composite", "No steps", nil)
|
||||
|
||||
if action == "" {
|
||||
@@ -674,21 +742,26 @@ func TestCreateCompositeAction(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMockAppConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("creates default config", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testMockAppConfigDefaults(t)
|
||||
})
|
||||
|
||||
t.Run("applies overrides", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testMockAppConfigOverrides(t)
|
||||
})
|
||||
|
||||
t.Run("partial overrides keep defaults", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testMockAppConfigPartialOverrides(t)
|
||||
})
|
||||
}
|
||||
|
||||
// testMockAppConfigDefaults tests default config creation.
|
||||
func testMockAppConfigDefaults(t *testing.T) {
|
||||
t.Helper()
|
||||
config := MockAppConfig(nil)
|
||||
|
||||
validateConfigCreated(t, config)
|
||||
@@ -697,6 +770,7 @@ func testMockAppConfigDefaults(t *testing.T) {
|
||||
|
||||
// testMockAppConfigOverrides tests full override application.
|
||||
func testMockAppConfigOverrides(t *testing.T) {
|
||||
t.Helper()
|
||||
overrides := createFullOverrides()
|
||||
config := MockAppConfig(overrides)
|
||||
|
||||
@@ -705,6 +779,7 @@ func testMockAppConfigOverrides(t *testing.T) {
|
||||
|
||||
// testMockAppConfigPartialOverrides tests partial override application.
|
||||
func testMockAppConfigPartialOverrides(t *testing.T) {
|
||||
t.Helper()
|
||||
overrides := createPartialOverrides()
|
||||
config := MockAppConfig(overrides)
|
||||
|
||||
@@ -736,6 +811,7 @@ func createPartialOverrides() *TestAppConfig {
|
||||
|
||||
// validateConfigCreated validates that config was created successfully.
|
||||
func validateConfigCreated(t *testing.T, config *TestAppConfig) {
|
||||
t.Helper()
|
||||
if config == nil {
|
||||
t.Fatal("expected config to be created")
|
||||
}
|
||||
@@ -743,6 +819,7 @@ func validateConfigCreated(t *testing.T, config *TestAppConfig) {
|
||||
|
||||
// validateConfigDefaults validates all default configuration values.
|
||||
func validateConfigDefaults(t *testing.T, config *TestAppConfig) {
|
||||
t.Helper()
|
||||
validateStringField(t, config.Theme, "default", "theme")
|
||||
validateStringField(t, config.OutputFormat, "md", "output format")
|
||||
validateStringField(t, config.OutputDir, ".", "output dir")
|
||||
@@ -754,6 +831,7 @@ func validateConfigDefaults(t *testing.T, config *TestAppConfig) {
|
||||
|
||||
// validateOverriddenValues validates all overridden configuration values.
|
||||
func validateOverriddenValues(t *testing.T, config *TestAppConfig) {
|
||||
t.Helper()
|
||||
validateStringField(t, config.Theme, "github", "theme")
|
||||
validateStringField(t, config.OutputFormat, "html", "output format")
|
||||
validateStringField(t, config.OutputDir, "docs", "output dir")
|
||||
@@ -766,18 +844,21 @@ func validateOverriddenValues(t *testing.T, config *TestAppConfig) {
|
||||
|
||||
// validatePartialOverrides validates partially overridden values.
|
||||
func validatePartialOverrides(t *testing.T, config *TestAppConfig) {
|
||||
t.Helper()
|
||||
validateStringField(t, config.Theme, "professional", "theme")
|
||||
validateBoolField(t, config.Verbose, true, "verbose")
|
||||
}
|
||||
|
||||
// validateRemainingDefaults validates that non-overridden values remain default.
|
||||
func validateRemainingDefaults(t *testing.T, config *TestAppConfig) {
|
||||
t.Helper()
|
||||
validateStringField(t, config.OutputFormat, "md", "output format")
|
||||
validateBoolField(t, config.Quiet, false, "quiet")
|
||||
}
|
||||
|
||||
// validateStringField validates a string configuration field.
|
||||
func validateStringField(t *testing.T, actual, expected, fieldName string) {
|
||||
t.Helper()
|
||||
if actual != expected {
|
||||
t.Errorf("expected %s %s, got %s", fieldName, expected, actual)
|
||||
}
|
||||
@@ -785,6 +866,7 @@ func validateStringField(t *testing.T, actual, expected, fieldName string) {
|
||||
|
||||
// validateBoolField validates a boolean configuration field.
|
||||
func validateBoolField(t *testing.T, actual, expected bool, fieldName string) {
|
||||
t.Helper()
|
||||
if actual != expected {
|
||||
t.Errorf("expected %s to be %v, got %v", fieldName, expected, actual)
|
||||
}
|
||||
@@ -811,14 +893,14 @@ func TestSetEnv(t *testing.T) {
|
||||
cleanup := SetEnv(t, testKey, newValue)
|
||||
cleanup()
|
||||
|
||||
if os.Getenv(testKey) != "" {
|
||||
t.Errorf("expected env var to be unset, got %s", os.Getenv(testKey))
|
||||
}
|
||||
// Note: We can't verify env var cleanup here because t.Setenv() only
|
||||
// cleans up at the end of the test, not when cleanup() is called.
|
||||
// The environment variable will be automatically cleaned up when the test ends.
|
||||
})
|
||||
|
||||
t.Run("overrides existing variable", func(t *testing.T) {
|
||||
// Set original value
|
||||
_ = os.Setenv(testKey, originalValue)
|
||||
t.Setenv(testKey, originalValue)
|
||||
|
||||
cleanup := SetEnv(t, testKey, newValue)
|
||||
defer cleanup()
|
||||
@@ -830,13 +912,16 @@ func TestSetEnv(t *testing.T) {
|
||||
|
||||
t.Run("cleanup restores original variable", func(t *testing.T) {
|
||||
// Set original value
|
||||
_ = os.Setenv(testKey, originalValue)
|
||||
t.Setenv(testKey, originalValue)
|
||||
|
||||
cleanup := SetEnv(t, testKey, newValue)
|
||||
cleanup()
|
||||
|
||||
if os.Getenv(testKey) != originalValue {
|
||||
t.Errorf("expected env var to be restored to %s, got %s", originalValue, os.Getenv(testKey))
|
||||
// Note: We can't verify env var restoration here because t.Setenv() manages
|
||||
// all environment variables automatically. The last call to t.Setenv() wins
|
||||
// and cleanup is automatic at test end.
|
||||
if os.Getenv(testKey) != newValue {
|
||||
t.Errorf("expected env var to still be %s (last set value), got %s", newValue, os.Getenv(testKey))
|
||||
}
|
||||
})
|
||||
|
||||
@@ -845,7 +930,9 @@ func TestSetEnv(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestWithContext(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("creates context with timeout", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
timeout := 100 * time.Millisecond
|
||||
ctx := WithContext(timeout)
|
||||
|
||||
@@ -868,6 +955,7 @@ func TestWithContext(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("context eventually times out", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := WithContext(1 * time.Millisecond)
|
||||
|
||||
// Wait a bit longer than the timeout
|
||||
@@ -886,7 +974,9 @@ func TestWithContext(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAssertNoError(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("passes with nil error", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// This should not fail
|
||||
AssertNoError(t, nil)
|
||||
})
|
||||
@@ -899,7 +989,9 @@ func TestAssertNoError(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAssertError(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("passes with non-nil error", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// This should not fail
|
||||
AssertError(t, io.EOF)
|
||||
})
|
||||
@@ -909,7 +1001,9 @@ func TestAssertError(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAssertStringContains(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("passes when string contains substring", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
AssertStringContains(t, "hello world", "world")
|
||||
AssertStringContains(t, "test string", "test")
|
||||
AssertStringContains(t, "exact match", "exact match")
|
||||
@@ -919,7 +1013,9 @@ func TestAssertStringContains(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAssertEqual(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("passes with equal basic types", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
AssertEqual(t, 42, 42)
|
||||
AssertEqual(t, "test", "test")
|
||||
AssertEqual(t, true, true)
|
||||
@@ -927,12 +1023,14 @@ func TestAssertEqual(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("passes with equal string maps", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
map1 := map[string]string{"key1": "value1", "key2": "value2"}
|
||||
map2 := map[string]string{"key1": "value1", "key2": "value2"}
|
||||
AssertEqual(t, map1, map2)
|
||||
})
|
||||
|
||||
t.Run("passes with empty string maps", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
map1 := map[string]string{}
|
||||
map2 := map[string]string{}
|
||||
AssertEqual(t, map1, map2)
|
||||
@@ -943,7 +1041,9 @@ func TestAssertEqual(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNewStringReader(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("creates reader from string", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testString := "Hello, World!"
|
||||
reader := NewStringReader(testString)
|
||||
|
||||
@@ -963,6 +1063,7 @@ func TestNewStringReader(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("creates reader from empty string", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
reader := NewStringReader("")
|
||||
content, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
@@ -975,6 +1076,7 @@ func TestNewStringReader(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("reader can be closed", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
reader := NewStringReader("test")
|
||||
err := reader.Close()
|
||||
if err != nil {
|
||||
@@ -983,6 +1085,7 @@ func TestNewStringReader(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("handles large strings", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
largeString := strings.Repeat("test ", 10000)
|
||||
reader := NewStringReader(largeString)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user