refactor(php): remove QA CLI commands — moved to core/lint
QA subcommands (fmt, stan, psalm, audit, security, rector, infection, test, qa) now live in core/lint cmd/qa/. Library code (quality.go, testing.go) retained for cmd_ci.go. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ad8af2fb83
commit
a9c1afe492
6 changed files with 1 additions and 2383 deletions
26
cmd.go
26
cmd.go
|
|
@ -49,20 +49,9 @@ var (
|
|||
phpStatusError = cli.ErrorStyle
|
||||
)
|
||||
|
||||
// QA command styles (from shared)
|
||||
// QA command styles (from shared) — most moved to core/lint
|
||||
var (
|
||||
phpQAPassedStyle = cli.SuccessStyle
|
||||
phpQAFailedStyle = cli.ErrorStyle
|
||||
phpQAWarningStyle = cli.WarningStyle
|
||||
phpQAStageStyle = cli.HeaderStyle
|
||||
)
|
||||
|
||||
// Security severity styles (from shared)
|
||||
var (
|
||||
phpSecurityCriticalStyle = cli.NewStyle().Bold().Foreground(cli.ColourRed500)
|
||||
phpSecurityHighStyle = cli.NewStyle().Bold().Foreground(cli.ColourOrange500)
|
||||
phpSecurityMediumStyle = cli.NewStyle().Foreground(cli.ColourAmber500)
|
||||
phpSecurityLowStyle = cli.NewStyle().Foreground(cli.ColourGray500)
|
||||
)
|
||||
|
||||
// AddPHPCommands adds PHP/Laravel development commands.
|
||||
|
|
@ -128,19 +117,6 @@ func AddPHPCommands(root *cli.Command) {
|
|||
addPHPServeCommand(phpCmd)
|
||||
addPHPShellCommand(phpCmd)
|
||||
|
||||
// Quality (existing)
|
||||
addPHPTestCommand(phpCmd)
|
||||
addPHPFmtCommand(phpCmd)
|
||||
addPHPStanCommand(phpCmd)
|
||||
|
||||
// Quality (new)
|
||||
addPHPPsalmCommand(phpCmd)
|
||||
addPHPAuditCommand(phpCmd)
|
||||
addPHPSecurityCommand(phpCmd)
|
||||
addPHPQACommand(phpCmd)
|
||||
addPHPRectorCommand(phpCmd)
|
||||
addPHPInfectionCommand(phpCmd)
|
||||
|
||||
// CI/CD Integration
|
||||
addPHPCICommand(phpCmd)
|
||||
|
||||
|
|
|
|||
343
cmd_qa_runner.go
343
cmd_qa_runner.go
|
|
@ -1,343 +0,0 @@
|
|||
package php
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"forge.lthn.ai/core/cli/pkg/cli"
|
||||
"forge.lthn.ai/core/go/pkg/core"
|
||||
"forge.lthn.ai/core/go-i18n"
|
||||
"forge.lthn.ai/core/go-process"
|
||||
)
|
||||
|
||||
// QARunner orchestrates PHP QA checks using pkg/process.
|
||||
type QARunner struct {
|
||||
dir string
|
||||
fix bool
|
||||
service *process.Service
|
||||
core *core.Core
|
||||
|
||||
// Output tracking
|
||||
outputMu sync.Mutex
|
||||
checkOutputs map[string][]string
|
||||
}
|
||||
|
||||
// NewQARunner creates a QA runner for the given directory.
|
||||
func NewQARunner(dir string, fix bool) (*QARunner, error) {
|
||||
// Create a Core with process service for the QA session
|
||||
app, err := core.New(
|
||||
core.WithName("process", process.NewService(process.Options{})),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, cli.WrapVerb(err, "create", "process service")
|
||||
}
|
||||
|
||||
svc, err := core.ServiceFor[*process.Service](app, "process")
|
||||
if err != nil {
|
||||
return nil, cli.WrapVerb(err, "get", "process service")
|
||||
}
|
||||
|
||||
runner := &QARunner{
|
||||
dir: dir,
|
||||
fix: fix,
|
||||
service: svc,
|
||||
core: app,
|
||||
checkOutputs: make(map[string][]string),
|
||||
}
|
||||
|
||||
return runner, nil
|
||||
}
|
||||
|
||||
// BuildSpecs creates RunSpecs for the given QA checks.
|
||||
func (r *QARunner) BuildSpecs(checks []string) []process.RunSpec {
|
||||
specs := make([]process.RunSpec, 0, len(checks))
|
||||
|
||||
for _, check := range checks {
|
||||
spec := r.buildSpec(check)
|
||||
if spec != nil {
|
||||
specs = append(specs, *spec)
|
||||
}
|
||||
}
|
||||
|
||||
return specs
|
||||
}
|
||||
|
||||
// buildSpec creates a RunSpec for a single check.
|
||||
func (r *QARunner) buildSpec(check string) *process.RunSpec {
|
||||
switch check {
|
||||
case "audit":
|
||||
return &process.RunSpec{
|
||||
Name: "audit",
|
||||
Command: "composer",
|
||||
Args: []string{"audit", "--format=summary"},
|
||||
Dir: r.dir,
|
||||
}
|
||||
|
||||
case "fmt":
|
||||
m := getMedium()
|
||||
formatter, found := DetectFormatter(r.dir)
|
||||
if !found {
|
||||
return nil
|
||||
}
|
||||
if formatter == FormatterPint {
|
||||
vendorBin := filepath.Join(r.dir, "vendor", "bin", "pint")
|
||||
cmd := "pint"
|
||||
if m.IsFile(vendorBin) {
|
||||
cmd = vendorBin
|
||||
}
|
||||
args := []string{}
|
||||
if !r.fix {
|
||||
args = append(args, "--test")
|
||||
}
|
||||
return &process.RunSpec{
|
||||
Name: "fmt",
|
||||
Command: cmd,
|
||||
Args: args,
|
||||
Dir: r.dir,
|
||||
After: []string{"audit"},
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
case "stan":
|
||||
m := getMedium()
|
||||
_, found := DetectAnalyser(r.dir)
|
||||
if !found {
|
||||
return nil
|
||||
}
|
||||
vendorBin := filepath.Join(r.dir, "vendor", "bin", "phpstan")
|
||||
cmd := "phpstan"
|
||||
if m.IsFile(vendorBin) {
|
||||
cmd = vendorBin
|
||||
}
|
||||
return &process.RunSpec{
|
||||
Name: "stan",
|
||||
Command: cmd,
|
||||
Args: []string{"analyse", "--no-progress"},
|
||||
Dir: r.dir,
|
||||
After: []string{"fmt"},
|
||||
}
|
||||
|
||||
case "psalm":
|
||||
m := getMedium()
|
||||
_, found := DetectPsalm(r.dir)
|
||||
if !found {
|
||||
return nil
|
||||
}
|
||||
vendorBin := filepath.Join(r.dir, "vendor", "bin", "psalm")
|
||||
cmd := "psalm"
|
||||
if m.IsFile(vendorBin) {
|
||||
cmd = vendorBin
|
||||
}
|
||||
args := []string{"--no-progress"}
|
||||
if r.fix {
|
||||
args = append(args, "--alter", "--issues=all")
|
||||
}
|
||||
return &process.RunSpec{
|
||||
Name: "psalm",
|
||||
Command: cmd,
|
||||
Args: args,
|
||||
Dir: r.dir,
|
||||
After: []string{"stan"},
|
||||
}
|
||||
|
||||
case "test":
|
||||
m := getMedium()
|
||||
// Check for Pest first, fall back to PHPUnit
|
||||
pestBin := filepath.Join(r.dir, "vendor", "bin", "pest")
|
||||
phpunitBin := filepath.Join(r.dir, "vendor", "bin", "phpunit")
|
||||
|
||||
var cmd string
|
||||
if m.IsFile(pestBin) {
|
||||
cmd = pestBin
|
||||
} else if m.IsFile(phpunitBin) {
|
||||
cmd = phpunitBin
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Tests depend on stan (or psalm if available)
|
||||
after := []string{"stan"}
|
||||
if _, found := DetectPsalm(r.dir); found {
|
||||
after = []string{"psalm"}
|
||||
}
|
||||
|
||||
return &process.RunSpec{
|
||||
Name: "test",
|
||||
Command: cmd,
|
||||
Args: []string{},
|
||||
Dir: r.dir,
|
||||
After: after,
|
||||
}
|
||||
|
||||
case "rector":
|
||||
m := getMedium()
|
||||
if !DetectRector(r.dir) {
|
||||
return nil
|
||||
}
|
||||
vendorBin := filepath.Join(r.dir, "vendor", "bin", "rector")
|
||||
cmd := "rector"
|
||||
if m.IsFile(vendorBin) {
|
||||
cmd = vendorBin
|
||||
}
|
||||
args := []string{"process"}
|
||||
if !r.fix {
|
||||
args = append(args, "--dry-run")
|
||||
}
|
||||
return &process.RunSpec{
|
||||
Name: "rector",
|
||||
Command: cmd,
|
||||
Args: args,
|
||||
Dir: r.dir,
|
||||
After: []string{"test"},
|
||||
AllowFailure: true, // Dry-run returns non-zero if changes would be made
|
||||
}
|
||||
|
||||
case "infection":
|
||||
m := getMedium()
|
||||
if !DetectInfection(r.dir) {
|
||||
return nil
|
||||
}
|
||||
vendorBin := filepath.Join(r.dir, "vendor", "bin", "infection")
|
||||
cmd := "infection"
|
||||
if m.IsFile(vendorBin) {
|
||||
cmd = vendorBin
|
||||
}
|
||||
return &process.RunSpec{
|
||||
Name: "infection",
|
||||
Command: cmd,
|
||||
Args: []string{"--min-msi=50", "--min-covered-msi=70", "--threads=4"},
|
||||
Dir: r.dir,
|
||||
After: []string{"test"},
|
||||
AllowFailure: true,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run executes all QA checks and returns the results.
|
||||
func (r *QARunner) Run(ctx context.Context, stages []QAStage) (*QARunResult, error) {
|
||||
// Collect all checks from all stages
|
||||
var allChecks []string
|
||||
for _, stage := range stages {
|
||||
checks := GetQAChecks(r.dir, stage)
|
||||
allChecks = append(allChecks, checks...)
|
||||
}
|
||||
|
||||
if len(allChecks) == 0 {
|
||||
return &QARunResult{Passed: true}, nil
|
||||
}
|
||||
|
||||
// Build specs
|
||||
specs := r.BuildSpecs(allChecks)
|
||||
if len(specs) == 0 {
|
||||
return &QARunResult{Passed: true}, nil
|
||||
}
|
||||
|
||||
// Register output handler
|
||||
r.core.RegisterAction(func(c *core.Core, msg core.Message) error {
|
||||
switch m := msg.(type) {
|
||||
case process.ActionProcessOutput:
|
||||
r.outputMu.Lock()
|
||||
// Extract check name from process ID mapping
|
||||
for _, spec := range specs {
|
||||
if strings.Contains(m.ID, spec.Name) || m.ID != "" {
|
||||
// Store output for later display if needed
|
||||
r.checkOutputs[spec.Name] = append(r.checkOutputs[spec.Name], m.Line)
|
||||
break
|
||||
}
|
||||
}
|
||||
r.outputMu.Unlock()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// Create runner and execute
|
||||
runner := process.NewRunner(r.service)
|
||||
result, err := runner.RunAll(ctx, specs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert to QA result
|
||||
qaResult := &QARunResult{
|
||||
Passed: result.Success(),
|
||||
Duration: result.Duration.String(),
|
||||
Results: make([]QACheckRunResult, 0, len(result.Results)),
|
||||
}
|
||||
|
||||
for _, res := range result.Results {
|
||||
qaResult.Results = append(qaResult.Results, QACheckRunResult{
|
||||
Name: res.Name,
|
||||
Passed: res.Passed(),
|
||||
Skipped: res.Skipped,
|
||||
ExitCode: res.ExitCode,
|
||||
Duration: res.Duration.String(),
|
||||
Output: res.Output,
|
||||
})
|
||||
if res.Passed() {
|
||||
qaResult.PassedCount++
|
||||
} else if res.Skipped {
|
||||
qaResult.SkippedCount++
|
||||
} else {
|
||||
qaResult.FailedCount++
|
||||
}
|
||||
}
|
||||
|
||||
return qaResult, nil
|
||||
}
|
||||
|
||||
// GetCheckOutput returns captured output for a check.
|
||||
func (r *QARunner) GetCheckOutput(check string) []string {
|
||||
r.outputMu.Lock()
|
||||
defer r.outputMu.Unlock()
|
||||
return r.checkOutputs[check]
|
||||
}
|
||||
|
||||
// QARunResult holds the results of running QA checks.
|
||||
type QARunResult struct {
|
||||
Passed bool `json:"passed"`
|
||||
Duration string `json:"duration"`
|
||||
Results []QACheckRunResult `json:"results"`
|
||||
PassedCount int `json:"passed_count"`
|
||||
FailedCount int `json:"failed_count"`
|
||||
SkippedCount int `json:"skipped_count"`
|
||||
}
|
||||
|
||||
// QACheckRunResult holds the result of a single QA check.
|
||||
type QACheckRunResult struct {
|
||||
Name string `json:"name"`
|
||||
Passed bool `json:"passed"`
|
||||
Skipped bool `json:"skipped"`
|
||||
ExitCode int `json:"exit_code"`
|
||||
Duration string `json:"duration"`
|
||||
Output string `json:"output,omitempty"`
|
||||
}
|
||||
|
||||
// GetIssueMessage returns an issue message for a check.
|
||||
func (r QACheckRunResult) GetIssueMessage() string {
|
||||
if r.Passed || r.Skipped {
|
||||
return ""
|
||||
}
|
||||
switch r.Name {
|
||||
case "audit":
|
||||
return i18n.T("i18n.done.find", "vulnerabilities")
|
||||
case "fmt":
|
||||
return i18n.T("i18n.done.find", "style issues")
|
||||
case "stan":
|
||||
return i18n.T("i18n.done.find", "analysis errors")
|
||||
case "psalm":
|
||||
return i18n.T("i18n.done.find", "type errors")
|
||||
case "test":
|
||||
return i18n.T("i18n.done.fail", "tests")
|
||||
case "rector":
|
||||
return i18n.T("i18n.done.find", "refactoring suggestions")
|
||||
case "infection":
|
||||
return i18n.T("i18n.fail.pass", "mutation testing")
|
||||
default:
|
||||
return i18n.T("i18n.done.find", "issues")
|
||||
}
|
||||
}
|
||||
814
cmd_quality.go
814
cmd_quality.go
|
|
@ -1,814 +0,0 @@
|
|||
package php
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"forge.lthn.ai/core/cli/pkg/cli"
|
||||
"forge.lthn.ai/core/go-i18n"
|
||||
)
|
||||
|
||||
var (
|
||||
testParallel bool
|
||||
testCoverage bool
|
||||
testFilter string
|
||||
testGroup string
|
||||
testJSON bool
|
||||
)
|
||||
|
||||
func addPHPTestCommand(parent *cli.Command) {
|
||||
testCmd := &cli.Command{
|
||||
Use: "test",
|
||||
Short: i18n.T("cmd.php.test.short"),
|
||||
Long: i18n.T("cmd.php.test.long"),
|
||||
RunE: func(cmd *cli.Command, args []string) error {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err)
|
||||
}
|
||||
|
||||
if !IsPHPProject(cwd) {
|
||||
return errors.New(i18n.T("cmd.php.error.not_php"))
|
||||
}
|
||||
|
||||
if !testJSON {
|
||||
cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.ProgressSubject("run", "tests"))
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
opts := TestOptions{
|
||||
Dir: cwd,
|
||||
Filter: testFilter,
|
||||
Parallel: testParallel,
|
||||
Coverage: testCoverage,
|
||||
JUnit: testJSON,
|
||||
Output: os.Stdout,
|
||||
}
|
||||
|
||||
if testGroup != "" {
|
||||
opts.Groups = []string{testGroup}
|
||||
}
|
||||
|
||||
if err := RunTests(ctx, opts); err != nil {
|
||||
return cli.Err("%s: %w", i18n.T("i18n.fail.run", "tests"), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
testCmd.Flags().BoolVar(&testParallel, "parallel", false, i18n.T("cmd.php.test.flag.parallel"))
|
||||
testCmd.Flags().BoolVar(&testCoverage, "coverage", false, i18n.T("cmd.php.test.flag.coverage"))
|
||||
testCmd.Flags().StringVar(&testFilter, "filter", "", i18n.T("cmd.php.test.flag.filter"))
|
||||
testCmd.Flags().StringVar(&testGroup, "group", "", i18n.T("cmd.php.test.flag.group"))
|
||||
testCmd.Flags().BoolVar(&testJSON, "junit", false, i18n.T("cmd.php.test.flag.junit"))
|
||||
|
||||
parent.AddCommand(testCmd)
|
||||
}
|
||||
|
||||
var (
|
||||
fmtFix bool
|
||||
fmtDiff bool
|
||||
fmtJSON bool
|
||||
)
|
||||
|
||||
func addPHPFmtCommand(parent *cli.Command) {
|
||||
fmtCmd := &cli.Command{
|
||||
Use: "fmt [paths...]",
|
||||
Short: i18n.T("cmd.php.fmt.short"),
|
||||
Long: i18n.T("cmd.php.fmt.long"),
|
||||
RunE: func(cmd *cli.Command, args []string) error {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err)
|
||||
}
|
||||
|
||||
if !IsPHPProject(cwd) {
|
||||
return errors.New(i18n.T("cmd.php.error.not_php"))
|
||||
}
|
||||
|
||||
// Detect formatter
|
||||
formatter, found := DetectFormatter(cwd)
|
||||
if !found {
|
||||
return errors.New(i18n.T("cmd.php.fmt.no_formatter"))
|
||||
}
|
||||
|
||||
if !fmtJSON {
|
||||
var msg string
|
||||
if fmtFix {
|
||||
msg = i18n.T("cmd.php.fmt.formatting", map[string]interface{}{"Formatter": formatter})
|
||||
} else {
|
||||
msg = i18n.ProgressSubject("check", "code style")
|
||||
}
|
||||
cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), msg)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
opts := FormatOptions{
|
||||
Dir: cwd,
|
||||
Fix: fmtFix,
|
||||
Diff: fmtDiff,
|
||||
JSON: fmtJSON,
|
||||
Output: os.Stdout,
|
||||
}
|
||||
|
||||
// Get any additional paths from args
|
||||
if len(args) > 0 {
|
||||
opts.Paths = args
|
||||
}
|
||||
|
||||
if err := Format(ctx, opts); err != nil {
|
||||
if fmtFix {
|
||||
return cli.Err("%s: %w", i18n.T("cmd.php.error.fmt_failed"), err)
|
||||
}
|
||||
return cli.Err("%s: %w", i18n.T("cmd.php.error.fmt_issues"), err)
|
||||
}
|
||||
|
||||
if !fmtJSON {
|
||||
if fmtFix {
|
||||
cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("common.success.completed", map[string]any{"Action": "Code formatted"}))
|
||||
} else {
|
||||
cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("cmd.php.fmt.no_issues"))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
fmtCmd.Flags().BoolVar(&fmtFix, "fix", false, i18n.T("cmd.php.fmt.flag.fix"))
|
||||
fmtCmd.Flags().BoolVar(&fmtDiff, "diff", false, i18n.T("common.flag.diff"))
|
||||
fmtCmd.Flags().BoolVar(&fmtJSON, "json", false, i18n.T("common.flag.json"))
|
||||
|
||||
parent.AddCommand(fmtCmd)
|
||||
}
|
||||
|
||||
var (
|
||||
stanLevel int
|
||||
stanMemory string
|
||||
stanJSON bool
|
||||
stanSARIF bool
|
||||
)
|
||||
|
||||
func addPHPStanCommand(parent *cli.Command) {
|
||||
stanCmd := &cli.Command{
|
||||
Use: "stan [paths...]",
|
||||
Short: i18n.T("cmd.php.analyse.short"),
|
||||
Long: i18n.T("cmd.php.analyse.long"),
|
||||
RunE: func(cmd *cli.Command, args []string) error {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err)
|
||||
}
|
||||
|
||||
if !IsPHPProject(cwd) {
|
||||
return errors.New(i18n.T("cmd.php.error.not_php"))
|
||||
}
|
||||
|
||||
// Detect analyser
|
||||
_, found := DetectAnalyser(cwd)
|
||||
if !found {
|
||||
return errors.New(i18n.T("cmd.php.analyse.no_analyser"))
|
||||
}
|
||||
|
||||
if stanJSON && stanSARIF {
|
||||
return errors.New(i18n.T("common.error.json_sarif_exclusive"))
|
||||
}
|
||||
|
||||
if !stanJSON && !stanSARIF {
|
||||
cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.ProgressSubject("run", "static analysis"))
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
opts := AnalyseOptions{
|
||||
Dir: cwd,
|
||||
Level: stanLevel,
|
||||
Memory: stanMemory,
|
||||
JSON: stanJSON,
|
||||
SARIF: stanSARIF,
|
||||
Output: os.Stdout,
|
||||
}
|
||||
|
||||
// Get any additional paths from args
|
||||
if len(args) > 0 {
|
||||
opts.Paths = args
|
||||
}
|
||||
|
||||
if err := Analyse(ctx, opts); err != nil {
|
||||
return cli.Err("%s: %w", i18n.T("cmd.php.error.analysis_issues"), err)
|
||||
}
|
||||
|
||||
if !stanJSON && !stanSARIF {
|
||||
cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("common.result.no_issues"))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
stanCmd.Flags().IntVar(&stanLevel, "level", 0, i18n.T("cmd.php.analyse.flag.level"))
|
||||
stanCmd.Flags().StringVar(&stanMemory, "memory", "", i18n.T("cmd.php.analyse.flag.memory"))
|
||||
stanCmd.Flags().BoolVar(&stanJSON, "json", false, i18n.T("common.flag.json"))
|
||||
stanCmd.Flags().BoolVar(&stanSARIF, "sarif", false, i18n.T("common.flag.sarif"))
|
||||
|
||||
parent.AddCommand(stanCmd)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// New QA Commands
|
||||
// =============================================================================
|
||||
|
||||
var (
|
||||
psalmLevel int
|
||||
psalmFix bool
|
||||
psalmBaseline bool
|
||||
psalmShowInfo bool
|
||||
psalmJSON bool
|
||||
psalmSARIF bool
|
||||
)
|
||||
|
||||
func addPHPPsalmCommand(parent *cli.Command) {
|
||||
psalmCmd := &cli.Command{
|
||||
Use: "psalm",
|
||||
Short: i18n.T("cmd.php.psalm.short"),
|
||||
Long: i18n.T("cmd.php.psalm.long"),
|
||||
RunE: func(cmd *cli.Command, args []string) error {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err)
|
||||
}
|
||||
|
||||
if !IsPHPProject(cwd) {
|
||||
return errors.New(i18n.T("cmd.php.error.not_php"))
|
||||
}
|
||||
|
||||
// Check if Psalm is available
|
||||
_, found := DetectPsalm(cwd)
|
||||
if !found {
|
||||
cli.Print("%s %s\n\n", errorStyle.Render(i18n.Label("error")), i18n.T("cmd.php.psalm.not_found"))
|
||||
cli.Print("%s %s\n", dimStyle.Render(i18n.Label("install")), i18n.T("cmd.php.psalm.install"))
|
||||
cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.setup")), i18n.T("cmd.php.psalm.setup"))
|
||||
return errors.New(i18n.T("cmd.php.error.psalm_not_installed"))
|
||||
}
|
||||
|
||||
if psalmJSON && psalmSARIF {
|
||||
return errors.New(i18n.T("common.error.json_sarif_exclusive"))
|
||||
}
|
||||
|
||||
if !psalmJSON && !psalmSARIF {
|
||||
var msg string
|
||||
if psalmFix {
|
||||
msg = i18n.T("cmd.php.psalm.analysing_fixing")
|
||||
} else {
|
||||
msg = i18n.T("cmd.php.psalm.analysing")
|
||||
}
|
||||
cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.psalm")), msg)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
opts := PsalmOptions{
|
||||
Dir: cwd,
|
||||
Level: psalmLevel,
|
||||
Fix: psalmFix,
|
||||
Baseline: psalmBaseline,
|
||||
ShowInfo: psalmShowInfo,
|
||||
JSON: psalmJSON,
|
||||
SARIF: psalmSARIF,
|
||||
Output: os.Stdout,
|
||||
}
|
||||
|
||||
if err := RunPsalm(ctx, opts); err != nil {
|
||||
return cli.Err("%s: %w", i18n.T("cmd.php.error.psalm_issues"), err)
|
||||
}
|
||||
|
||||
if !psalmJSON && !psalmSARIF {
|
||||
cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("common.result.no_issues"))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
psalmCmd.Flags().IntVar(&psalmLevel, "level", 0, i18n.T("cmd.php.psalm.flag.level"))
|
||||
psalmCmd.Flags().BoolVar(&psalmFix, "fix", false, i18n.T("common.flag.fix"))
|
||||
psalmCmd.Flags().BoolVar(&psalmBaseline, "baseline", false, i18n.T("cmd.php.psalm.flag.baseline"))
|
||||
psalmCmd.Flags().BoolVar(&psalmShowInfo, "show-info", false, i18n.T("cmd.php.psalm.flag.show_info"))
|
||||
psalmCmd.Flags().BoolVar(&psalmJSON, "json", false, i18n.T("common.flag.json"))
|
||||
psalmCmd.Flags().BoolVar(&psalmSARIF, "sarif", false, i18n.T("common.flag.sarif"))
|
||||
|
||||
parent.AddCommand(psalmCmd)
|
||||
}
|
||||
|
||||
var (
|
||||
auditJSONOutput bool
|
||||
auditFix bool
|
||||
)
|
||||
|
||||
func addPHPAuditCommand(parent *cli.Command) {
|
||||
auditCmd := &cli.Command{
|
||||
Use: "audit",
|
||||
Short: i18n.T("cmd.php.audit.short"),
|
||||
Long: i18n.T("cmd.php.audit.long"),
|
||||
RunE: func(cmd *cli.Command, args []string) error {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err)
|
||||
}
|
||||
|
||||
if !IsPHPProject(cwd) {
|
||||
return errors.New(i18n.T("cmd.php.error.not_php"))
|
||||
}
|
||||
|
||||
cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.audit")), i18n.T("cmd.php.audit.scanning"))
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
results, err := RunAudit(ctx, AuditOptions{
|
||||
Dir: cwd,
|
||||
JSON: auditJSONOutput,
|
||||
Fix: auditFix,
|
||||
Output: os.Stdout,
|
||||
})
|
||||
if err != nil {
|
||||
return cli.Err("%s: %w", i18n.T("cmd.php.error.audit_failed"), err)
|
||||
}
|
||||
|
||||
// Print results
|
||||
totalVulns := 0
|
||||
hasErrors := false
|
||||
|
||||
for _, result := range results {
|
||||
icon := successStyle.Render("✓")
|
||||
status := successStyle.Render(i18n.T("cmd.php.audit.secure"))
|
||||
|
||||
if result.Error != nil {
|
||||
icon = errorStyle.Render("✗")
|
||||
status = errorStyle.Render(i18n.T("cmd.php.audit.error"))
|
||||
hasErrors = true
|
||||
} else if result.Vulnerabilities > 0 {
|
||||
icon = errorStyle.Render("✗")
|
||||
status = errorStyle.Render(i18n.T("cmd.php.audit.vulnerabilities", map[string]interface{}{"Count": result.Vulnerabilities}))
|
||||
totalVulns += result.Vulnerabilities
|
||||
}
|
||||
|
||||
cli.Print(" %s %s %s\n", icon, dimStyle.Render(result.Tool+":"), status)
|
||||
|
||||
// Show advisories
|
||||
for _, adv := range result.Advisories {
|
||||
severity := adv.Severity
|
||||
if severity == "" {
|
||||
severity = "unknown"
|
||||
}
|
||||
sevStyle := getSeverityStyle(severity)
|
||||
cli.Print(" %s %s\n", sevStyle.Render("["+severity+"]"), adv.Package)
|
||||
if adv.Title != "" {
|
||||
cli.Print(" %s\n", dimStyle.Render(adv.Title))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cli.Blank()
|
||||
|
||||
if totalVulns > 0 {
|
||||
cli.Print("%s %s\n", errorStyle.Render(i18n.Label("warning")), i18n.T("cmd.php.audit.found_vulns", map[string]interface{}{"Count": totalVulns}))
|
||||
cli.Print("%s %s\n", dimStyle.Render(i18n.Label("fix")), i18n.T("common.hint.fix_deps"))
|
||||
return errors.New(i18n.T("cmd.php.error.vulns_found"))
|
||||
}
|
||||
|
||||
if hasErrors {
|
||||
return errors.New(i18n.T("cmd.php.audit.completed_errors"))
|
||||
}
|
||||
|
||||
cli.Print("%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("cmd.php.audit.all_secure"))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
auditCmd.Flags().BoolVar(&auditJSONOutput, "json", false, i18n.T("common.flag.json"))
|
||||
auditCmd.Flags().BoolVar(&auditFix, "fix", false, i18n.T("cmd.php.audit.flag.fix"))
|
||||
|
||||
parent.AddCommand(auditCmd)
|
||||
}
|
||||
|
||||
var (
|
||||
securitySeverity string
|
||||
securityJSONOutput bool
|
||||
securitySarif bool
|
||||
securityURL string
|
||||
)
|
||||
|
||||
func addPHPSecurityCommand(parent *cli.Command) {
|
||||
securityCmd := &cli.Command{
|
||||
Use: "security",
|
||||
Short: i18n.T("cmd.php.security.short"),
|
||||
Long: i18n.T("cmd.php.security.long"),
|
||||
RunE: func(cmd *cli.Command, args []string) error {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err)
|
||||
}
|
||||
|
||||
if !IsPHPProject(cwd) {
|
||||
return errors.New(i18n.T("cmd.php.error.not_php"))
|
||||
}
|
||||
|
||||
cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.security")), i18n.ProgressSubject("run", "security checks"))
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
result, err := RunSecurityChecks(ctx, SecurityOptions{
|
||||
Dir: cwd,
|
||||
Severity: securitySeverity,
|
||||
JSON: securityJSONOutput,
|
||||
SARIF: securitySarif,
|
||||
URL: securityURL,
|
||||
Output: os.Stdout,
|
||||
})
|
||||
if err != nil {
|
||||
return cli.Err("%s: %w", i18n.T("cmd.php.error.security_failed"), err)
|
||||
}
|
||||
|
||||
// Print results by category
|
||||
currentCategory := ""
|
||||
for _, check := range result.Checks {
|
||||
category := strings.Split(check.ID, "_")[0]
|
||||
if category != currentCategory {
|
||||
if currentCategory != "" {
|
||||
cli.Blank()
|
||||
}
|
||||
currentCategory = category
|
||||
cli.Print(" %s\n", dimStyle.Render(strings.ToUpper(category)+i18n.T("cmd.php.security.checks_suffix")))
|
||||
}
|
||||
|
||||
icon := successStyle.Render("✓")
|
||||
if !check.Passed {
|
||||
icon = getSeverityStyle(check.Severity).Render("✗")
|
||||
}
|
||||
|
||||
cli.Print(" %s %s\n", icon, check.Name)
|
||||
if !check.Passed && check.Message != "" {
|
||||
cli.Print(" %s\n", dimStyle.Render(check.Message))
|
||||
if check.Fix != "" {
|
||||
cli.Print(" %s %s\n", dimStyle.Render(i18n.Label("fix")), check.Fix)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cli.Blank()
|
||||
|
||||
// Print summary
|
||||
cli.Print("%s %s\n", dimStyle.Render(i18n.Label("summary")), i18n.T("cmd.php.security.summary"))
|
||||
cli.Print(" %s %d/%d\n", dimStyle.Render(i18n.T("cmd.php.security.passed")), result.Summary.Passed, result.Summary.Total)
|
||||
|
||||
if result.Summary.Critical > 0 {
|
||||
cli.Print(" %s %d\n", phpSecurityCriticalStyle.Render(i18n.T("cmd.php.security.critical")), result.Summary.Critical)
|
||||
}
|
||||
if result.Summary.High > 0 {
|
||||
cli.Print(" %s %d\n", phpSecurityHighStyle.Render(i18n.T("cmd.php.security.high")), result.Summary.High)
|
||||
}
|
||||
if result.Summary.Medium > 0 {
|
||||
cli.Print(" %s %d\n", phpSecurityMediumStyle.Render(i18n.T("cmd.php.security.medium")), result.Summary.Medium)
|
||||
}
|
||||
if result.Summary.Low > 0 {
|
||||
cli.Print(" %s %d\n", phpSecurityLowStyle.Render(i18n.T("cmd.php.security.low")), result.Summary.Low)
|
||||
}
|
||||
|
||||
if result.Summary.Critical > 0 || result.Summary.High > 0 {
|
||||
return errors.New(i18n.T("cmd.php.error.critical_high_issues"))
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
securityCmd.Flags().StringVar(&securitySeverity, "severity", "", i18n.T("cmd.php.security.flag.severity"))
|
||||
securityCmd.Flags().BoolVar(&securityJSONOutput, "json", false, i18n.T("common.flag.json"))
|
||||
securityCmd.Flags().BoolVar(&securitySarif, "sarif", false, i18n.T("cmd.php.security.flag.sarif"))
|
||||
securityCmd.Flags().StringVar(&securityURL, "url", "", i18n.T("cmd.php.security.flag.url"))
|
||||
|
||||
parent.AddCommand(securityCmd)
|
||||
}
|
||||
|
||||
var (
|
||||
qaQuick bool
|
||||
qaFull bool
|
||||
qaFix bool
|
||||
qaJSON bool
|
||||
)
|
||||
|
||||
func addPHPQACommand(parent *cli.Command) {
|
||||
qaCmd := &cli.Command{
|
||||
Use: "qa",
|
||||
Short: i18n.T("cmd.php.qa.short"),
|
||||
Long: i18n.T("cmd.php.qa.long"),
|
||||
RunE: func(cmd *cli.Command, args []string) error {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err)
|
||||
}
|
||||
|
||||
if !IsPHPProject(cwd) {
|
||||
return errors.New(i18n.T("cmd.php.error.not_php"))
|
||||
}
|
||||
|
||||
// Determine stages
|
||||
opts := QAOptions{
|
||||
Dir: cwd,
|
||||
Quick: qaQuick,
|
||||
Full: qaFull,
|
||||
Fix: qaFix,
|
||||
JSON: qaJSON,
|
||||
}
|
||||
stages := GetQAStages(opts)
|
||||
|
||||
// Print header
|
||||
if !qaJSON {
|
||||
cli.Print("%s %s\n\n", dimStyle.Render(i18n.Label("qa")), i18n.ProgressSubject("run", "QA pipeline"))
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Create QA runner using pkg/process
|
||||
runner, err := NewQARunner(cwd, qaFix)
|
||||
if err != nil {
|
||||
return cli.Err("%s: %w", i18n.T("i18n.fail.create", "QA runner"), err)
|
||||
}
|
||||
|
||||
// Run all checks with dependency ordering
|
||||
result, err := runner.Run(ctx, stages)
|
||||
if err != nil {
|
||||
return cli.Err("%s: %w", i18n.T("i18n.fail.run", "QA checks"), err)
|
||||
}
|
||||
|
||||
// Display results by stage (skip when JSON output is enabled)
|
||||
if !qaJSON {
|
||||
currentStage := ""
|
||||
for _, checkResult := range result.Results {
|
||||
// Determine stage for this check
|
||||
stage := getCheckStage(checkResult.Name, stages, cwd)
|
||||
if stage != currentStage {
|
||||
if currentStage != "" {
|
||||
cli.Blank()
|
||||
}
|
||||
currentStage = stage
|
||||
cli.Print("%s\n", phpQAStageStyle.Render("── "+strings.ToUpper(stage)+" ──"))
|
||||
}
|
||||
|
||||
icon := phpQAPassedStyle.Render("✓")
|
||||
status := phpQAPassedStyle.Render(i18n.T("i18n.done.pass"))
|
||||
if checkResult.Skipped {
|
||||
icon = dimStyle.Render("-")
|
||||
status = dimStyle.Render(i18n.T("i18n.done.skip"))
|
||||
} else if !checkResult.Passed {
|
||||
icon = phpQAFailedStyle.Render("✗")
|
||||
status = phpQAFailedStyle.Render(i18n.T("i18n.done.fail"))
|
||||
}
|
||||
|
||||
cli.Print(" %s %s %s %s\n", icon, checkResult.Name, status, dimStyle.Render(checkResult.Duration))
|
||||
}
|
||||
cli.Blank()
|
||||
|
||||
// Print summary
|
||||
if result.Passed {
|
||||
cli.Print("%s %s\n", phpQAPassedStyle.Render("QA PASSED:"), i18n.T("i18n.count.check", result.PassedCount)+" "+i18n.T("i18n.done.pass"))
|
||||
cli.Print("%s %s\n", dimStyle.Render(i18n.T("i18n.label.duration")), result.Duration)
|
||||
return nil
|
||||
}
|
||||
|
||||
cli.Print("%s %s\n\n", phpQAFailedStyle.Render("QA FAILED:"), i18n.T("i18n.count.check", result.PassedCount)+"/"+cli.Sprint(len(result.Results))+" "+i18n.T("i18n.done.pass"))
|
||||
|
||||
// Show what needs fixing
|
||||
cli.Print("%s\n", dimStyle.Render(i18n.T("i18n.label.fix")))
|
||||
for _, checkResult := range result.Results {
|
||||
if checkResult.Passed || checkResult.Skipped {
|
||||
continue
|
||||
}
|
||||
fixCmd := getQAFixCommand(checkResult.Name, qaFix)
|
||||
issue := checkResult.GetIssueMessage()
|
||||
if issue == "" {
|
||||
issue = "issues found"
|
||||
}
|
||||
cli.Print(" %s %s\n", phpQAFailedStyle.Render("*"), checkResult.Name+": "+issue)
|
||||
if fixCmd != "" {
|
||||
cli.Print(" %s %s\n", dimStyle.Render("->"), fixCmd)
|
||||
}
|
||||
}
|
||||
|
||||
return cli.Err("%s", i18n.T("i18n.fail.run", "QA pipeline"))
|
||||
}
|
||||
|
||||
// JSON mode: output results as JSON
|
||||
output, err := json.MarshalIndent(result, "", " ")
|
||||
if err != nil {
|
||||
return cli.Wrap(err, "marshal JSON output")
|
||||
}
|
||||
cli.Text(string(output))
|
||||
|
||||
if !result.Passed {
|
||||
return cli.Err("%s", i18n.T("i18n.fail.run", "QA pipeline"))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
qaCmd.Flags().BoolVar(&qaQuick, "quick", false, i18n.T("cmd.php.qa.flag.quick"))
|
||||
qaCmd.Flags().BoolVar(&qaFull, "full", false, i18n.T("cmd.php.qa.flag.full"))
|
||||
qaCmd.Flags().BoolVar(&qaFix, "fix", false, i18n.T("common.flag.fix"))
|
||||
qaCmd.Flags().BoolVar(&qaJSON, "json", false, i18n.T("common.flag.json"))
|
||||
|
||||
parent.AddCommand(qaCmd)
|
||||
}
|
||||
|
||||
// getCheckStage determines which stage a check belongs to.
|
||||
func getCheckStage(checkName string, stages []QAStage, dir string) string {
|
||||
for _, stage := range stages {
|
||||
checks := GetQAChecks(dir, stage)
|
||||
for _, c := range checks {
|
||||
if c == checkName {
|
||||
return string(stage)
|
||||
}
|
||||
}
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
func getQAFixCommand(checkName string, fixEnabled bool) string {
|
||||
switch checkName {
|
||||
case "audit":
|
||||
return i18n.T("i18n.progress.update", "dependencies")
|
||||
case "fmt":
|
||||
if fixEnabled {
|
||||
return ""
|
||||
}
|
||||
return "core php fmt --fix"
|
||||
case "stan":
|
||||
return i18n.T("i18n.progress.fix", "PHPStan errors")
|
||||
case "psalm":
|
||||
return i18n.T("i18n.progress.fix", "Psalm errors")
|
||||
case "test":
|
||||
return i18n.T("i18n.progress.fix", i18n.T("i18n.done.fail")+" tests")
|
||||
case "rector":
|
||||
if fixEnabled {
|
||||
return ""
|
||||
}
|
||||
return "core php rector --fix"
|
||||
case "infection":
|
||||
return i18n.T("i18n.progress.improve", "test coverage")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var (
|
||||
rectorFix bool
|
||||
rectorDiff bool
|
||||
rectorClearCache bool
|
||||
)
|
||||
|
||||
func addPHPRectorCommand(parent *cli.Command) {
|
||||
rectorCmd := &cli.Command{
|
||||
Use: "rector",
|
||||
Short: i18n.T("cmd.php.rector.short"),
|
||||
Long: i18n.T("cmd.php.rector.long"),
|
||||
RunE: func(cmd *cli.Command, args []string) error {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err)
|
||||
}
|
||||
|
||||
if !IsPHPProject(cwd) {
|
||||
return errors.New(i18n.T("cmd.php.error.not_php"))
|
||||
}
|
||||
|
||||
// Check if Rector is available
|
||||
if !DetectRector(cwd) {
|
||||
cli.Print("%s %s\n\n", errorStyle.Render(i18n.Label("error")), i18n.T("cmd.php.rector.not_found"))
|
||||
cli.Print("%s %s\n", dimStyle.Render(i18n.Label("install")), i18n.T("cmd.php.rector.install"))
|
||||
cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.setup")), i18n.T("cmd.php.rector.setup"))
|
||||
return errors.New(i18n.T("cmd.php.error.rector_not_installed"))
|
||||
}
|
||||
|
||||
var msg string
|
||||
if rectorFix {
|
||||
msg = i18n.T("cmd.php.rector.refactoring")
|
||||
} else {
|
||||
msg = i18n.T("cmd.php.rector.analysing")
|
||||
}
|
||||
cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.rector")), msg)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
opts := RectorOptions{
|
||||
Dir: cwd,
|
||||
Fix: rectorFix,
|
||||
Diff: rectorDiff,
|
||||
ClearCache: rectorClearCache,
|
||||
Output: os.Stdout,
|
||||
}
|
||||
|
||||
if err := RunRector(ctx, opts); err != nil {
|
||||
if rectorFix {
|
||||
return cli.Err("%s: %w", i18n.T("cmd.php.error.rector_failed"), err)
|
||||
}
|
||||
// Dry-run returns non-zero if changes would be made
|
||||
cli.Print("\n%s %s\n", phpQAWarningStyle.Render(i18n.T("cmd.php.label.info")), i18n.T("cmd.php.rector.changes_suggested"))
|
||||
return nil
|
||||
}
|
||||
|
||||
if rectorFix {
|
||||
cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("common.success.completed", map[string]any{"Action": "Code refactored"}))
|
||||
} else {
|
||||
cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("cmd.php.rector.no_changes"))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
rectorCmd.Flags().BoolVar(&rectorFix, "fix", false, i18n.T("cmd.php.rector.flag.fix"))
|
||||
rectorCmd.Flags().BoolVar(&rectorDiff, "diff", false, i18n.T("cmd.php.rector.flag.diff"))
|
||||
rectorCmd.Flags().BoolVar(&rectorClearCache, "clear-cache", false, i18n.T("cmd.php.rector.flag.clear_cache"))
|
||||
|
||||
parent.AddCommand(rectorCmd)
|
||||
}
|
||||
|
||||
var (
|
||||
infectionMinMSI int
|
||||
infectionMinCoveredMSI int
|
||||
infectionThreads int
|
||||
infectionFilter string
|
||||
infectionOnlyCovered bool
|
||||
)
|
||||
|
||||
func addPHPInfectionCommand(parent *cli.Command) {
|
||||
infectionCmd := &cli.Command{
|
||||
Use: "infection",
|
||||
Short: i18n.T("cmd.php.infection.short"),
|
||||
Long: i18n.T("cmd.php.infection.long"),
|
||||
RunE: func(cmd *cli.Command, args []string) error {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err)
|
||||
}
|
||||
|
||||
if !IsPHPProject(cwd) {
|
||||
return errors.New(i18n.T("cmd.php.error.not_php"))
|
||||
}
|
||||
|
||||
// Check if Infection is available
|
||||
if !DetectInfection(cwd) {
|
||||
cli.Print("%s %s\n\n", errorStyle.Render(i18n.Label("error")), i18n.T("cmd.php.infection.not_found"))
|
||||
cli.Print("%s %s\n", dimStyle.Render(i18n.Label("install")), i18n.T("cmd.php.infection.install"))
|
||||
return errors.New(i18n.T("cmd.php.error.infection_not_installed"))
|
||||
}
|
||||
|
||||
cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.infection")), i18n.ProgressSubject("run", "mutation testing"))
|
||||
cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.info")), i18n.T("cmd.php.infection.note"))
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
opts := InfectionOptions{
|
||||
Dir: cwd,
|
||||
MinMSI: infectionMinMSI,
|
||||
MinCoveredMSI: infectionMinCoveredMSI,
|
||||
Threads: infectionThreads,
|
||||
Filter: infectionFilter,
|
||||
OnlyCovered: infectionOnlyCovered,
|
||||
Output: os.Stdout,
|
||||
}
|
||||
|
||||
if err := RunInfection(ctx, opts); err != nil {
|
||||
return cli.Err("%s: %w", i18n.T("cmd.php.error.infection_failed"), err)
|
||||
}
|
||||
|
||||
cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("cmd.php.infection.complete"))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
infectionCmd.Flags().IntVar(&infectionMinMSI, "min-msi", 0, i18n.T("cmd.php.infection.flag.min_msi"))
|
||||
infectionCmd.Flags().IntVar(&infectionMinCoveredMSI, "min-covered-msi", 0, i18n.T("cmd.php.infection.flag.min_covered_msi"))
|
||||
infectionCmd.Flags().IntVar(&infectionThreads, "threads", 0, i18n.T("cmd.php.infection.flag.threads"))
|
||||
infectionCmd.Flags().StringVar(&infectionFilter, "filter", "", i18n.T("cmd.php.infection.flag.filter"))
|
||||
infectionCmd.Flags().BoolVar(&infectionOnlyCovered, "only-covered", false, i18n.T("cmd.php.infection.flag.only_covered"))
|
||||
|
||||
parent.AddCommand(infectionCmd)
|
||||
}
|
||||
|
||||
func getSeverityStyle(severity string) *cli.AnsiStyle {
|
||||
switch strings.ToLower(severity) {
|
||||
case "critical":
|
||||
return phpSecurityCriticalStyle
|
||||
case "high":
|
||||
return phpSecurityHighStyle
|
||||
case "medium":
|
||||
return phpSecurityMediumStyle
|
||||
case "low":
|
||||
return phpSecurityLowStyle
|
||||
default:
|
||||
return dimStyle
|
||||
}
|
||||
}
|
||||
|
|
@ -1,304 +0,0 @@
|
|||
package php
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFormatOptions_Struct(t *testing.T) {
|
||||
t.Run("all fields accessible", func(t *testing.T) {
|
||||
opts := FormatOptions{
|
||||
Dir: "/project",
|
||||
Fix: true,
|
||||
Diff: true,
|
||||
Paths: []string{"app", "tests"},
|
||||
Output: os.Stdout,
|
||||
}
|
||||
|
||||
assert.Equal(t, "/project", opts.Dir)
|
||||
assert.True(t, opts.Fix)
|
||||
assert.True(t, opts.Diff)
|
||||
assert.Equal(t, []string{"app", "tests"}, opts.Paths)
|
||||
assert.NotNil(t, opts.Output)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAnalyseOptions_Struct(t *testing.T) {
|
||||
t.Run("all fields accessible", func(t *testing.T) {
|
||||
opts := AnalyseOptions{
|
||||
Dir: "/project",
|
||||
Level: 5,
|
||||
Paths: []string{"src"},
|
||||
Memory: "2G",
|
||||
Output: os.Stdout,
|
||||
}
|
||||
|
||||
assert.Equal(t, "/project", opts.Dir)
|
||||
assert.Equal(t, 5, opts.Level)
|
||||
assert.Equal(t, []string{"src"}, opts.Paths)
|
||||
assert.Equal(t, "2G", opts.Memory)
|
||||
assert.NotNil(t, opts.Output)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFormatterType_Constants(t *testing.T) {
|
||||
t.Run("constants are defined", func(t *testing.T) {
|
||||
assert.Equal(t, FormatterType("pint"), FormatterPint)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAnalyserType_Constants(t *testing.T) {
|
||||
t.Run("constants are defined", func(t *testing.T) {
|
||||
assert.Equal(t, AnalyserType("phpstan"), AnalyserPHPStan)
|
||||
assert.Equal(t, AnalyserType("larastan"), AnalyserLarastan)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDetectFormatter_Extended(t *testing.T) {
|
||||
t.Run("returns not found for empty directory", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
_, found := DetectFormatter(dir)
|
||||
assert.False(t, found)
|
||||
})
|
||||
|
||||
t.Run("prefers pint.json over vendor binary", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
// Create pint.json
|
||||
err := os.WriteFile(filepath.Join(dir, "pint.json"), []byte("{}"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
formatter, found := DetectFormatter(dir)
|
||||
assert.True(t, found)
|
||||
assert.Equal(t, FormatterPint, formatter)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDetectAnalyser_Extended(t *testing.T) {
|
||||
t.Run("returns not found for empty directory", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
_, found := DetectAnalyser(dir)
|
||||
assert.False(t, found)
|
||||
})
|
||||
|
||||
t.Run("detects phpstan from vendor binary alone", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
// Create vendor binary
|
||||
binDir := filepath.Join(dir, "vendor", "bin")
|
||||
err := os.MkdirAll(binDir, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = os.WriteFile(filepath.Join(binDir, "phpstan"), []byte(""), 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
analyser, found := DetectAnalyser(dir)
|
||||
assert.True(t, found)
|
||||
assert.Equal(t, AnalyserPHPStan, analyser)
|
||||
})
|
||||
|
||||
t.Run("detects larastan from larastan/larastan vendor path", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
// Create phpstan.neon
|
||||
err := os.WriteFile(filepath.Join(dir, "phpstan.neon"), []byte(""), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create larastan/larastan path
|
||||
larastanPath := filepath.Join(dir, "vendor", "larastan", "larastan")
|
||||
err = os.MkdirAll(larastanPath, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
analyser, found := DetectAnalyser(dir)
|
||||
assert.True(t, found)
|
||||
assert.Equal(t, AnalyserLarastan, analyser)
|
||||
})
|
||||
|
||||
t.Run("detects larastan from nunomaduro/larastan vendor path", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
// Create phpstan.neon
|
||||
err := os.WriteFile(filepath.Join(dir, "phpstan.neon"), []byte(""), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create nunomaduro/larastan path
|
||||
larastanPath := filepath.Join(dir, "vendor", "nunomaduro", "larastan")
|
||||
err = os.MkdirAll(larastanPath, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
analyser, found := DetectAnalyser(dir)
|
||||
assert.True(t, found)
|
||||
assert.Equal(t, AnalyserLarastan, analyser)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuildPintCommand_Extended(t *testing.T) {
|
||||
t.Run("uses global pint when no vendor binary", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
opts := FormatOptions{Dir: dir}
|
||||
|
||||
cmd, _ := buildPintCommand(opts)
|
||||
assert.Equal(t, "pint", cmd)
|
||||
})
|
||||
|
||||
t.Run("adds test flag when Fix is false", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
opts := FormatOptions{Dir: dir, Fix: false}
|
||||
|
||||
_, args := buildPintCommand(opts)
|
||||
assert.Contains(t, args, "--test")
|
||||
})
|
||||
|
||||
t.Run("does not add test flag when Fix is true", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
opts := FormatOptions{Dir: dir, Fix: true}
|
||||
|
||||
_, args := buildPintCommand(opts)
|
||||
assert.NotContains(t, args, "--test")
|
||||
})
|
||||
|
||||
t.Run("adds diff flag", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
opts := FormatOptions{Dir: dir, Diff: true}
|
||||
|
||||
_, args := buildPintCommand(opts)
|
||||
assert.Contains(t, args, "--diff")
|
||||
})
|
||||
|
||||
t.Run("adds paths", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
opts := FormatOptions{Dir: dir, Paths: []string{"app", "tests"}}
|
||||
|
||||
_, args := buildPintCommand(opts)
|
||||
assert.Contains(t, args, "app")
|
||||
assert.Contains(t, args, "tests")
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuildPHPStanCommand_Extended(t *testing.T) {
|
||||
t.Run("uses global phpstan when no vendor binary", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
opts := AnalyseOptions{Dir: dir}
|
||||
|
||||
cmd, _ := buildPHPStanCommand(opts)
|
||||
assert.Equal(t, "phpstan", cmd)
|
||||
})
|
||||
|
||||
t.Run("adds level flag", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
opts := AnalyseOptions{Dir: dir, Level: 8}
|
||||
|
||||
_, args := buildPHPStanCommand(opts)
|
||||
assert.Contains(t, args, "--level")
|
||||
assert.Contains(t, args, "8")
|
||||
})
|
||||
|
||||
t.Run("does not add level flag when zero", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
opts := AnalyseOptions{Dir: dir, Level: 0}
|
||||
|
||||
_, args := buildPHPStanCommand(opts)
|
||||
assert.NotContains(t, args, "--level")
|
||||
})
|
||||
|
||||
t.Run("adds memory limit", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
opts := AnalyseOptions{Dir: dir, Memory: "4G"}
|
||||
|
||||
_, args := buildPHPStanCommand(opts)
|
||||
assert.Contains(t, args, "--memory-limit")
|
||||
assert.Contains(t, args, "4G")
|
||||
})
|
||||
|
||||
t.Run("does not add memory flag when empty", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
opts := AnalyseOptions{Dir: dir, Memory: ""}
|
||||
|
||||
_, args := buildPHPStanCommand(opts)
|
||||
assert.NotContains(t, args, "--memory-limit")
|
||||
})
|
||||
|
||||
t.Run("adds paths", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
opts := AnalyseOptions{Dir: dir, Paths: []string{"src", "app"}}
|
||||
|
||||
_, args := buildPHPStanCommand(opts)
|
||||
assert.Contains(t, args, "src")
|
||||
assert.Contains(t, args, "app")
|
||||
})
|
||||
}
|
||||
|
||||
func TestFormat_Bad(t *testing.T) {
|
||||
t.Run("fails when no formatter found", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
opts := FormatOptions{Dir: dir}
|
||||
|
||||
err := Format(context.TODO(), opts)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "no formatter found")
|
||||
})
|
||||
|
||||
t.Run("uses cwd when dir not specified", func(t *testing.T) {
|
||||
// When no formatter found in cwd, should still fail with "no formatter found"
|
||||
opts := FormatOptions{Dir: ""}
|
||||
|
||||
err := Format(context.TODO(), opts)
|
||||
// May or may not find a formatter depending on cwd, but function should not panic
|
||||
if err != nil {
|
||||
// Expected - no formatter in cwd
|
||||
assert.Contains(t, err.Error(), "no formatter")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("uses stdout when output not specified", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
// Create pint.json to enable formatter detection
|
||||
err := os.WriteFile(filepath.Join(dir, "pint.json"), []byte("{}"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
opts := FormatOptions{Dir: dir, Output: nil}
|
||||
|
||||
// Will fail because pint isn't actually installed, but tests the code path
|
||||
err = Format(context.Background(), opts)
|
||||
assert.Error(t, err) // Pint not installed
|
||||
})
|
||||
}
|
||||
|
||||
func TestAnalyse_Bad(t *testing.T) {
|
||||
t.Run("fails when no analyser found", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
opts := AnalyseOptions{Dir: dir}
|
||||
|
||||
err := Analyse(context.TODO(), opts)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "no static analyser found")
|
||||
})
|
||||
|
||||
t.Run("uses cwd when dir not specified", func(t *testing.T) {
|
||||
opts := AnalyseOptions{Dir: ""}
|
||||
|
||||
err := Analyse(context.TODO(), opts)
|
||||
// May or may not find an analyser depending on cwd
|
||||
if err != nil {
|
||||
assert.Contains(t, err.Error(), "no static analyser")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("uses stdout when output not specified", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
// Create phpstan.neon to enable analyser detection
|
||||
err := os.WriteFile(filepath.Join(dir, "phpstan.neon"), []byte(""), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
opts := AnalyseOptions{Dir: dir, Output: nil}
|
||||
|
||||
// Will fail because phpstan isn't actually installed, but tests the code path
|
||||
err = Analyse(context.Background(), opts)
|
||||
assert.Error(t, err) // PHPStan not installed
|
||||
})
|
||||
}
|
||||
517
quality_test.go
517
quality_test.go
|
|
@ -1,517 +0,0 @@
|
|||
package php
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDetectFormatter_Good(t *testing.T) {
|
||||
t.Run("detects pint.json", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
err := os.WriteFile(filepath.Join(dir, "pint.json"), []byte("{}"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
formatter, found := DetectFormatter(dir)
|
||||
assert.True(t, found)
|
||||
assert.Equal(t, FormatterPint, formatter)
|
||||
})
|
||||
|
||||
t.Run("detects vendor binary", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
binDir := filepath.Join(dir, "vendor", "bin")
|
||||
err := os.MkdirAll(binDir, 0755)
|
||||
require.NoError(t, err)
|
||||
err = os.WriteFile(filepath.Join(binDir, "pint"), []byte(""), 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
formatter, found := DetectFormatter(dir)
|
||||
assert.True(t, found)
|
||||
assert.Equal(t, FormatterPint, formatter)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDetectFormatter_Bad(t *testing.T) {
|
||||
t.Run("no formatter", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
_, found := DetectFormatter(dir)
|
||||
assert.False(t, found)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDetectAnalyser_Good(t *testing.T) {
|
||||
t.Run("detects phpstan.neon", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
err := os.WriteFile(filepath.Join(dir, "phpstan.neon"), []byte(""), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
analyser, found := DetectAnalyser(dir)
|
||||
assert.True(t, found)
|
||||
assert.Equal(t, AnalyserPHPStan, analyser)
|
||||
})
|
||||
|
||||
t.Run("detects phpstan.neon.dist", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
err := os.WriteFile(filepath.Join(dir, "phpstan.neon.dist"), []byte(""), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
analyser, found := DetectAnalyser(dir)
|
||||
assert.True(t, found)
|
||||
assert.Equal(t, AnalyserPHPStan, analyser)
|
||||
})
|
||||
|
||||
t.Run("detects larastan", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
err := os.WriteFile(filepath.Join(dir, "phpstan.neon"), []byte(""), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
larastanDir := filepath.Join(dir, "vendor", "larastan", "larastan")
|
||||
err = os.MkdirAll(larastanDir, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
analyser, found := DetectAnalyser(dir)
|
||||
assert.True(t, found)
|
||||
assert.Equal(t, AnalyserLarastan, analyser)
|
||||
})
|
||||
|
||||
t.Run("detects nunomaduro/larastan", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
err := os.WriteFile(filepath.Join(dir, "phpstan.neon"), []byte(""), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
larastanDir := filepath.Join(dir, "vendor", "nunomaduro", "larastan")
|
||||
err = os.MkdirAll(larastanDir, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
analyser, found := DetectAnalyser(dir)
|
||||
assert.True(t, found)
|
||||
assert.Equal(t, AnalyserLarastan, analyser)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuildPintCommand_Good(t *testing.T) {
|
||||
t.Run("basic command", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
opts := FormatOptions{Dir: dir}
|
||||
cmd, args := buildPintCommand(opts)
|
||||
assert.Equal(t, "pint", cmd)
|
||||
assert.Contains(t, args, "--test")
|
||||
})
|
||||
|
||||
t.Run("fix enabled", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
opts := FormatOptions{Dir: dir, Fix: true}
|
||||
_, args := buildPintCommand(opts)
|
||||
assert.NotContains(t, args, "--test")
|
||||
})
|
||||
|
||||
t.Run("diff enabled", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
opts := FormatOptions{Dir: dir, Diff: true}
|
||||
_, args := buildPintCommand(opts)
|
||||
assert.Contains(t, args, "--diff")
|
||||
})
|
||||
|
||||
t.Run("with specific paths", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
paths := []string{"app", "tests"}
|
||||
opts := FormatOptions{Dir: dir, Paths: paths}
|
||||
_, args := buildPintCommand(opts)
|
||||
assert.Equal(t, paths, args[len(args)-2:])
|
||||
})
|
||||
|
||||
t.Run("uses vendor binary if exists", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
binDir := filepath.Join(dir, "vendor", "bin")
|
||||
err := os.MkdirAll(binDir, 0755)
|
||||
require.NoError(t, err)
|
||||
pintPath := filepath.Join(binDir, "pint")
|
||||
err = os.WriteFile(pintPath, []byte(""), 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
opts := FormatOptions{Dir: dir}
|
||||
cmd, _ := buildPintCommand(opts)
|
||||
assert.Equal(t, pintPath, cmd)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuildPHPStanCommand_Good(t *testing.T) {
|
||||
t.Run("basic command", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
opts := AnalyseOptions{Dir: dir}
|
||||
cmd, args := buildPHPStanCommand(opts)
|
||||
assert.Equal(t, "phpstan", cmd)
|
||||
assert.Equal(t, []string{"analyse"}, args)
|
||||
})
|
||||
|
||||
t.Run("with level", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
opts := AnalyseOptions{Dir: dir, Level: 5}
|
||||
_, args := buildPHPStanCommand(opts)
|
||||
assert.Contains(t, args, "--level")
|
||||
assert.Contains(t, args, "5")
|
||||
})
|
||||
|
||||
t.Run("with memory limit", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
opts := AnalyseOptions{Dir: dir, Memory: "2G"}
|
||||
_, args := buildPHPStanCommand(opts)
|
||||
assert.Contains(t, args, "--memory-limit")
|
||||
assert.Contains(t, args, "2G")
|
||||
})
|
||||
|
||||
t.Run("uses vendor binary if exists", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
binDir := filepath.Join(dir, "vendor", "bin")
|
||||
err := os.MkdirAll(binDir, 0755)
|
||||
require.NoError(t, err)
|
||||
phpstanPath := filepath.Join(binDir, "phpstan")
|
||||
err = os.WriteFile(phpstanPath, []byte(""), 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
opts := AnalyseOptions{Dir: dir}
|
||||
cmd, _ := buildPHPStanCommand(opts)
|
||||
assert.Equal(t, phpstanPath, cmd)
|
||||
})
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Psalm Detection Tests
|
||||
// =============================================================================
|
||||
|
||||
func TestDetectPsalm_Good(t *testing.T) {
|
||||
t.Run("detects psalm.xml", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
err := os.WriteFile(filepath.Join(dir, "psalm.xml"), []byte(""), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Also need vendor binary for it to return true
|
||||
binDir := filepath.Join(dir, "vendor", "bin")
|
||||
err = os.MkdirAll(binDir, 0755)
|
||||
require.NoError(t, err)
|
||||
err = os.WriteFile(filepath.Join(binDir, "psalm"), []byte(""), 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
psalmType, found := DetectPsalm(dir)
|
||||
assert.True(t, found)
|
||||
assert.Equal(t, PsalmStandard, psalmType)
|
||||
})
|
||||
|
||||
t.Run("detects psalm.xml.dist", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
err := os.WriteFile(filepath.Join(dir, "psalm.xml.dist"), []byte(""), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
binDir := filepath.Join(dir, "vendor", "bin")
|
||||
err = os.MkdirAll(binDir, 0755)
|
||||
require.NoError(t, err)
|
||||
err = os.WriteFile(filepath.Join(binDir, "psalm"), []byte(""), 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, found := DetectPsalm(dir)
|
||||
assert.True(t, found)
|
||||
})
|
||||
|
||||
t.Run("detects vendor binary only", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
binDir := filepath.Join(dir, "vendor", "bin")
|
||||
err := os.MkdirAll(binDir, 0755)
|
||||
require.NoError(t, err)
|
||||
err = os.WriteFile(filepath.Join(binDir, "psalm"), []byte(""), 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, found := DetectPsalm(dir)
|
||||
assert.True(t, found)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDetectPsalm_Bad(t *testing.T) {
|
||||
t.Run("no psalm", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
_, found := DetectPsalm(dir)
|
||||
assert.False(t, found)
|
||||
})
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Rector Detection Tests
|
||||
// =============================================================================
|
||||
|
||||
func TestDetectRector_Good(t *testing.T) {
|
||||
t.Run("detects rector.php", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
err := os.WriteFile(filepath.Join(dir, "rector.php"), []byte("<?php"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
found := DetectRector(dir)
|
||||
assert.True(t, found)
|
||||
})
|
||||
|
||||
t.Run("detects vendor binary", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
binDir := filepath.Join(dir, "vendor", "bin")
|
||||
err := os.MkdirAll(binDir, 0755)
|
||||
require.NoError(t, err)
|
||||
err = os.WriteFile(filepath.Join(binDir, "rector"), []byte(""), 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
found := DetectRector(dir)
|
||||
assert.True(t, found)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDetectRector_Bad(t *testing.T) {
|
||||
t.Run("no rector", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
found := DetectRector(dir)
|
||||
assert.False(t, found)
|
||||
})
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Infection Detection Tests
|
||||
// =============================================================================
|
||||
|
||||
func TestDetectInfection_Good(t *testing.T) {
|
||||
t.Run("detects infection.json", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
err := os.WriteFile(filepath.Join(dir, "infection.json"), []byte("{}"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
found := DetectInfection(dir)
|
||||
assert.True(t, found)
|
||||
})
|
||||
|
||||
t.Run("detects infection.json5", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
err := os.WriteFile(filepath.Join(dir, "infection.json5"), []byte("{}"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
found := DetectInfection(dir)
|
||||
assert.True(t, found)
|
||||
})
|
||||
|
||||
t.Run("detects vendor binary", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
binDir := filepath.Join(dir, "vendor", "bin")
|
||||
err := os.MkdirAll(binDir, 0755)
|
||||
require.NoError(t, err)
|
||||
err = os.WriteFile(filepath.Join(binDir, "infection"), []byte(""), 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
found := DetectInfection(dir)
|
||||
assert.True(t, found)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDetectInfection_Bad(t *testing.T) {
|
||||
t.Run("no infection", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
found := DetectInfection(dir)
|
||||
assert.False(t, found)
|
||||
})
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// QA Pipeline Tests
|
||||
// =============================================================================
|
||||
|
||||
func TestGetQAStages_Good(t *testing.T) {
|
||||
t.Run("default stages", func(t *testing.T) {
|
||||
opts := QAOptions{}
|
||||
stages := GetQAStages(opts)
|
||||
assert.Equal(t, []QAStage{QAStageQuick, QAStageStandard}, stages)
|
||||
})
|
||||
|
||||
t.Run("quick only", func(t *testing.T) {
|
||||
opts := QAOptions{Quick: true}
|
||||
stages := GetQAStages(opts)
|
||||
assert.Equal(t, []QAStage{QAStageQuick}, stages)
|
||||
})
|
||||
|
||||
t.Run("full stages", func(t *testing.T) {
|
||||
opts := QAOptions{Full: true}
|
||||
stages := GetQAStages(opts)
|
||||
assert.Equal(t, []QAStage{QAStageQuick, QAStageStandard, QAStageFull}, stages)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetQAChecks_Good(t *testing.T) {
|
||||
t.Run("quick stage checks", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
checks := GetQAChecks(dir, QAStageQuick)
|
||||
assert.Contains(t, checks, "audit")
|
||||
assert.Contains(t, checks, "fmt")
|
||||
assert.Contains(t, checks, "stan")
|
||||
})
|
||||
|
||||
t.Run("standard stage includes test", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
checks := GetQAChecks(dir, QAStageStandard)
|
||||
assert.Contains(t, checks, "test")
|
||||
})
|
||||
|
||||
t.Run("standard stage includes psalm if available", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
binDir := filepath.Join(dir, "vendor", "bin")
|
||||
err := os.MkdirAll(binDir, 0755)
|
||||
require.NoError(t, err)
|
||||
err = os.WriteFile(filepath.Join(binDir, "psalm"), []byte(""), 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
checks := GetQAChecks(dir, QAStageStandard)
|
||||
assert.Contains(t, checks, "psalm")
|
||||
})
|
||||
|
||||
t.Run("full stage includes rector if available", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
err := os.WriteFile(filepath.Join(dir, "rector.php"), []byte("<?php"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
checks := GetQAChecks(dir, QAStageFull)
|
||||
assert.Contains(t, checks, "rector")
|
||||
})
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Security Checks Tests
|
||||
// =============================================================================
|
||||
|
||||
func TestRunEnvSecurityChecks_Good(t *testing.T) {
|
||||
t.Run("detects debug mode enabled", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
envContent := "APP_DEBUG=true\nAPP_KEY=base64:abcdefghijklmnopqrstuvwxyz123456\n"
|
||||
err := os.WriteFile(filepath.Join(dir, ".env"), []byte(envContent), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
checks := runEnvSecurityChecks(dir)
|
||||
|
||||
var debugCheck *SecurityCheck
|
||||
for i := range checks {
|
||||
if checks[i].ID == "debug_mode" {
|
||||
debugCheck = &checks[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
require.NotNil(t, debugCheck)
|
||||
assert.False(t, debugCheck.Passed)
|
||||
assert.Equal(t, "critical", debugCheck.Severity)
|
||||
})
|
||||
|
||||
t.Run("passes with debug disabled", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
envContent := "APP_DEBUG=false\nAPP_KEY=base64:abcdefghijklmnopqrstuvwxyz123456\n"
|
||||
err := os.WriteFile(filepath.Join(dir, ".env"), []byte(envContent), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
checks := runEnvSecurityChecks(dir)
|
||||
|
||||
var debugCheck *SecurityCheck
|
||||
for i := range checks {
|
||||
if checks[i].ID == "debug_mode" {
|
||||
debugCheck = &checks[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
require.NotNil(t, debugCheck)
|
||||
assert.True(t, debugCheck.Passed)
|
||||
})
|
||||
|
||||
t.Run("detects weak app key", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
envContent := "APP_KEY=short\n"
|
||||
err := os.WriteFile(filepath.Join(dir, ".env"), []byte(envContent), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
checks := runEnvSecurityChecks(dir)
|
||||
|
||||
var keyCheck *SecurityCheck
|
||||
for i := range checks {
|
||||
if checks[i].ID == "app_key_set" {
|
||||
keyCheck = &checks[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
require.NotNil(t, keyCheck)
|
||||
assert.False(t, keyCheck.Passed)
|
||||
})
|
||||
|
||||
t.Run("detects non-https app url", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
envContent := "APP_URL=http://example.com\n"
|
||||
err := os.WriteFile(filepath.Join(dir, ".env"), []byte(envContent), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
checks := runEnvSecurityChecks(dir)
|
||||
|
||||
var urlCheck *SecurityCheck
|
||||
for i := range checks {
|
||||
if checks[i].ID == "https_enforced" {
|
||||
urlCheck = &checks[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
require.NotNil(t, urlCheck)
|
||||
assert.False(t, urlCheck.Passed)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRunFilesystemSecurityChecks_Good(t *testing.T) {
|
||||
t.Run("detects .env in public", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
publicDir := filepath.Join(dir, "public")
|
||||
err := os.MkdirAll(publicDir, 0755)
|
||||
require.NoError(t, err)
|
||||
err = os.WriteFile(filepath.Join(publicDir, ".env"), []byte(""), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
checks := runFilesystemSecurityChecks(dir)
|
||||
|
||||
found := false
|
||||
for _, check := range checks {
|
||||
if check.ID == "env_not_public" && !check.Passed {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, found, "should detect .env in public directory")
|
||||
})
|
||||
|
||||
t.Run("detects .git in public", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
gitDir := filepath.Join(dir, "public", ".git")
|
||||
err := os.MkdirAll(gitDir, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
checks := runFilesystemSecurityChecks(dir)
|
||||
|
||||
found := false
|
||||
for _, check := range checks {
|
||||
if check.ID == "git_not_public" && !check.Passed {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, found, "should detect .git in public directory")
|
||||
})
|
||||
|
||||
t.Run("passes with clean public directory", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
publicDir := filepath.Join(dir, "public")
|
||||
err := os.MkdirAll(publicDir, 0755)
|
||||
require.NoError(t, err)
|
||||
// Add only safe files
|
||||
err = os.WriteFile(filepath.Join(publicDir, "index.php"), []byte("<?php"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
checks := runFilesystemSecurityChecks(dir)
|
||||
assert.Empty(t, checks, "should not report issues for clean public directory")
|
||||
})
|
||||
}
|
||||
380
testing_test.go
380
testing_test.go
|
|
@ -1,380 +0,0 @@
|
|||
package php
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDetectTestRunner_Good(t *testing.T) {
|
||||
t.Run("detects Pest when tests/Pest.php exists", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
testsDir := filepath.Join(dir, "tests")
|
||||
err := os.MkdirAll(testsDir, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = os.WriteFile(filepath.Join(testsDir, "Pest.php"), []byte("<?php\n"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
runner := DetectTestRunner(dir)
|
||||
assert.Equal(t, TestRunnerPest, runner)
|
||||
})
|
||||
|
||||
t.Run("returns PHPUnit when no Pest.php", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
runner := DetectTestRunner(dir)
|
||||
assert.Equal(t, TestRunnerPHPUnit, runner)
|
||||
})
|
||||
|
||||
t.Run("returns PHPUnit when tests directory exists but no Pest.php", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
testsDir := filepath.Join(dir, "tests")
|
||||
err := os.MkdirAll(testsDir, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
runner := DetectTestRunner(dir)
|
||||
assert.Equal(t, TestRunnerPHPUnit, runner)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuildPestCommand_Good(t *testing.T) {
|
||||
t.Run("basic command", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
opts := TestOptions{Dir: dir}
|
||||
|
||||
cmd, args := buildPestCommand(opts)
|
||||
assert.Equal(t, "pest", cmd)
|
||||
assert.Empty(t, args)
|
||||
})
|
||||
|
||||
t.Run("with filter", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
opts := TestOptions{Dir: dir, Filter: "UserTest"}
|
||||
|
||||
_, args := buildPestCommand(opts)
|
||||
assert.Contains(t, args, "--filter")
|
||||
assert.Contains(t, args, "UserTest")
|
||||
})
|
||||
|
||||
t.Run("with parallel", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
opts := TestOptions{Dir: dir, Parallel: true}
|
||||
|
||||
_, args := buildPestCommand(opts)
|
||||
assert.Contains(t, args, "--parallel")
|
||||
})
|
||||
|
||||
t.Run("with coverage", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
opts := TestOptions{Dir: dir, Coverage: true}
|
||||
|
||||
_, args := buildPestCommand(opts)
|
||||
assert.Contains(t, args, "--coverage")
|
||||
})
|
||||
|
||||
t.Run("with coverage HTML format", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
opts := TestOptions{Dir: dir, Coverage: true, CoverageFormat: "html"}
|
||||
|
||||
_, args := buildPestCommand(opts)
|
||||
assert.Contains(t, args, "--coverage-html")
|
||||
assert.Contains(t, args, "coverage")
|
||||
})
|
||||
|
||||
t.Run("with coverage clover format", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
opts := TestOptions{Dir: dir, Coverage: true, CoverageFormat: "clover"}
|
||||
|
||||
_, args := buildPestCommand(opts)
|
||||
assert.Contains(t, args, "--coverage-clover")
|
||||
assert.Contains(t, args, "coverage.xml")
|
||||
})
|
||||
|
||||
t.Run("with groups", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
opts := TestOptions{Dir: dir, Groups: []string{"unit", "integration"}}
|
||||
|
||||
_, args := buildPestCommand(opts)
|
||||
assert.Contains(t, args, "--group")
|
||||
assert.Contains(t, args, "unit")
|
||||
assert.Contains(t, args, "integration")
|
||||
})
|
||||
|
||||
t.Run("uses vendor binary when exists", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
binDir := filepath.Join(dir, "vendor", "bin")
|
||||
err := os.MkdirAll(binDir, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
pestPath := filepath.Join(binDir, "pest")
|
||||
err = os.WriteFile(pestPath, []byte("#!/bin/bash"), 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
opts := TestOptions{Dir: dir}
|
||||
cmd, _ := buildPestCommand(opts)
|
||||
assert.Equal(t, pestPath, cmd)
|
||||
})
|
||||
|
||||
t.Run("all options combined", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
opts := TestOptions{
|
||||
Dir: dir,
|
||||
Filter: "Test",
|
||||
Parallel: true,
|
||||
Coverage: true,
|
||||
CoverageFormat: "html",
|
||||
Groups: []string{"unit"},
|
||||
}
|
||||
|
||||
_, args := buildPestCommand(opts)
|
||||
assert.Contains(t, args, "--filter")
|
||||
assert.Contains(t, args, "--parallel")
|
||||
assert.Contains(t, args, "--coverage-html")
|
||||
assert.Contains(t, args, "--group")
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuildPHPUnitCommand_Good(t *testing.T) {
|
||||
t.Run("basic command", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
opts := TestOptions{Dir: dir}
|
||||
|
||||
cmd, args := buildPHPUnitCommand(opts)
|
||||
assert.Equal(t, "phpunit", cmd)
|
||||
assert.Empty(t, args)
|
||||
})
|
||||
|
||||
t.Run("with filter", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
opts := TestOptions{Dir: dir, Filter: "UserTest"}
|
||||
|
||||
_, args := buildPHPUnitCommand(opts)
|
||||
assert.Contains(t, args, "--filter")
|
||||
assert.Contains(t, args, "UserTest")
|
||||
})
|
||||
|
||||
t.Run("with parallel uses paratest", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
binDir := filepath.Join(dir, "vendor", "bin")
|
||||
err := os.MkdirAll(binDir, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
paratestPath := filepath.Join(binDir, "paratest")
|
||||
err = os.WriteFile(paratestPath, []byte("#!/bin/bash"), 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
opts := TestOptions{Dir: dir, Parallel: true}
|
||||
cmd, _ := buildPHPUnitCommand(opts)
|
||||
assert.Equal(t, paratestPath, cmd)
|
||||
})
|
||||
|
||||
t.Run("parallel without paratest stays phpunit", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
opts := TestOptions{Dir: dir, Parallel: true}
|
||||
|
||||
cmd, _ := buildPHPUnitCommand(opts)
|
||||
assert.Equal(t, "phpunit", cmd)
|
||||
})
|
||||
|
||||
t.Run("with coverage", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
opts := TestOptions{Dir: dir, Coverage: true}
|
||||
|
||||
_, args := buildPHPUnitCommand(opts)
|
||||
assert.Contains(t, args, "--coverage-text")
|
||||
})
|
||||
|
||||
t.Run("with coverage HTML format", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
opts := TestOptions{Dir: dir, Coverage: true, CoverageFormat: "html"}
|
||||
|
||||
_, args := buildPHPUnitCommand(opts)
|
||||
assert.Contains(t, args, "--coverage-html")
|
||||
assert.Contains(t, args, "coverage")
|
||||
})
|
||||
|
||||
t.Run("with coverage clover format", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
opts := TestOptions{Dir: dir, Coverage: true, CoverageFormat: "clover"}
|
||||
|
||||
_, args := buildPHPUnitCommand(opts)
|
||||
assert.Contains(t, args, "--coverage-clover")
|
||||
assert.Contains(t, args, "coverage.xml")
|
||||
})
|
||||
|
||||
t.Run("with groups", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
opts := TestOptions{Dir: dir, Groups: []string{"unit", "integration"}}
|
||||
|
||||
_, args := buildPHPUnitCommand(opts)
|
||||
assert.Contains(t, args, "--group")
|
||||
assert.Contains(t, args, "unit")
|
||||
assert.Contains(t, args, "integration")
|
||||
})
|
||||
|
||||
t.Run("uses vendor binary when exists", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
binDir := filepath.Join(dir, "vendor", "bin")
|
||||
err := os.MkdirAll(binDir, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
phpunitPath := filepath.Join(binDir, "phpunit")
|
||||
err = os.WriteFile(phpunitPath, []byte("#!/bin/bash"), 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
opts := TestOptions{Dir: dir}
|
||||
cmd, _ := buildPHPUnitCommand(opts)
|
||||
assert.Equal(t, phpunitPath, cmd)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTestOptions_Struct(t *testing.T) {
|
||||
t.Run("all fields accessible", func(t *testing.T) {
|
||||
opts := TestOptions{
|
||||
Dir: "/test",
|
||||
Filter: "TestName",
|
||||
Parallel: true,
|
||||
Coverage: true,
|
||||
CoverageFormat: "html",
|
||||
Groups: []string{"unit"},
|
||||
Output: os.Stdout,
|
||||
}
|
||||
|
||||
assert.Equal(t, "/test", opts.Dir)
|
||||
assert.Equal(t, "TestName", opts.Filter)
|
||||
assert.True(t, opts.Parallel)
|
||||
assert.True(t, opts.Coverage)
|
||||
assert.Equal(t, "html", opts.CoverageFormat)
|
||||
assert.Equal(t, []string{"unit"}, opts.Groups)
|
||||
assert.NotNil(t, opts.Output)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTestRunner_Constants(t *testing.T) {
|
||||
t.Run("constants are defined", func(t *testing.T) {
|
||||
assert.Equal(t, TestRunner("pest"), TestRunnerPest)
|
||||
assert.Equal(t, TestRunner("phpunit"), TestRunnerPHPUnit)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRunTests_Bad(t *testing.T) {
|
||||
t.Skip("requires PHP test runner installed")
|
||||
}
|
||||
|
||||
func TestRunParallel_Bad(t *testing.T) {
|
||||
t.Skip("requires PHP test runner installed")
|
||||
}
|
||||
|
||||
func TestRunTests_Integration(t *testing.T) {
|
||||
t.Skip("requires PHP/Pest/PHPUnit installed")
|
||||
}
|
||||
|
||||
func TestBuildPestCommand_CoverageOptions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
coverageFormat string
|
||||
expectedArg string
|
||||
}{
|
||||
{"default coverage", "", "--coverage"},
|
||||
{"html coverage", "html", "--coverage-html"},
|
||||
{"clover coverage", "clover", "--coverage-clover"},
|
||||
{"unknown format uses default", "unknown", "--coverage"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
opts := TestOptions{
|
||||
Dir: dir,
|
||||
Coverage: true,
|
||||
CoverageFormat: tt.coverageFormat,
|
||||
}
|
||||
|
||||
_, args := buildPestCommand(opts)
|
||||
|
||||
// For unknown format, should fall through to default
|
||||
if tt.coverageFormat == "unknown" {
|
||||
assert.Contains(t, args, "--coverage")
|
||||
} else {
|
||||
assert.Contains(t, args, tt.expectedArg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPHPUnitCommand_CoverageOptions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
coverageFormat string
|
||||
expectedArg string
|
||||
}{
|
||||
{"default coverage", "", "--coverage-text"},
|
||||
{"html coverage", "html", "--coverage-html"},
|
||||
{"clover coverage", "clover", "--coverage-clover"},
|
||||
{"unknown format uses default", "unknown", "--coverage-text"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
opts := TestOptions{
|
||||
Dir: dir,
|
||||
Coverage: true,
|
||||
CoverageFormat: tt.coverageFormat,
|
||||
}
|
||||
|
||||
_, args := buildPHPUnitCommand(opts)
|
||||
|
||||
if tt.coverageFormat == "unknown" {
|
||||
assert.Contains(t, args, "--coverage-text")
|
||||
} else {
|
||||
assert.Contains(t, args, tt.expectedArg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPestCommand_MultipleGroups(t *testing.T) {
|
||||
t.Run("adds multiple group flags", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
opts := TestOptions{
|
||||
Dir: dir,
|
||||
Groups: []string{"unit", "integration", "feature"},
|
||||
}
|
||||
|
||||
_, args := buildPestCommand(opts)
|
||||
|
||||
// Should have --group for each group
|
||||
groupCount := 0
|
||||
for _, arg := range args {
|
||||
if arg == "--group" {
|
||||
groupCount++
|
||||
}
|
||||
}
|
||||
assert.Equal(t, 3, groupCount)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuildPHPUnitCommand_MultipleGroups(t *testing.T) {
|
||||
t.Run("adds multiple group flags", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
opts := TestOptions{
|
||||
Dir: dir,
|
||||
Groups: []string{"unit", "integration"},
|
||||
}
|
||||
|
||||
_, args := buildPHPUnitCommand(opts)
|
||||
|
||||
groupCount := 0
|
||||
for _, arg := range args {
|
||||
if arg == "--group" {
|
||||
groupCount++
|
||||
}
|
||||
}
|
||||
assert.Equal(t, 2, groupCount)
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue