mirror of
https://github.com/ivuorinen/gh-action-readme.git
synced 2026-03-12 17:00:14 +00:00
refactor: major codebase improvements and test framework overhaul
This commit represents a comprehensive refactoring of the codebase focused on improving code quality, testability, and maintainability. Key improvements: - Implement dependency injection and interface-based architecture - Add comprehensive test framework with fixtures and test suites - Fix all linting issues (errcheck, gosec, staticcheck, goconst, etc.) - Achieve full EditorConfig compliance across all files - Replace hardcoded test data with proper fixture files - Add configuration loader with hierarchical config support - Improve error handling with contextual information - Add progress indicators for better user feedback - Enhance Makefile with help system and improved editorconfig commands - Consolidate constants and remove deprecated code - Strengthen validation logic for GitHub Actions - Add focused consumer interfaces for better separation of concerns Testing improvements: - Add comprehensive integration tests - Implement test executor pattern for better test organization - Create extensive YAML fixture library for testing - Fix all failing tests and improve test coverage - Add validation test fixtures to avoid embedded YAML in Go files Build and tooling: - Update Makefile to show help by default - Fix editorconfig commands to use eclint properly - Add comprehensive help documentation to all make targets - Improve file selection patterns to avoid glob errors This refactoring maintains backward compatibility while significantly improving the internal architecture and developer experience.
This commit is contained in:
@@ -1,6 +1,103 @@
|
||||
// Package testutil provides testing fixtures for gh-action-readme.
|
||||
// Package testutil provides testing fixtures and fixture management for gh-action-readme.
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// 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.
|
||||
func mustReadFixture(filename string) string {
|
||||
_, currentFile, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
panic("failed to get current file path")
|
||||
}
|
||||
|
||||
// Get the project root (go up from testutil/fixtures.go to project root)
|
||||
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
|
||||
if err != nil {
|
||||
panic("failed to read fixture " + filename + ": " + err.Error())
|
||||
}
|
||||
|
||||
return string(content)
|
||||
}
|
||||
|
||||
// Constants for fixture management.
|
||||
const (
|
||||
// YmlExtension represents the standard YAML file extension.
|
||||
YmlExtension = ".yml"
|
||||
// YamlExtension represents the alternative YAML file extension.
|
||||
YamlExtension = ".yaml"
|
||||
)
|
||||
|
||||
// ActionType represents the type of GitHub Action being tested.
|
||||
type ActionType string
|
||||
|
||||
const (
|
||||
// ActionTypeJavaScript represents JavaScript-based GitHub Actions that run on Node.js.
|
||||
ActionTypeJavaScript ActionType = "javascript"
|
||||
// ActionTypeComposite represents composite GitHub Actions that combine multiple steps.
|
||||
ActionTypeComposite ActionType = "composite"
|
||||
// ActionTypeDocker represents Docker-based GitHub Actions that run in containers.
|
||||
ActionTypeDocker ActionType = "docker"
|
||||
// ActionTypeInvalid represents invalid or malformed GitHub Actions for testing error scenarios.
|
||||
ActionTypeInvalid ActionType = "invalid"
|
||||
// ActionTypeMinimal represents minimal GitHub Actions with basic configuration.
|
||||
ActionTypeMinimal ActionType = "minimal"
|
||||
)
|
||||
|
||||
// TestScenario represents a structured test scenario with metadata.
|
||||
type TestScenario struct {
|
||||
ID string `yaml:"id"`
|
||||
Name string `yaml:"name"`
|
||||
Description string `yaml:"description"`
|
||||
ActionType ActionType `yaml:"action_type"`
|
||||
Fixture string `yaml:"fixture"`
|
||||
ExpectValid bool `yaml:"expect_valid"`
|
||||
ExpectError bool `yaml:"expect_error"`
|
||||
Tags []string `yaml:"tags"`
|
||||
Metadata map[string]any `yaml:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// ActionFixture represents a loaded action YAML fixture with metadata.
|
||||
type ActionFixture struct {
|
||||
Name string
|
||||
Path string
|
||||
Content string
|
||||
ActionType ActionType
|
||||
IsValid bool
|
||||
Scenario *TestScenario
|
||||
}
|
||||
|
||||
// ConfigFixture represents a loaded configuration YAML fixture.
|
||||
type ConfigFixture struct {
|
||||
Name string
|
||||
Path string
|
||||
Content string
|
||||
Type string
|
||||
IsValid bool
|
||||
}
|
||||
|
||||
// FixtureManager manages test fixtures and scenarios.
|
||||
type FixtureManager struct {
|
||||
basePath string
|
||||
scenarios map[string]*TestScenario
|
||||
cache map[string]*ActionFixture
|
||||
}
|
||||
|
||||
// GitHub API response fixtures for testing.
|
||||
|
||||
// GitHubReleaseResponse is a mock GitHub release API response.
|
||||
@@ -182,124 +279,6 @@ func MockGitHubResponses() map[string]string {
|
||||
}
|
||||
}
|
||||
|
||||
// Sample action.yml files for testing.
|
||||
|
||||
// SimpleActionYML is a basic GitHub Action YAML.
|
||||
const SimpleActionYML = `name: 'Simple Action'
|
||||
description: 'A simple test action'
|
||||
inputs:
|
||||
input1:
|
||||
description: 'First input'
|
||||
required: true
|
||||
input2:
|
||||
description: 'Second input'
|
||||
required: false
|
||||
default: 'default-value'
|
||||
outputs:
|
||||
output1:
|
||||
description: 'First output'
|
||||
runs:
|
||||
using: 'node20'
|
||||
main: 'index.js'
|
||||
branding:
|
||||
icon: 'activity'
|
||||
color: 'blue'
|
||||
`
|
||||
|
||||
// CompositeActionYML is a composite GitHub Action with dependencies.
|
||||
const CompositeActionYML = `name: 'Composite Action'
|
||||
description: 'A composite action with dependencies'
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version to use'
|
||||
required: true
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '${{ inputs.version }}'
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
shell: bash
|
||||
`
|
||||
|
||||
// DockerActionYML is a Docker-based GitHub Action.
|
||||
const DockerActionYML = `name: 'Docker Action'
|
||||
description: 'A Docker-based action'
|
||||
inputs:
|
||||
dockerfile:
|
||||
description: 'Path to Dockerfile'
|
||||
required: false
|
||||
default: 'Dockerfile'
|
||||
outputs:
|
||||
image:
|
||||
description: 'Built image name'
|
||||
runs:
|
||||
using: 'docker'
|
||||
image: 'Dockerfile'
|
||||
env:
|
||||
CUSTOM_VAR: 'value'
|
||||
branding:
|
||||
icon: 'package'
|
||||
color: 'purple'
|
||||
`
|
||||
|
||||
// InvalidActionYML is an invalid action.yml for error testing.
|
||||
const InvalidActionYML = `name: 'Invalid Action'
|
||||
# Missing required description field
|
||||
inputs:
|
||||
invalid_input:
|
||||
# Missing required description
|
||||
required: true
|
||||
runs:
|
||||
# Invalid using value
|
||||
using: 'invalid-runtime'
|
||||
`
|
||||
|
||||
// MinimalActionYML is a minimal valid action.yml.
|
||||
const MinimalActionYML = `name: 'Minimal Action'
|
||||
description: 'Minimal test action'
|
||||
runs:
|
||||
using: 'node20'
|
||||
main: 'index.js'
|
||||
`
|
||||
|
||||
// Configuration file fixtures.
|
||||
|
||||
// DefaultConfigYAML is a default configuration file.
|
||||
const DefaultConfigYAML = `theme: github
|
||||
output_format: md
|
||||
output_dir: .
|
||||
verbose: false
|
||||
quiet: false
|
||||
`
|
||||
|
||||
// CustomConfigYAML is a custom configuration file.
|
||||
const CustomConfigYAML = `theme: professional
|
||||
output_format: html
|
||||
output_dir: docs
|
||||
template: custom-template.tmpl
|
||||
schema: custom-schema.json
|
||||
verbose: true
|
||||
quiet: false
|
||||
github_token: test-token-from-config
|
||||
`
|
||||
|
||||
// RepoSpecificConfigYAML is a repository-specific configuration.
|
||||
const RepoSpecificConfigYAML = `theme: minimal
|
||||
output_format: json
|
||||
branding:
|
||||
icon: star
|
||||
color: green
|
||||
dependencies:
|
||||
pin_versions: true
|
||||
auto_update: false
|
||||
`
|
||||
|
||||
// GitIgnoreContent is a sample .gitignore file.
|
||||
const GitIgnoreContent = `# Dependencies
|
||||
node_modules/
|
||||
@@ -315,22 +294,493 @@ Thumbs.db
|
||||
`
|
||||
|
||||
// PackageJSONContent is a sample package.json file.
|
||||
const PackageJSONContent = `{
|
||||
"name": "test-action",
|
||||
"version": "1.0.0",
|
||||
"description": "Test GitHub Action",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "jest",
|
||||
"build": "webpack"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.10.0",
|
||||
"@actions/github": "^5.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^29.0.0",
|
||||
"webpack": "^5.0.0"
|
||||
}
|
||||
var PackageJSONContent = func() string {
|
||||
var result string
|
||||
result += "{\n"
|
||||
result += " \"name\": \"test-action\",\n"
|
||||
result += " \"version\": \"1.0.0\",\n"
|
||||
result += " \"description\": \"Test GitHub Action\",\n"
|
||||
result += " \"main\": \"index.js\",\n"
|
||||
result += " \"scripts\": {\n"
|
||||
result += " \"test\": \"jest\",\n"
|
||||
result += " \"build\": \"webpack\"\n"
|
||||
result += " },\n"
|
||||
result += " \"dependencies\": {\n"
|
||||
result += " \"@actions/core\": \"^1.10.0\",\n"
|
||||
result += " \"@actions/github\": \"^5.1.1\"\n"
|
||||
result += " },\n"
|
||||
result += " \"devDependencies\": {\n"
|
||||
result += " \"jest\": \"^29.0.0\",\n"
|
||||
result += " \"webpack\": \"^5.0.0\"\n"
|
||||
result += " }\n"
|
||||
result += "}\n"
|
||||
return result
|
||||
}()
|
||||
|
||||
// NewFixtureManager creates a new fixture manager.
|
||||
func NewFixtureManager() *FixtureManager {
|
||||
_, currentFile, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
panic("failed to get current file path")
|
||||
}
|
||||
|
||||
// Get the project root (go up from testutil/fixtures.go to project root)
|
||||
projectRoot := filepath.Dir(filepath.Dir(currentFile))
|
||||
basePath := filepath.Join(projectRoot, "testdata", "yaml-fixtures")
|
||||
|
||||
return &FixtureManager{
|
||||
basePath: basePath,
|
||||
scenarios: make(map[string]*TestScenario),
|
||||
cache: make(map[string]*ActionFixture),
|
||||
}
|
||||
}
|
||||
|
||||
// LoadScenarios loads test scenarios from the scenarios directory.
|
||||
func (fm *FixtureManager) LoadScenarios() error {
|
||||
scenarioFile := filepath.Join(fm.basePath, "scenarios", "test-scenarios.yml")
|
||||
|
||||
// Create default scenarios if file doesn't exist
|
||||
if _, err := os.Stat(scenarioFile); os.IsNotExist(err) {
|
||||
return fm.createDefaultScenarios(scenarioFile)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(scenarioFile) // #nosec G304 -- test fixture path from project structure
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read scenarios file: %w", err)
|
||||
}
|
||||
|
||||
var scenarios struct {
|
||||
Scenarios []TestScenario `yaml:"scenarios"`
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal(data, &scenarios); err != nil {
|
||||
return fmt.Errorf("failed to parse scenarios YAML: %w", err)
|
||||
}
|
||||
|
||||
for i := range scenarios.Scenarios {
|
||||
scenario := &scenarios.Scenarios[i]
|
||||
fm.scenarios[scenario.ID] = scenario
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadActionFixture loads an action fixture with metadata.
|
||||
func (fm *FixtureManager) LoadActionFixture(name string) (*ActionFixture, error) {
|
||||
// Check cache first
|
||||
if fixture, exists := fm.cache[name]; exists {
|
||||
return fixture, nil
|
||||
}
|
||||
|
||||
// Determine fixture path based on naming convention
|
||||
fixturePath := fm.resolveFixturePath(name)
|
||||
|
||||
content, err := os.ReadFile(fixturePath) // #nosec G304 -- test fixture path resolution
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read fixture %s: %w", name, err)
|
||||
}
|
||||
|
||||
fixture := &ActionFixture{
|
||||
Name: name,
|
||||
Path: fixturePath,
|
||||
Content: string(content),
|
||||
ActionType: fm.determineActionType(name, string(content)),
|
||||
IsValid: fm.validateFixtureContent(string(content)),
|
||||
}
|
||||
|
||||
// Try to find associated scenario
|
||||
if scenario, exists := fm.scenarios[name]; exists {
|
||||
fixture.Scenario = scenario
|
||||
}
|
||||
|
||||
// Cache the fixture
|
||||
fm.cache[name] = fixture
|
||||
|
||||
return fixture, nil
|
||||
}
|
||||
|
||||
// LoadConfigFixture loads a configuration fixture.
|
||||
func (fm *FixtureManager) LoadConfigFixture(name string) (*ConfigFixture, error) {
|
||||
configPath := filepath.Join(fm.basePath, "configs", name)
|
||||
if !strings.HasSuffix(configPath, YmlExtension) && !strings.HasSuffix(configPath, YamlExtension) {
|
||||
configPath += YmlExtension
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(configPath) // #nosec G304 -- test fixture path from project structure
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read config fixture %s: %w", name, err)
|
||||
}
|
||||
|
||||
return &ConfigFixture{
|
||||
Name: name,
|
||||
Path: configPath,
|
||||
Content: string(content),
|
||||
Type: fm.determineConfigType(name),
|
||||
IsValid: fm.validateConfigContent(string(content)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetFixturesByTag returns fixture names matching the specified tags.
|
||||
func (fm *FixtureManager) GetFixturesByTag(tags ...string) []string {
|
||||
var matches []string
|
||||
|
||||
for _, scenario := range fm.scenarios {
|
||||
if fm.scenarioMatchesTags(scenario, tags) {
|
||||
matches = append(matches, scenario.Fixture)
|
||||
}
|
||||
}
|
||||
|
||||
return matches
|
||||
}
|
||||
|
||||
// GetFixturesByActionType returns fixtures of a specific action type.
|
||||
func (fm *FixtureManager) GetFixturesByActionType(actionType ActionType) []string {
|
||||
var matches []string
|
||||
|
||||
for _, scenario := range fm.scenarios {
|
||||
if scenario.ActionType == actionType {
|
||||
matches = append(matches, scenario.Fixture)
|
||||
}
|
||||
}
|
||||
|
||||
return matches
|
||||
}
|
||||
|
||||
// GetValidFixtures returns all fixtures that should parse as valid actions.
|
||||
func (fm *FixtureManager) GetValidFixtures() []string {
|
||||
var matches []string
|
||||
|
||||
for _, scenario := range fm.scenarios {
|
||||
if scenario.ExpectValid {
|
||||
matches = append(matches, scenario.Fixture)
|
||||
}
|
||||
}
|
||||
|
||||
return matches
|
||||
}
|
||||
|
||||
// GetInvalidFixtures returns all fixtures that should be invalid.
|
||||
func (fm *FixtureManager) GetInvalidFixtures() []string {
|
||||
var matches []string
|
||||
|
||||
for _, scenario := range fm.scenarios {
|
||||
if !scenario.ExpectValid {
|
||||
matches = append(matches, scenario.Fixture)
|
||||
}
|
||||
}
|
||||
|
||||
return matches
|
||||
}
|
||||
|
||||
// resolveFixturePath determines the full path to a fixture file.
|
||||
func (fm *FixtureManager) resolveFixturePath(name string) string {
|
||||
// If it's a direct path, use it
|
||||
if strings.Contains(name, "/") {
|
||||
return fm.ensureYamlExtension(filepath.Join(fm.basePath, name))
|
||||
}
|
||||
|
||||
// Try to find the fixture in search directories
|
||||
if foundPath := fm.searchInDirectories(name); foundPath != "" {
|
||||
return foundPath
|
||||
}
|
||||
|
||||
// Default to root level if not found
|
||||
return fm.ensureYamlExtension(filepath.Join(fm.basePath, name))
|
||||
}
|
||||
|
||||
// ensureYamlExtension adds YAML extension if not present.
|
||||
func (fm *FixtureManager) ensureYamlExtension(path string) string {
|
||||
if !strings.HasSuffix(path, YmlExtension) && !strings.HasSuffix(path, YamlExtension) {
|
||||
path += YmlExtension
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// searchInDirectories searches for fixture in predefined directories.
|
||||
func (fm *FixtureManager) searchInDirectories(name string) string {
|
||||
searchDirs := []string{
|
||||
"actions/javascript",
|
||||
"actions/composite",
|
||||
"actions/docker",
|
||||
"actions/invalid",
|
||||
"", // root level
|
||||
}
|
||||
|
||||
for _, dir := range searchDirs {
|
||||
path := fm.buildSearchPath(dir, name)
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return path
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// buildSearchPath constructs search path for a directory.
|
||||
func (fm *FixtureManager) buildSearchPath(dir, name string) string {
|
||||
var path string
|
||||
if dir == "" {
|
||||
path = filepath.Join(fm.basePath, name)
|
||||
} else {
|
||||
path = filepath.Join(fm.basePath, dir, name)
|
||||
}
|
||||
return fm.ensureYamlExtension(path)
|
||||
}
|
||||
|
||||
// determineActionType infers action type from fixture name and content.
|
||||
func (fm *FixtureManager) determineActionType(name, content string) ActionType {
|
||||
// Check by name/path first
|
||||
if actionType := fm.determineActionTypeByName(name); actionType != ActionTypeMinimal {
|
||||
return actionType
|
||||
}
|
||||
|
||||
// Fall back to content analysis
|
||||
return fm.determineActionTypeByContent(content)
|
||||
}
|
||||
|
||||
// determineActionTypeByName infers action type from fixture name or path.
|
||||
func (fm *FixtureManager) determineActionTypeByName(name string) ActionType {
|
||||
if strings.Contains(name, "javascript") || strings.Contains(name, "node") {
|
||||
return ActionTypeJavaScript
|
||||
}
|
||||
if strings.Contains(name, "composite") {
|
||||
return ActionTypeComposite
|
||||
}
|
||||
if strings.Contains(name, "docker") {
|
||||
return ActionTypeDocker
|
||||
}
|
||||
if strings.Contains(name, "invalid") {
|
||||
return ActionTypeInvalid
|
||||
}
|
||||
if strings.Contains(name, "minimal") {
|
||||
return ActionTypeMinimal
|
||||
}
|
||||
return ActionTypeMinimal
|
||||
}
|
||||
|
||||
// determineActionTypeByContent infers action type from YAML content.
|
||||
func (fm *FixtureManager) determineActionTypeByContent(content string) ActionType {
|
||||
if strings.Contains(content, `using: 'composite'`) || strings.Contains(content, `using: "composite"`) {
|
||||
return ActionTypeComposite
|
||||
}
|
||||
if strings.Contains(content, `using: 'docker'`) || strings.Contains(content, `using: "docker"`) {
|
||||
return ActionTypeDocker
|
||||
}
|
||||
if strings.Contains(content, `using: 'node`) {
|
||||
return ActionTypeJavaScript
|
||||
}
|
||||
return ActionTypeMinimal
|
||||
}
|
||||
|
||||
// determineConfigType determines the type of configuration fixture.
|
||||
func (fm *FixtureManager) determineConfigType(name string) string {
|
||||
if strings.Contains(name, "global") {
|
||||
return "global"
|
||||
}
|
||||
if strings.Contains(name, "repo") {
|
||||
return "repo-specific"
|
||||
}
|
||||
if strings.Contains(name, "user") {
|
||||
return "user-specific"
|
||||
}
|
||||
return "generic"
|
||||
}
|
||||
|
||||
// validateFixtureContent performs basic validation on fixture content.
|
||||
func (fm *FixtureManager) validateFixtureContent(content string) bool {
|
||||
// Basic YAML structure validation
|
||||
var data map[string]any
|
||||
if err := yaml.Unmarshal([]byte(content), &data); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check for required fields for valid actions
|
||||
if _, hasName := data["name"]; !hasName {
|
||||
return false
|
||||
}
|
||||
if _, hasDescription := data["description"]; !hasDescription {
|
||||
return false
|
||||
}
|
||||
runs, hasRuns := data["runs"]
|
||||
if !hasRuns {
|
||||
return false
|
||||
}
|
||||
|
||||
// Validate the runs section content more thoroughly
|
||||
runsMap, ok := runs.(map[string]any)
|
||||
if !ok {
|
||||
return false // runs field exists but is not a map
|
||||
}
|
||||
|
||||
using, hasUsing := runsMap["using"]
|
||||
if !hasUsing {
|
||||
return false // runs section exists but has no using field
|
||||
}
|
||||
|
||||
usingStr, ok := using.(string)
|
||||
if !ok {
|
||||
return false // using field exists but is not a string
|
||||
}
|
||||
|
||||
// Use the same validation logic as ValidateActionYML
|
||||
if !isValidRuntime(usingStr) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// isValidRuntime checks if the given runtime is valid for GitHub Actions.
|
||||
// This is duplicated from internal/validator.go to avoid import cycle.
|
||||
func isValidRuntime(runtime string) bool {
|
||||
validRuntimes := []string{
|
||||
"node12", // Legacy Node.js runtime (deprecated)
|
||||
"node16", // Legacy Node.js runtime (deprecated)
|
||||
"node20", // Current Node.js runtime
|
||||
"docker", // Docker container runtime
|
||||
"composite", // Composite action runtime
|
||||
}
|
||||
|
||||
runtime = strings.TrimSpace(strings.ToLower(runtime))
|
||||
for _, valid := range validRuntimes {
|
||||
if runtime == valid {
|
||||
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
|
||||
}
|
||||
|
||||
// scenarioMatchesTags checks if a scenario matches any of the provided tags.
|
||||
func (fm *FixtureManager) scenarioMatchesTags(scenario *TestScenario, tags []string) bool {
|
||||
if len(tags) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, tag := range tags {
|
||||
for _, scenarioTag := range scenario.Tags {
|
||||
if tag == scenarioTag {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// createDefaultScenarios creates a default scenarios file.
|
||||
func (fm *FixtureManager) createDefaultScenarios(scenarioFile string) error {
|
||||
// Ensure the directory exists
|
||||
if err := os.MkdirAll(filepath.Dir(scenarioFile), 0750); err != nil { // #nosec G301 -- test directory permissions
|
||||
return fmt.Errorf("failed to create scenarios directory: %w", err)
|
||||
}
|
||||
|
||||
defaultScenarios := struct {
|
||||
Scenarios []TestScenario `yaml:"scenarios"`
|
||||
}{
|
||||
Scenarios: []TestScenario{
|
||||
{
|
||||
ID: "simple-javascript",
|
||||
Name: "Simple JavaScript Action",
|
||||
Description: "Basic JavaScript action with minimal configuration",
|
||||
ActionType: ActionTypeJavaScript,
|
||||
Fixture: "actions/javascript/simple.yml",
|
||||
ExpectValid: true,
|
||||
ExpectError: false,
|
||||
Tags: []string{"javascript", "basic", "valid"},
|
||||
},
|
||||
{
|
||||
ID: "composite-basic",
|
||||
Name: "Basic Composite Action",
|
||||
Description: "Composite action with multiple steps",
|
||||
ActionType: ActionTypeComposite,
|
||||
Fixture: "actions/composite/basic.yml",
|
||||
ExpectValid: true,
|
||||
ExpectError: false,
|
||||
Tags: []string{"composite", "basic", "valid"},
|
||||
},
|
||||
{
|
||||
ID: "docker-basic",
|
||||
Name: "Basic Docker Action",
|
||||
Description: "Docker-based action with Dockerfile",
|
||||
ActionType: ActionTypeDocker,
|
||||
Fixture: "actions/docker/basic.yml",
|
||||
ExpectValid: true,
|
||||
ExpectError: false,
|
||||
Tags: []string{"docker", "basic", "valid"},
|
||||
},
|
||||
{
|
||||
ID: "invalid-missing-description",
|
||||
Name: "Invalid Action - Missing Description",
|
||||
Description: "Action missing required description field",
|
||||
ActionType: ActionTypeInvalid,
|
||||
Fixture: "actions/invalid/missing-description.yml",
|
||||
ExpectValid: false,
|
||||
ExpectError: true,
|
||||
Tags: []string{"invalid", "validation", "error"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
data, err := yaml.Marshal(&defaultScenarios)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal default scenarios: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(scenarioFile, data, 0600); err != nil {
|
||||
return fmt.Errorf("failed to write scenarios file: %w", err)
|
||||
}
|
||||
|
||||
// Load the scenarios we just created
|
||||
return fm.LoadScenarios()
|
||||
}
|
||||
|
||||
// Global fixture manager instance.
|
||||
var defaultFixtureManager *FixtureManager
|
||||
|
||||
// GetFixtureManager returns the global fixture manager instance.
|
||||
func GetFixtureManager() *FixtureManager {
|
||||
if defaultFixtureManager == nil {
|
||||
defaultFixtureManager = NewFixtureManager()
|
||||
if err := defaultFixtureManager.LoadScenarios(); err != nil {
|
||||
panic(fmt.Sprintf("failed to load test scenarios: %v", err))
|
||||
}
|
||||
}
|
||||
return defaultFixtureManager
|
||||
}
|
||||
|
||||
// Helper functions for backward compatibility and convenience
|
||||
|
||||
// LoadActionFixture loads an action fixture using the global fixture manager.
|
||||
func LoadActionFixture(name string) (*ActionFixture, error) {
|
||||
return GetFixtureManager().LoadActionFixture(name)
|
||||
}
|
||||
|
||||
// LoadConfigFixture loads a config fixture using the global fixture manager.
|
||||
func LoadConfigFixture(name string) (*ConfigFixture, error) {
|
||||
return GetFixtureManager().LoadConfigFixture(name)
|
||||
}
|
||||
|
||||
// GetFixturesByTag returns fixtures matching tags using the global fixture manager.
|
||||
func GetFixturesByTag(tags ...string) []string {
|
||||
return GetFixtureManager().GetFixturesByTag(tags...)
|
||||
}
|
||||
|
||||
// GetFixturesByActionType returns fixtures by action type using the global fixture manager.
|
||||
func GetFixturesByActionType(actionType ActionType) []string {
|
||||
return GetFixtureManager().GetFixturesByActionType(actionType)
|
||||
}
|
||||
|
||||
// GetValidFixtures returns all valid fixtures using the global fixture manager.
|
||||
func GetValidFixtures() []string {
|
||||
return GetFixtureManager().GetValidFixtures()
|
||||
}
|
||||
|
||||
// GetInvalidFixtures returns all invalid fixtures using the global fixture manager.
|
||||
func GetInvalidFixtures() []string {
|
||||
return GetFixtureManager().GetInvalidFixtures()
|
||||
}
|
||||
`
|
||||
|
||||
Reference in New Issue
Block a user