mirror of
https://github.com/ivuorinen/gh-action-readme.git
synced 2026-01-26 11:14:04 +00:00
* chore(lint): added nlreturn, run linting * chore(lint): replace some fmt.Sprintf calls * chore(lint): replace fmt.Sprintf with strconv * chore(lint): add goconst, use http lib for status codes, and methods * chore(lint): use errors lib, errCodes from internal/errors * chore(lint): dupl, thelper and usetesting * chore(lint): fmt.Errorf %v to %w, more linters * chore(lint): paralleltest, where possible * perf(test): optimize test performance by 78% - Implement shared binary building with package-level cache to eliminate redundant builds - Add strategic parallelization to 15+ tests while preserving environment variable isolation - Implement thread-safe fixture caching with RWMutex to reduce I/O operations - Remove unnecessary working directory changes by leveraging embedded templates - Add embedded template system with go:embed directive for reliable template resolution - Fix linting issues: rename sharedBinaryError to errSharedBinary, add nolint directive Performance improvements: - Total test execution time: 12+ seconds → 2.7 seconds (78% faster) - Binary build overhead: 14+ separate builds → 1 shared build (93% reduction) - Parallel execution: Limited → 15+ concurrent tests (60-70% better CPU usage) - I/O operations: 66+ fixture reads → cached with sync.RWMutex (50% reduction) All tests maintain 100% success rate and coverage while running nearly 4x faster.
386 lines
9.1 KiB
Go
386 lines
9.1 KiB
Go
package errors
|
|
|
|
import (
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestGetSuggestions(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
code ErrorCode
|
|
context map[string]string
|
|
contains []string
|
|
}{
|
|
{
|
|
name: "file not found with path",
|
|
code: ErrCodeFileNotFound,
|
|
context: map[string]string{
|
|
"path": "/path/to/action.yml",
|
|
},
|
|
contains: []string{
|
|
"Check if the file exists: /path/to/action.yml",
|
|
"Verify the file path is correct",
|
|
"--recursive flag",
|
|
},
|
|
},
|
|
{
|
|
name: "file not found action file",
|
|
code: ErrCodeFileNotFound,
|
|
context: map[string]string{
|
|
"path": "/project/action.yml",
|
|
},
|
|
contains: []string{
|
|
"Common action file names: action.yml, action.yaml",
|
|
"Check if the file is in a subdirectory",
|
|
},
|
|
},
|
|
{
|
|
name: "permission denied",
|
|
code: ErrCodePermission,
|
|
context: map[string]string{
|
|
"path": "/restricted/file.txt",
|
|
},
|
|
contains: []string{
|
|
"Check file permissions: ls -la /restricted/file.txt",
|
|
"chmod 644 /restricted/file.txt",
|
|
},
|
|
},
|
|
{
|
|
name: "invalid YAML with line number",
|
|
code: ErrCodeInvalidYAML,
|
|
context: map[string]string{
|
|
"line": "25",
|
|
},
|
|
contains: []string{
|
|
"Error near line 25",
|
|
"Check YAML indentation",
|
|
"use spaces, not tabs",
|
|
"YAML validator",
|
|
},
|
|
},
|
|
{
|
|
name: "invalid YAML with tab error",
|
|
code: ErrCodeInvalidYAML,
|
|
context: map[string]string{
|
|
"error": "found character that cannot start any token (tab)",
|
|
},
|
|
contains: []string{
|
|
"YAML files must use spaces for indentation, not tabs",
|
|
"Replace all tabs with spaces",
|
|
},
|
|
},
|
|
{
|
|
name: "invalid action with missing fields",
|
|
code: ErrCodeInvalidAction,
|
|
context: map[string]string{
|
|
"missing_fields": "name, description",
|
|
},
|
|
contains: []string{
|
|
"Missing required fields: name, description",
|
|
"required fields: name, description",
|
|
"gh-action-readme schema",
|
|
},
|
|
},
|
|
{
|
|
name: "no action files",
|
|
code: ErrCodeNoActionFiles,
|
|
context: map[string]string{
|
|
"directory": "/project",
|
|
},
|
|
contains: []string{
|
|
"Current directory: /project",
|
|
"find /project -name 'action.y*ml'",
|
|
"--recursive flag",
|
|
"action.yml or action.yaml",
|
|
},
|
|
},
|
|
{
|
|
name: "GitHub API 401 error",
|
|
code: ErrCodeGitHubAPI,
|
|
context: map[string]string{
|
|
"status_code": "401",
|
|
},
|
|
contains: []string{
|
|
"Authentication failed",
|
|
"check your GitHub token",
|
|
"Token may be expired",
|
|
},
|
|
},
|
|
{
|
|
name: "GitHub API 403 error",
|
|
code: ErrCodeGitHubAPI,
|
|
context: map[string]string{
|
|
"status_code": "403",
|
|
},
|
|
contains: []string{
|
|
"Access forbidden",
|
|
"check token permissions",
|
|
"rate limit",
|
|
},
|
|
},
|
|
{
|
|
name: "GitHub API 404 error",
|
|
code: ErrCodeGitHubAPI,
|
|
context: map[string]string{
|
|
"status_code": "404",
|
|
},
|
|
contains: []string{
|
|
"Repository or resource not found",
|
|
"repository is private",
|
|
},
|
|
},
|
|
{
|
|
name: "GitHub rate limit",
|
|
code: ErrCodeGitHubRateLimit,
|
|
context: map[string]string{},
|
|
contains: []string{
|
|
"rate limit exceeded",
|
|
"GITHUB_TOKEN",
|
|
"gh auth login",
|
|
"Rate limits reset every hour",
|
|
},
|
|
},
|
|
{
|
|
name: "GitHub auth",
|
|
code: ErrCodeGitHubAuth,
|
|
context: map[string]string{},
|
|
contains: []string{
|
|
"export GITHUB_TOKEN",
|
|
"gh auth login",
|
|
"https://github.com/settings/tokens",
|
|
"'repo' scope",
|
|
},
|
|
},
|
|
{
|
|
name: "configuration error with path",
|
|
code: ErrCodeConfiguration,
|
|
context: map[string]string{
|
|
"config_path": "~/.config/gh-action-readme/config.yaml",
|
|
},
|
|
contains: []string{
|
|
"Config path: ~/.config/gh-action-readme/config.yaml",
|
|
"ls -la ~/.config/gh-action-readme/config.yaml",
|
|
"gh-action-readme config init",
|
|
},
|
|
},
|
|
{
|
|
name: "validation error with invalid fields",
|
|
code: ErrCodeValidation,
|
|
context: map[string]string{
|
|
"invalid_fields": "runs.using, inputs.test",
|
|
},
|
|
contains: []string{
|
|
"Invalid fields: runs.using, inputs.test",
|
|
"Check spelling and nesting",
|
|
"gh-action-readme schema",
|
|
},
|
|
},
|
|
{
|
|
name: "template error with theme",
|
|
code: ErrCodeTemplateRender,
|
|
context: map[string]string{
|
|
"theme": "custom",
|
|
},
|
|
contains: []string{
|
|
"Current theme: custom",
|
|
"Try using a different theme",
|
|
"Available themes:",
|
|
},
|
|
},
|
|
{
|
|
name: "file write error with output path",
|
|
code: ErrCodeFileWrite,
|
|
context: map[string]string{
|
|
"output_path": "/output/README.md",
|
|
},
|
|
contains: []string{
|
|
"Output directory: /output",
|
|
"Check permissions: ls -la /output",
|
|
"mkdir -p /output",
|
|
},
|
|
},
|
|
{
|
|
name: "dependency analysis error",
|
|
code: ErrCodeDependencyAnalysis,
|
|
context: map[string]string{
|
|
"action": "my-action",
|
|
},
|
|
contains: []string{
|
|
"Analyzing action: my-action",
|
|
"GitHub token is set",
|
|
"composite actions",
|
|
},
|
|
},
|
|
{
|
|
name: "cache access error",
|
|
code: ErrCodeCacheAccess,
|
|
context: map[string]string{
|
|
"cache_path": "~/.cache/gh-action-readme",
|
|
},
|
|
contains: []string{
|
|
"Cache path: ~/.cache/gh-action-readme",
|
|
"gh-action-readme cache clear",
|
|
"permissions: ls -la ~/.cache/gh-action-readme",
|
|
},
|
|
},
|
|
{
|
|
name: "unknown error code",
|
|
code: "UNKNOWN_TEST_CODE",
|
|
context: map[string]string{},
|
|
contains: []string{
|
|
"Check the error message",
|
|
"--verbose flag",
|
|
"project documentation",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
suggestions := GetSuggestions(tt.code, tt.context)
|
|
|
|
if len(suggestions) == 0 {
|
|
t.Error("GetSuggestions() returned empty slice")
|
|
|
|
return
|
|
}
|
|
|
|
allSuggestions := strings.Join(suggestions, " ")
|
|
for _, expected := range tt.contains {
|
|
if !strings.Contains(allSuggestions, expected) {
|
|
t.Errorf(
|
|
"GetSuggestions() missing expected content:\nExpected to contain: %q\nSuggestions:\n%s",
|
|
expected,
|
|
strings.Join(suggestions, "\n"),
|
|
)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetPermissionSuggestions_OSSpecific(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
context := map[string]string{"path": "/test/file"}
|
|
suggestions := getPermissionSuggestions(context)
|
|
|
|
allSuggestions := strings.Join(suggestions, " ")
|
|
|
|
switch runtime.GOOS {
|
|
case "windows":
|
|
if !strings.Contains(allSuggestions, "Administrator") {
|
|
t.Error("Windows-specific suggestions should mention Administrator")
|
|
}
|
|
if !strings.Contains(allSuggestions, "Windows file permissions") {
|
|
t.Error("Windows-specific suggestions should mention Windows file permissions")
|
|
}
|
|
default:
|
|
if !strings.Contains(allSuggestions, "sudo") {
|
|
t.Error("Unix-specific suggestions should mention sudo")
|
|
}
|
|
if !strings.Contains(allSuggestions, "ls -la") {
|
|
t.Error("Unix-specific suggestions should mention ls -la")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGetSuggestions_EmptyContext(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Test that all error codes work with empty context
|
|
errorCodes := []ErrorCode{
|
|
ErrCodeFileNotFound,
|
|
ErrCodePermission,
|
|
ErrCodeInvalidYAML,
|
|
ErrCodeInvalidAction,
|
|
ErrCodeNoActionFiles,
|
|
ErrCodeGitHubAPI,
|
|
ErrCodeGitHubRateLimit,
|
|
ErrCodeGitHubAuth,
|
|
ErrCodeConfiguration,
|
|
ErrCodeValidation,
|
|
ErrCodeTemplateRender,
|
|
ErrCodeFileWrite,
|
|
ErrCodeDependencyAnalysis,
|
|
ErrCodeCacheAccess,
|
|
}
|
|
|
|
for _, code := range errorCodes {
|
|
t.Run(string(code), func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
suggestions := GetSuggestions(code, map[string]string{})
|
|
if len(suggestions) == 0 {
|
|
t.Errorf("GetSuggestions(%s, {}) returned empty slice", code)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetFileNotFoundSuggestions_ActionFile(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
context := map[string]string{
|
|
"path": "/project/action.yml",
|
|
}
|
|
|
|
suggestions := getFileNotFoundSuggestions(context)
|
|
allSuggestions := strings.Join(suggestions, " ")
|
|
|
|
// Should suggest common action file names when path contains "action"
|
|
if !strings.Contains(allSuggestions, "action.yml, action.yaml") {
|
|
t.Error("Should suggest common action file names for action file paths")
|
|
}
|
|
|
|
if !strings.Contains(allSuggestions, "subdirectory") {
|
|
t.Error("Should suggest checking subdirectories for action files")
|
|
}
|
|
}
|
|
|
|
func TestGetInvalidYAMLSuggestions_TabError(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
context := map[string]string{
|
|
"error": "found character that cannot start any token, tab character",
|
|
}
|
|
|
|
suggestions := getInvalidYAMLSuggestions(context)
|
|
allSuggestions := strings.Join(suggestions, " ")
|
|
|
|
// Should prioritize tab-specific suggestions when error mentions tabs
|
|
if !strings.Contains(allSuggestions, "tabs with spaces") {
|
|
t.Error("Should provide tab-specific suggestions when error mentions tabs")
|
|
}
|
|
}
|
|
|
|
func TestGetGitHubAPISuggestions_StatusCodes(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
statusCodes := map[string]string{
|
|
"401": "Authentication failed",
|
|
"403": "Access forbidden",
|
|
"404": "not found",
|
|
}
|
|
|
|
for code, expectedText := range statusCodes {
|
|
t.Run("status_"+code, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
context := map[string]string{"status_code": code}
|
|
suggestions := getGitHubAPISuggestions(context)
|
|
allSuggestions := strings.Join(suggestions, " ")
|
|
|
|
if !strings.Contains(allSuggestions, expectedText) {
|
|
t.Errorf("Status code %s suggestions should contain %q", code, expectedText)
|
|
}
|
|
})
|
|
}
|
|
}
|