168 lines
3.7 KiB
Go
168 lines
3.7 KiB
Go
|
|
package devops
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"encoding/json"
|
||
|
|
"fmt"
|
||
|
|
"os"
|
||
|
|
"path/filepath"
|
||
|
|
"strings"
|
||
|
|
|
||
|
|
"gopkg.in/yaml.v3"
|
||
|
|
)
|
||
|
|
|
||
|
|
// TestConfig holds test configuration from .core/test.yaml.
|
||
|
|
type TestConfig struct {
|
||
|
|
Version int `yaml:"version"`
|
||
|
|
Command string `yaml:"command,omitempty"`
|
||
|
|
Commands []TestCommand `yaml:"commands,omitempty"`
|
||
|
|
Env map[string]string `yaml:"env,omitempty"`
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestCommand is a named test command.
|
||
|
|
type TestCommand struct {
|
||
|
|
Name string `yaml:"name"`
|
||
|
|
Run string `yaml:"run"`
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestOptions configures test execution.
|
||
|
|
type TestOptions struct {
|
||
|
|
Name string // Run specific named command from .core/test.yaml
|
||
|
|
Command []string // Override command (from -- args)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Test runs tests in the dev environment.
|
||
|
|
func (d *DevOps) Test(ctx context.Context, projectDir string, opts TestOptions) error {
|
||
|
|
running, err := d.IsRunning(ctx)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if !running {
|
||
|
|
return fmt.Errorf("dev environment not running (run 'core dev boot' first)")
|
||
|
|
}
|
||
|
|
|
||
|
|
var cmd string
|
||
|
|
|
||
|
|
// Priority: explicit command > named command > auto-detect
|
||
|
|
if len(opts.Command) > 0 {
|
||
|
|
cmd = strings.Join(opts.Command, " ")
|
||
|
|
} else if opts.Name != "" {
|
||
|
|
cfg, err := LoadTestConfig(projectDir)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
for _, c := range cfg.Commands {
|
||
|
|
if c.Name == opts.Name {
|
||
|
|
cmd = c.Run
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if cmd == "" {
|
||
|
|
return fmt.Errorf("test command %q not found in .core/test.yaml", opts.Name)
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
cmd = DetectTestCommand(projectDir)
|
||
|
|
if cmd == "" {
|
||
|
|
return fmt.Errorf("could not detect test command (create .core/test.yaml)")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Run via SSH - construct command as single string for shell execution
|
||
|
|
return d.sshShell(ctx, []string{"cd", "/app", "&&", cmd})
|
||
|
|
}
|
||
|
|
|
||
|
|
// DetectTestCommand auto-detects the test command for a project.
|
||
|
|
func DetectTestCommand(projectDir string) string {
|
||
|
|
// 1. Check .core/test.yaml
|
||
|
|
cfg, err := LoadTestConfig(projectDir)
|
||
|
|
if err == nil && cfg.Command != "" {
|
||
|
|
return cfg.Command
|
||
|
|
}
|
||
|
|
|
||
|
|
// 2. Check composer.json for test script
|
||
|
|
if hasFile(projectDir, "composer.json") {
|
||
|
|
if hasComposerScript(projectDir, "test") {
|
||
|
|
return "composer test"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 3. Check package.json for test script
|
||
|
|
if hasFile(projectDir, "package.json") {
|
||
|
|
if hasPackageScript(projectDir, "test") {
|
||
|
|
return "npm test"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 4. Check go.mod
|
||
|
|
if hasFile(projectDir, "go.mod") {
|
||
|
|
return "go test ./..."
|
||
|
|
}
|
||
|
|
|
||
|
|
// 5. Check pytest
|
||
|
|
if hasFile(projectDir, "pytest.ini") || hasFile(projectDir, "pyproject.toml") {
|
||
|
|
return "pytest"
|
||
|
|
}
|
||
|
|
|
||
|
|
// 6. Check Taskfile
|
||
|
|
if hasFile(projectDir, "Taskfile.yaml") || hasFile(projectDir, "Taskfile.yml") {
|
||
|
|
return "task test"
|
||
|
|
}
|
||
|
|
|
||
|
|
return ""
|
||
|
|
}
|
||
|
|
|
||
|
|
// LoadTestConfig loads .core/test.yaml.
|
||
|
|
func LoadTestConfig(projectDir string) (*TestConfig, error) {
|
||
|
|
path := filepath.Join(projectDir, ".core", "test.yaml")
|
||
|
|
data, err := os.ReadFile(path)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
|
||
|
|
var cfg TestConfig
|
||
|
|
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
|
||
|
|
return &cfg, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func hasFile(dir, name string) bool {
|
||
|
|
_, err := os.Stat(filepath.Join(dir, name))
|
||
|
|
return err == nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func hasPackageScript(projectDir, script string) bool {
|
||
|
|
data, err := os.ReadFile(filepath.Join(projectDir, "package.json"))
|
||
|
|
if err != nil {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
var pkg struct {
|
||
|
|
Scripts map[string]string `json:"scripts"`
|
||
|
|
}
|
||
|
|
if err := json.Unmarshal(data, &pkg); err != nil {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
_, ok := pkg.Scripts[script]
|
||
|
|
return ok
|
||
|
|
}
|
||
|
|
|
||
|
|
func hasComposerScript(projectDir, script string) bool {
|
||
|
|
data, err := os.ReadFile(filepath.Join(projectDir, "composer.json"))
|
||
|
|
if err != nil {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
var pkg struct {
|
||
|
|
Scripts map[string]interface{} `json:"scripts"`
|
||
|
|
}
|
||
|
|
if err := json.Unmarshal(data, &pkg); err != nil {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
_, ok := pkg.Scripts[script]
|
||
|
|
return ok
|
||
|
|
}
|